Skip to content
quarry Artifact [039f74658c]
Login

Artifact [039f74658c]

Artifact 039f74658ccf40f1e36e03942a08f61358b632c66e09ec44684037404d4da22f:


// stylesheet.go is the whole stylesheet as written: the palette
// tokens and the rules, the two constants every page's appearance
// comes from.
//
// They were 4,685 of web.go's 32,153 lines, sitting between the page
// templates and the handlers. Their own file for two reasons.
//
// The tests already treat them as a unit and name them directly --
// styleSource is baseHTML plus rawSheet, palette_test.go reads
// paletteCSS by name, sheet_test.go and stylesource_test.go both
// reach for the literal -- so this codifies a seam that was already
// there in everything but the file layout.
//
// And the drift these accumulated is the kind that comes from living
// inside something too large to hold at once: three severity tokens
// read by thirteen rules and declared nowhere, a fallback colour that
// never met the contrast floor because it sat outside the block the
// floor inspects, and two dark selectors carrying different token
// sets. None of that survives being looked at. All of it survived
// being scrolled past.
//
// What stays in web.go is composition, not authoring: rawSheet,
// leanCSS and the hashed asset URL are about SERVING these bytes.

package web

// darkTokens is the dark theme's whole token set, written once and
// used by both dark selectors: the OS-preference media query and the
// explicit data-theme stamp. Two hand-maintained copies is how the
// stamp came to be missing the diff colours.
const darkTokens = `
    --fg: #f0ece4; --bg: #1d2021; --muted: #bdb6ab; --code-bg: #32302f;
    --rule: #504945; --accent: #d99058; --uname-l: 70%;
    color-scheme: dark; --idn-l: 72%; --slice-empty: #3a3735;
    --mark: #665418; --add: #b8bb26; --del: #fc8c7f;
    --add-row: #2c3122; --del-row: #382724; --add-mark: #384226; --del-mark: #4a2d27;
    --code-block: #32302f;
    --syn-kw: #fc8c7f; --syn-str: #b8bb26; --syn-num: #da99aa; --syn-fn: #fabd2f;
    --syn-type: #8ec07c; --syn-com: #b6a997; --syn-builtin: #fe8e32; --syn-attr: #95b2a7;
    --surface: #a899841a;
    --warn: #e3b341;
  `

// paletteCSS is every colour the site uses, in one place so the
// contrast tests can find them all. A colour defined anywhere else
// escapes those tests, which is how the highlight and the danger red
// came to be wrong in one of the three theme states.
const paletteCSS = `/* Gruvbox, after craftdesign.group: a warm cream rather than white and a
   soft near-black rather than pure black, because a forge is read for
   long stretches and maximum contrast is tiring rather than legible.
   --accent carries links and focus. Every pair here is contrast-checked
   against both the page ground and --code-bg, the secondary surface
   links also sit on; see TestPaletteContrast, which fails the build
   rather than letting a nicer-looking colour quietly drop below AA.

   Three states, not two. An explicit choice stamps data-theme on the
   root; the default "system" stamps nothing, so only the media query
   separates light from dark. Each set is therefore declared three times:
   once bare, once under the media query GUARDED against an explicit
   light choice, and once under the explicit dark choice — otherwise
   picking light on a dark-mode phone would do nothing. */
:root {
  --fg: #282828; --bg: #f3ecda; --muted: #665c54; --code-bg: #e6dcc0;
  --rule: #d5c4a1; --accent: #a04412; --uname-l: 32%;
  --mark: #f7dd9a; --add: #605c0b; --del: #9d0006;
  /* SEVERITY. --ok and --danger are the diff palette under another
     name: in this design language "removed" and "wrong" are one red,
     "added" and "right" one green, and a second pair would be two
     things to keep tuned. Declared once, not per theme, because a
     custom property resolves at use time -- redefining --del below
     moves --danger with it.

     --warn is its own amber because nothing else in the palette is
     one. Its old value was not a token at all: nine rules read
     var(--warn, ...) for a property never declared anywhere, so they
     all rendered the fallback -- #b60 in six places and #b3261e in
     two, the same severity in two different colours. #b60 scores 3.55
     against the cream ground, BELOW AA, and it escaped the contrast
     floor precisely because it lived outside this block where
     TestPaletteColoursAreAllChecked could not see it. */
  --ok: var(--add); --danger: var(--del); --warn: #7a4a00;
  --add-row: #e8f0d2; --del-row: #fae4dc; --add-mark: #d3e4ae; --del-mark: #f4cbbd;
  /* --code-block is the ground under a code block and a diff. In light
     it is paper, a step LIGHTER than the cream page, so code stands out
     the way the dark theme's block does; the tan --code-bg stays for
     inline code spans. */
  --code-block: #fbf8ef;
  /* SYNTAX. Gruvbox's hues, each moved in lightness only until it reads
     at 4.5:1 on the code ground, the page, both diff rows and both
     word marks; highlight.CSS refers to these and nothing else. */
  --syn-kw: #9d0006; --syn-str: #605c0b; --syn-num: #8d3e70; --syn-fn: #7c510e;
  --syn-type: #356246; --syn-com: #62584d; --syn-builtin: #a23603; --syn-attr: #076273;
  /* --surface is the neutral wash under cards and the top bar. It is a
     warm grey at 10% alpha rather than a colour per theme: one value,
     and it tints whatever ground it lands on instead of fighting it.
     --code-bg stays opaque, because code needs a solid block. */
  --surface: #a899841a;
  /* People: hue and chroma band are identity (idn), lightness is the
     theme's, so the same person reads the same on both grounds without
     the wheel being recomputed. OKLCH keeps every hue equally loud. */
  --idn-l: 56%; --idn-c0: 0.17; --idn-c1: 0.10;
  /* A cell with nothing in it — a day nobody worked, or under a filter
     a day that was not this person's. Its own token because --surface
     is too faint to read as a cell and --rule is a line colour. */
  --slice-empty: #e2d8bf;
  /* What the browser paints the parts of a control it draws itself:
     the datetime picker's calendar glyph, spinners, scrollbars. Absent,
     every one of them is drawn for a light page, and on the dark ground
     the calendar icon is near-black on near-black. */
  color-scheme: light;
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {` + darkTokens + `}
}
/* An explicit DARK choice gets the same tokens as a dark OS. It used
   to get only nine of them: fg, bg, muted, code-bg, rule, accent,
   uname-l, idn-l and slice-empty. Every diff colour was left at its
   LIGHT value, so a reader who picked dark from the menu got pale
   green and pink diff rows on a near-black page, and a dark red --del
   with nothing like enough contrast against it. The media query had
   the full set and the explicit stamp did not, which is a drift only
   the menu path shows -- and nothing walks the menu path by hand. One
   list now, used twice. */
:root[data-theme="dark"] {` + darkTokens + `}
/* An explicit LIGHT choice has to beat a dark OS, native controls
   included — the media block above already lost that fight on tokens
   via :root:not([data-theme="light"]), and this is its other half. */
:root[data-theme="light"] { color-scheme: light; }
/* The page paints its own ground: a transparent body borrows whatever
   the browser decided, which is the white flash this palette exists to
   avoid. */
html { background: var(--bg); }
/* The rubber-band area past the top of the page painted --bg while the
   bar above it is --surface, so overscrolling showed a stripe of the
   wrong colour behind the header. Refusing the scroll chain is the
   direct fix and does not require painting html a colour that would
   then be wrong at the other end. */
html, body { overscroll-behavior: none; }
/* iOS Safari "text autosizing" inflates text inside wide scrollable
   blocks — code in a phone-width pre rendered LARGER than the prose
   around it. 100% keeps every font at its declared size; this is the
   documented opt-out, not a workaround. */
html { -webkit-text-size-adjust: 100%; text-size-adjust: 100%; }`

// It used to be inlined in every response: 112,118 bytes of <style>
// plus a 13,377-byte icon sprite, 40% of a 309,584-byte timeline page,
// byte-identical on every request and — being inline — impossible to
// cache. The JS assets already carried Cache-Control; the CSS could
// not, because it had no URL. Served as a file it is fetched once and
// then costs nothing at all, which beats any compression setting.
// sheetLiteral is a RAW STRING: a backtick anywhere inside it closes
// the literal, and the compiler then reports a syntax error thousands
// of lines below the edit that caused it. Quote CSS property names in
// comments plainly, without them.
const sheetLiteral = `
/* Chat and inbox used to carry their own inline <style> blocks. The
   strict CSP (style-src 'self') drops a <style> ELEMENT — style-src-attr
   covers the style="" attribute and nothing else — so the browser kept
   the tag in the DOM, gave it no sheet, and applied none of it. Silently:
   no console error a reader would see, no failing test, just a chat log
   rendering as a numbered list and an inbox bar that never became a flex
   row. They live here now, in the one served, hashed, cacheable sheet,
   which is where the CSP was written to insist they be. */
/* The account page's own layout, moved here from two inline <style>
   elements it used to carry.
   
   The strict CSP (style-src 'self') drops a <style> ELEMENT, and drops
   it SILENTLY: the browser keeps the tag in the DOM, gives it no
   sheet, and applies none of it. So /me had no two-column rail and no
   notification matrix styling at all — its rail stacked above the
   content instead of sitting beside it. The chat log and the inbox
   were moved here for exactly this reason and the comment above says
   so; these two were missed.
   
   Nothing about the rules changed. They are the same bytes, in the
   one served, hashed, cacheable sheet the CSP was written to insist
   they be. */
.pref-matrix{border-collapse:collapse;width:100%;font-size:var(--fs-body)}
.pref-matrix th,.pref-matrix td{padding:var(--sp-2);border-bottom:1px solid var(--rule);text-align:left}
.pref-matrix th.c,.pref-matrix td.c{text-align:center}
.pref-matrix td.when{color:var(--fg)}
.pref-when{color:var(--muted);font-size:var(--fs-sm);white-space:nowrap}
.pref-unknown{color:var(--muted);font-weight:600;cursor:help}
/* The agent badge. Quiet on purpose: it repeats on every row a machine
   wrote, and a loud one would make an automated timeline unreadable
   while telling the reader nothing they cannot get from its shape. It
   is a FACT about the author, not a warning about the content. */
.agent-tag{font-size:0.7em;text-transform:uppercase;letter-spacing:0.06em;
  color:var(--muted);border:1px solid var(--rule);border-radius:var(--r-4);
  padding:0 0.35em;margin-left:0.35em;vertical-align:0.1em;white-space:nowrap;cursor:help}
.pref-note{color:var(--muted);font-size:var(--fs-md);margin:var(--sp-2) 0}
.pref-switches{margin:var(--sp-4) 0;display:flex;flex-direction:column;gap:var(--sp-2)}
.pref-banner{border-left:3px solid var(--accent);padding-left:var(--sp-2);margin:var(--sp-3) 0}
.pref-mutes{list-style:none;margin:var(--sp-2) 0;padding:0;display:flex;
  flex-wrap:wrap;gap:var(--sp-2)}
.pref-mutes li{display:flex;gap:var(--sp-1);align-items:center;
  border:1px solid var(--rule);border-radius:var(--r-1);padding:0 var(--sp-2)}
.pref-unmute{background:none;border:0;padding:0;font:inherit;font-size:var(--fs-sm);
  color:var(--muted);text-decoration:underline;cursor:pointer}
.pref-unmute:hover{color:var(--accent)}
.pushrow{margin-top:var(--sp-6);border-top:1px solid var(--rule);padding-top:var(--sp-3)}
.push-head{display:flex;align-items:baseline;flex-wrap:wrap;gap:var(--sp-2)}
.push-head b{font-size:var(--fs-lg)}
.push-state{color:var(--muted);font-size:var(--fs-sm);border:1px solid var(--rule);
  border-radius:var(--r-4);padding:0 var(--sp-2);white-space:nowrap}
.pushrow[data-push="on"] .push-state{color:var(--add);
  border-color:color-mix(in srgb,var(--add) 45%,var(--rule))}
.push-acts{display:flex;flex-wrap:wrap;gap:var(--sp-2);margin-top:var(--sp-3)}
.push-acts .btn{white-space:nowrap}
.push-nojs{display:block;color:var(--muted);font-size:var(--fs-sm);margin-top:var(--sp-3);
  max-width:60ch}
.push-msg{display:block;color:var(--muted);font-size:var(--fs-md);margin-top:var(--sp-2)}
.push-msg[data-push-err]{color:var(--del)}
.push-devices{list-style:none;margin:var(--sp-4) 0 0;padding:0;font-size:var(--fs-md)}
.push-devices li{display:flex;flex-wrap:wrap;align-items:baseline;gap:var(--sp-2);
  padding:var(--sp-2) 0;border-top:1px solid var(--rule)}
.push-devices .pd-name{color:var(--fg);font-weight:600}
.push-devices .pd-when,.push-devices .pd-pending{color:var(--muted);font-size:var(--fs-sm)}
.push-devices .pd-pending{font-style:italic}
.acct-email-form{display:flex;gap:var(--sp-2);flex-wrap:wrap;margin-top:var(--sp-2)}
.email-unset{color:var(--muted);font-size:var(--fs-sm)}
.email-unset a{white-space:nowrap}
.acct-rail{display:grid;grid-template-columns:12rem minmax(0,1fr);gap:0 var(--sp-6);align-items:start}
.acct-rail>.set-body>section{padding:var(--sp-2) 0 var(--sp-5);border-bottom:1px solid var(--rule);scroll-margin-top:0.8rem}
.acct-rail>.set-body>section:last-child{border-bottom:0}
.acct-rail h2{font-size:var(--fs-lg);margin:var(--sp-2) 0 var(--sp-2)}
@media (max-width:720px){.acct-rail{display:block}
.set-rail{position:sticky;top:0;z-index:4;background:var(--bg)}
.set-rail ul{display:flex;gap:var(--sp-1);border-left:0;overflow-x:auto}
.set-rail li{display:contents}
.set-rail a{display:inline-block;margin:0;padding:var(--sp-1) var(--sp-3);border-left:0;border-bottom:2px solid transparent;white-space:nowrap}
.set-rail a[aria-current]{border-color:var(--accent)}}
.chatwrap{position:relative}
.chatlog{overflow-y:auto;border:1px solid var(--rule);border-radius:var(--r-2);
  padding:var(--sp-2) var(--sp-3);margin:0 0 var(--sp-2);list-style:none}
.chatlog>li{margin:0}
{{- /* The forum's own vocabulary at chat density, but at a chat
       product's proportions: a 44px gutter holding a 32px mark (an
       identicon is square art, so it takes a radius, never a circle),
       the author's hue as a 2px rail down the whole group, and the
       run's rows separated by 2px against 14px between groups. The
       rail is drawn with PADDING inside a group rather than margin, so
       it runs unbroken from the first row of a run to the last. */ -}}
.chatmsg{position:relative;display:grid;grid-template-columns:34px 1fr;
  align-items:start;margin:0;padding:var(--sp-0) var(--sp-2);
  border-left:2px solid oklch(var(--idn-l) var(--uc, var(--idn-c0)) var(--uh, 0) / 0.6);
  border-radius:0 var(--r-row) var(--r-row) 0;
  transition:background var(--t-fast)}
{{- /* A robot line is the channel talking about the repository, not
       a person with a column: no author hue, a muted rail, muted text,
       and a small mark where a name would be. */ -}}
.chatmsg[data-kind="robot"]{border-left-color:var(--rule);color:var(--muted)}
.chatmsg[data-kind="robot"] .chat-head b{font-weight:600;color:var(--muted)}
.chat-robot{display:inline-block;border:1px solid var(--rule);border-radius:var(--r-1);
  padding:0 var(--sp-1);margin-right:var(--sp-1);font-size:var(--fs-xs);font-weight:600;
  letter-spacing:0.06em;text-transform:uppercase;color:var(--muted);vertical-align:1px}
.chatlog>li:not(:first-child)>.chatmsg:not([data-grouped]){margin-top:var(--sp-3)}
{{- /* One step toward the rule, not a new colour: the row lifts off
       the page without introducing a surface the palette never
       declared. The flat wash is the fallback where color-mix is not
       understood. */ -}}
.chatmsg:hover,.chatmsg:focus-within{background:var(--surface)}
.chatmsg:hover,.chatmsg:focus-within{background:color-mix(in srgb, var(--rule) 35%, transparent)}
.chat-gut{grid-column:1;grid-row:1 / -1;display:flex;align-items:flex-start;padding-top:var(--sp-0)}
.chatmsg>*:not(.chat-gut){grid-column:2;min-width:0}
/* Sized to the name it sits beside, like every other byline in the
   forge. At 32px it read as an avatar in a contacts list rather than a
   mark against a line of text. */
.chat-gut .idn{width:20px;height:20px;border-radius:var(--r-1);vertical-align:0}
{{- /* A continuation row has no header, so the gutter carries the one
       thing it would otherwise never say: the absolute time, on hover,
       where the mark would have been. */ -}}
.chat-clock{font-size:var(--fs-xs);color:var(--muted);font-variant-numeric:tabular-nums;
  opacity:0;transition:opacity var(--t-fast)}
.chatmsg:hover .chat-clock,.chatmsg:focus-within .chat-clock{opacity:1}
.chat-head{display:block;line-height:1.4}
.chat-head b{font-size:var(--fs-body);font-weight:700;color:var(--fg)}
.chat-when{font-size:var(--fs-sm);font-weight:400;opacity:0.55}
.chatbody{display:block;font-size:var(--fs-body);line-height:1.5}
.chatbody h2,.chatbody h3,.chatbody h4,.chatbody h5,.chatbody h6{
  margin:0.4em 0 0.15em;line-height:1.3}
.chatbody h2{font-size:var(--fs-lg)}
.chatbody h3{font-size:var(--fs-lg)}
.chatbody h4,.chatbody h5,.chatbody h6{font-size:var(--fs-body)}
.chatbody>p{margin:var(--sp-1) 0}
.chatbody>p:first-child{margin-top:0}
.chatbody>p:last-child{margin-bottom:0}
{{- /* The quote is a card, not a blockquote: mark, name, number, one
       line, all on one row, and the whole card is the permalink. A
       reply-to quote and an inline #seq quote are the same card. */ -}}
.chat-quote{display:flex;align-items:center;gap:var(--sp-1);overflow:hidden;
  border-left:2px solid var(--rule);padding:var(--sp-1) var(--sp-2);margin:var(--sp-1) 0;
  background:var(--surface);background:color-mix(in srgb, var(--rule) 12%, transparent);
  border-radius:0 var(--r-row) var(--r-row) 0;font-size:var(--fs-md);
  color:var(--muted);text-decoration:none;white-space:nowrap}
.chat-quote .idn{width:16px;height:16px;border-radius:var(--r-1);flex:none;vertical-align:0}
.chat-quote b{color:var(--fg);font-weight:600}
.chat-quote:hover{background:color-mix(in srgb, var(--rule) 22%, transparent)}
.chatreply{font-size:var(--fs-md);color:var(--muted)}
{{- /* On touch the toolbar is simply there, under the row and to the
       right — "point at it" is not an instruction a touch screen can
       follow. Where there IS a pointer it becomes a floating box at
       the row's top-right, appearing on hover or focus. */ -}}
.chat-toolbar{display:flex;gap:var(--sp-0);align-items:flex-start;justify-content:flex-end;flex-wrap:wrap;
  margin-top:0;font-size:var(--fs-sm);color:var(--muted)}
{{- /* A real tap target. The glyph is 16px because that is what reads
       right beside 0.9rem text; the BOX around it is 36px, because
       that is what a thumb hits. Padding does that without making the
       icon bigger, and the row is only as tall as the icon thanks to
       the negative margin. 36 is short of the 44 the platform guides
       ask for; these sit at the end of a row with nothing dangerous
       beside them, and 44 would make the toolbar taller than the
       message. */ -}}
{{- /* Nothing here sets the display property on a <summary>. Changing
       a summary's display is a documented way to lose the disclosure's
       own behaviour in some engines, and what is behind these ones is
       an edit form and a delete confirmation — the two-step guard on
       the only irreversible action in the app. Not worth the risk for
       a box we can put on an inner span instead, so the tap target is
       that span and the summary keeps the box the UA gives it. */ -}}
.chat-icon{color:var(--muted);cursor:pointer;list-style:none;text-decoration:none}
.chat-icon::-webkit-details-marker{display:none}
.chat-icon-box{display:inline-grid;place-items:center;width:32px;height:32px;
  vertical-align:middle;border-radius:var(--r-2)}
.chat-icon .ic{width:16px;height:16px}
.chat-icon:hover,.chat-icon:focus-visible{color:var(--fg);text-decoration:none}
.chat-icon:hover>.chat-icon-box,.chat-icon:focus-visible>.chat-icon-box{background:var(--surface)}
{{- /* The open control keeps the pressed look, so the panel below is
       visibly attached to the thing that opened it. */ -}}
details[open]>.chat-icon{color:var(--fg)}
details[open]>.chat-icon>.chat-icon-box{background:var(--surface)}
{{- /* Two modes, and the plain one is the DEFAULT. In flow the panel
       makes the row taller and pushes the conversation down, which is
       a layout shift — but it can never be clipped and never needs
       measuring, so it is what a page with no script gets, and the
       menu is reachable there by scrolling a few pixels.

       Where script is running (the log adds .js at init) the panel
       floats instead: zero layout shift, nothing below it moves, and
       the toggle handler scrolls it into view if it would open past
       the log's edge. That is the enhancement — better, never load
       bearing. */ -}}
.chatmenu{position:relative}
.chatmenu-panel{display:flex;flex-direction:column;align-items:stretch;
  margin:var(--sp-1) 0 var(--sp-0);padding:var(--sp-1);border:1px solid var(--rule);
  border-radius:var(--r-2);box-shadow:var(--shadow-1);
  background-color:var(--bg);
  background-image:linear-gradient(var(--surface),var(--surface));
  min-width:12rem;text-align:left}
.chatlog.js .chatmenu-panel{position:absolute;top:calc(100% + 4px);right:0;
  z-index:3;margin:0}
{{- /* Same rule as above: this class also lands on a <summary>, so the
       block-level sizing is scoped to the elements where setting it
       cannot affect a disclosure. */ -}}
.chatmenu-item{padding:var(--sp-2) var(--sp-2);border-radius:var(--r-1);
  color:var(--fg);text-decoration:none;font-size:var(--fs-sm);cursor:pointer;
  list-style:none;text-align:left;width:100%;box-sizing:border-box}
a.chatmenu-item,button.chatmenu-item{display:block}
.chatmenu-item::-webkit-details-marker{display:none}
.chatmenu-item:hover,.chatmenu-item:focus-visible{background:var(--bg);text-decoration:none}
.chatmenu-item.chat-act-danger{color:var(--del)}
.chatmenu-note{color:var(--muted);cursor:default;font-size:var(--fs-sm)}
.chatdelete>form,.chatpurge>form{margin:0}
.chat-act{color:var(--muted);text-decoration:none;background:none;border:0;padding:0;
  font:inherit;font-size:var(--fs-sm);cursor:pointer}
.chat-act:hover,.chat-act:focus{color:var(--accent);text-decoration:underline}
.chatdelete{display:inline;position:relative}
.chatdelete>summary{cursor:pointer;list-style:none}
.chatdelete>summary::-webkit-details-marker{display:none}
.chatdelete[open]>form{position:absolute;right:0;top:1.6em;z-index:2;
  background:var(--bg);border:1px solid var(--rule);border-radius:var(--r-2);
  padding:var(--sp-1) var(--sp-2);box-shadow:var(--shadow-1);white-space:nowrap}
.chat-act-danger{color:var(--del)}
.chat-act-danger:hover,.chat-act-danger:focus{color:var(--del);text-decoration:underline}
/* THE APP SHELL.
   
   A chat is not a document. It used to grow down the page like one, and
   the arithmetic did not work: the composer began at 697px in a 757px
   window, so its bottom half sat behind the fixed footer and its Send
   button was off the screen entirely. You scrolled the PAGE to reach
   the control you type into, while the log scrolled separately inside
   it — two scrollbars competing for the same gesture.

   Now the window is the frame. Header and repo bar keep their height,
   ONE region scrolls, and the composer is pinned where a composer
   belongs. dvh, not vh: on a phone vh is the height with the browser
   chrome hidden, which is why bottom-anchored controls on mobile sites
   sit under the toolbar until you scroll.

   min-height:0 on every flex child in the chain is what actually lets
   the log scroll instead of the whole column growing — a flex item's
   default min-height is auto, which refuses to shrink below content. */
body.app-shell{height:100dvh;overflow:hidden;display:flex;flex-direction:column;
  padding-bottom:0}
body.app-shell>header,body.app-shell>.repobar{flex:none}
{{- /* min-width:0 as well as min-height:0. A flex item's automatic
       minimum is its MIN-CONTENT width, so one nowrap line anywhere
       inside — the composer's retention note — made the whole column
       as wide as that sentence and pushed the conversation off the
       side of the phone. */ -}}
body.app-shell>main{flex:1;min-height:0;min-width:0;display:flex;flex-direction:column;
  padding:0 var(--sp-3) var(--sp-2);max-width:none;width:auto;margin:0}
{{- /* The room is the page. On a desk the top bar and repo tabs stay —
       they are navigation — but the page heading (the underlined tab
       already says it), the footer and the card border are document
       habits, and they went from a widget in a document to a room by
       leaving. The log takes every pixel that is left. */ -}}
body.app-shell>main>h1{display:none}
body.app-shell>footer{display:none}
body.app-shell .chatwrap{flex:1;min-height:0;min-width:0;display:flex;flex-direction:column;gap:var(--sp-2)}
body.app-shell .chatlog{flex:1;min-height:0;margin:0;border:0;border-radius:0;padding-top:var(--sp-2)}
body.app-shell .composer{flex:none}
{{- /* Width: use it, but not all of it. A line that spans a 1920px
       window is unreadable; a column pinned at 730 wastes the desk.
       The conversation and the composer share one measure, centred. */ -}}
body.app-shell .chatwrap,body.app-shell .composer{width:100%;max-width:1080px;margin-left:auto;margin-right:auto}
{{- /* The rail exists only where there is a pointer and room; the
       phone's room is the whole screen. On a desk main becomes a grid:
       rail down the left, the conversation and its composer stacked
       in the right column. */ -}}
.chat-rail{display:none}
@media (hover: hover) and (pointer: fine) and (min-width: 40.01rem) and (min-height: 32.01rem){
  body.app-shell>main{display:grid;grid-template-columns:224px minmax(0,1fr);
    grid-template-rows:minmax(0,1fr) auto;column-gap:var(--sp-4);row-gap:var(--sp-2)}
  body.app-shell .chat-rail{display:flex;flex-direction:column;grid-column:1;grid-row:1 / -1;min-height:0;
    padding:var(--sp-3) 0 var(--sp-3) var(--sp-1);border-right:1px solid var(--rule)}
  body.app-shell .rail-top,body.app-shell .rail-about{flex:none}
  body.app-shell .rail-channels{flex:1;min-height:0;overflow-y:auto}
  body.app-shell .chatwrap{grid-column:2;grid-row:1}
  body.app-shell .composer{grid-column:2;grid-row:2}
  body.app-shell .perma-sub,body.app-shell .chat-gone,body.app-shell .chat-signin{grid-column:2}
}
.rail-h{margin:0 0 var(--sp-1);padding:0 var(--sp-2);font-size:var(--fs-xs);font-weight:700;letter-spacing:0.1em;
  text-transform:uppercase;color:var(--muted)}
.chat-here+.rail-h{margin-top:var(--sp-4)}
.rail-about{margin-top:var(--sp-4)}
.rail-channels,.chat-here{list-style:none;margin:0;padding:0;font-size:var(--fs-md)}
.rail-channels li{display:flex;align-items:center;justify-content:space-between;gap:var(--sp-2);
  padding:var(--sp-1) var(--sp-2);border-radius:var(--r-1);color:var(--muted)}
.rail-channels li a{color:inherit;text-decoration:none;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.rail-channels li:hover{background:var(--surface);color:var(--fg)}
.rail-channels li.on{color:var(--fg);background:var(--surface);box-shadow:inset 2px 0 0 var(--accent)}
.rail-hash{color:var(--muted);margin-right:var(--sp-0)}
.rail-n{font-size:var(--fs-xs);font-weight:700;background:var(--accent);color:var(--bg);
  border-radius:var(--r-4);padding:0 var(--sp-1);line-height:1.5;flex:none;font-variant-numeric:tabular-nums}
.chat-here li{display:flex;align-items:center;gap:var(--sp-2);padding:var(--sp-1) var(--sp-2);color:var(--fg)}
.chat-here .idn{width:16px;height:16px;border-radius:var(--r-1);vertical-align:0}
.chat-here-none{color:var(--muted);font-style:italic}
.rail-about{padding:0 var(--sp-2);font-size:var(--fs-xs);color:var(--muted);line-height:1.4}
body.app-shell .composer{margin-bottom:var(--sp-2)}
/* The bar exists only where the chrome is hidden. */
.chat-bar{display:none}
@media (max-width: 40rem), (max-height: 32rem){
  /* FULL SCREEN. A conversation on a phone is not a page inside a
     site — it is the screen. Leaving the header, repo bar and footer
     in place cost three bands of vertical space on the axis a chat has
     least of, and left two scroll containers stacked inside a third.
     The bar below carries the channel name and the way out, so nothing
     is lost but the chrome. */
  body.app-shell>header,body.app-shell>.repobar{display:none}
  body.app-shell>main{padding:0}
  .chat-bar{display:flex;align-items:center;justify-content:space-between;
    gap:var(--sp-2);flex:none;
    padding:var(--sp-2) var(--sp-3);border-bottom:1px solid var(--rule);
    background:var(--bg)}
  .chat-bar-name{font-weight:600}
  .chat-bar-x{display:grid;place-items:center;width:2rem;height:2rem;
    border-radius:var(--r-4);color:var(--muted);text-decoration:none}
  .chat-bar-x:hover,.chat-bar-x:focus{background:var(--surface);color:var(--fg)}
  .chat-bar-x .ic{width:1.1rem;height:1.1rem}
  body.app-shell .chatlog{padding-left:var(--sp-3);padding-right:var(--sp-3)}
  {{- /* The gone state has no log to fill the shell, so its pieces
         take the padding the log would have carried. */ -}}
  body.app-shell .perma-sub,body.app-shell .chat-gone{margin-left:var(--sp-3);margin-right:var(--sp-3)}
  {{- /* The composer is inset on a phone, so it must also be NARROWER
         by what it is inset. The desk rule gives it width:100% to
         share the conversation's measure; adding a margin here left
         the width alone, so the panel was 390px wide inside a 390px
         screen with 8px of margin — eight past the right edge, with
         Send hard against it and no gutter to tap beside. Measured at
         390px: the box ran 8 to 400. width:auto lets the margins take
         their room, and it stays stretched because main is a column
         flex. */ -}}
  body.app-shell .composer{margin:var(--sp-2);width:auto;border-radius:var(--r-panel)}
}
.chatedit{display:inline;position:static}
.chatedit>summary{list-style:none}
.chatedit>summary::-webkit-details-marker{display:none}
/* Editing happens IN PLACE, at the width of the message being edited.
   It used to open a 420px popover floating over that message — you
   edited text through a box sitting on top of it, which is the shape
   nothing else uses. Slack, Discord and Element all turn the row
   itself into the editor; the message keeps its position and the
   controls sit under it where a form's controls belong. */
.chatedit[open]{flex:1 0 100%;text-align:right}
.chatedit[open]>form{display:flex;flex-direction:column;gap:var(--sp-1);
  margin:var(--sp-1) 0 0;text-align:left}
.chatedit textarea{width:100%;box-sizing:border-box;font:inherit;font-size:max(var(--fs-base), 1em);resize:vertical;
  min-height:3.2em;background:var(--bg);color:var(--fg);
  border:1px solid var(--rule);border-radius:var(--r-2);padding:0.45em 0.7em}
.chatedit textarea:focus,.chatedit textarea:focus-visible{outline:none;
  border-color:color-mix(in srgb, var(--accent) 45%, var(--rule))}
.chatedit-bar{display:flex;align-items:center;justify-content:flex-end;
  gap:var(--sp-2);font-size:var(--fs-sm);color:var(--muted)}
.chatedit-bar .chat-hint{margin-right:auto}
{{- /* Where there is a pointer the icons rest a little dimmer and come
       up on approach — the convention's good half, which keeps the log
       calm until somebody is actually working with a row. They are
       never HIDDEN: that is what left a touch reader with nothing, and
       0.5 of a muted glyph is still a visible affordance where 0 is
       not. Touch gets no such query and simply sees them. */ -}}
{{- /* Desk density. Where there is a pointer AND room, the actions
       float at the row's top-right and appear on hover — the shape
       every desk chat client uses, and the one the phone cannot use
       because it has no hover. Both remain true at once: the phone
       keeps its row, and the desk gets a third more messages on
       screen. An open editor drops the toolbar back into the flow, so
       the full-width editor is not trapped in a floating box. */ -}}
@media (hover: hover) and (pointer: fine) and (min-width: 40.01rem) and (min-height: 32.01rem){
  .chatlog .chat-toolbar{position:absolute;top:-12px;right:var(--sp-2);margin:0;z-index:2;
    background:var(--bg);border:1px solid var(--rule);border-radius:var(--r-2);
    box-shadow:var(--shadow-1);padding:var(--sp-0);gap:0;
    opacity:0;pointer-events:none;transition:opacity var(--t-fast)}
  .chatmsg:hover .chat-toolbar,.chatmsg:focus-within .chat-toolbar,
  .chatmsg:has(.chatmenu[open]) .chat-toolbar,
  .chatmsg:has(.chatpurge[open]) .chat-toolbar,
  .chatmsg:has(.chatdelete[open]) .chat-toolbar{opacity:1;pointer-events:auto}
  .chatmsg:has(.chatedit[open]) .chat-toolbar{position:static;opacity:1;pointer-events:auto;
    background:none;border:0;box-shadow:none;padding:0;margin-top:var(--sp-1)}
  .chat-icon-box{width:28px;height:28px}
  .chatlog>li:not(:first-child)>.chatmsg:not([data-grouped]){margin-top:var(--sp-2)}
}
@media (hover: hover) and (pointer: fine){
  .chat-toolbar{opacity:0.5;transition:opacity var(--t-fast)}
  .chatmsg:hover .chat-toolbar,.chatmsg:focus-within .chat-toolbar,
  {{- /* ANY open disclosure holds the toolbar at full strength, so a
         confirm button never fades out from under the pointer on its
         way to it. */ -}}
  .chatmsg:has(.chatedit[open]) .chat-toolbar,
  .chatmsg:has(.chatmenu[open]) .chat-toolbar,
  .chatmsg:has(.chatpurge[open]) .chat-toolbar,
  .chatmsg:has(.chatdelete[open]) .chat-toolbar{opacity:1}
}
@media (prefers-reduced-motion: reduce){
  .chatmsg,.chat-toolbar,.chat-clock{transition:none}
}
{{- /* Code, mentions and references, at chat weight. The mention chip
       is scoped to chat: the forum keeps its byline treatment. */ -}}
.chatmsg code{background:var(--code-bg);border-radius:var(--r-1);padding:var(--sp-0) var(--sp-1)}
.chatmsg pre{background:var(--code-bg);border:1px solid var(--rule);
  border-radius:var(--r-2);padding:var(--sp-2) var(--sp-3);overflow-x:auto}
.chatmsg pre code{background:none;padding:0}
.chatmsg .at{background:var(--code-bg);
  background:color-mix(in srgb, var(--accent) 18%, transparent);
  color:var(--accent);border-radius:var(--r-1);padding:0 var(--sp-1);font-weight:600;
  text-decoration:none}
.chatmsg a.ref{opacity:0.6}
.chatmsg a.ref:hover,.chatmsg a.ref:focus{opacity:1}
{{- /* Two pills on a rule: the unread line in the accent, the day
       separator neutral. Same shape, so they read as the same kind of
       thing — a mark in the log, not a message. */ -}}
.chat-divider,.chat-date{display:flex;align-items:center;gap:var(--sp-2);
  margin:var(--sp-4) 0;list-style:none}
/* Said again at the list's own specificity, because .chatlog>li is
   (0,1,1) and .chat-date is (0,1,0): the margin above lost every time
   and the markers sat flush against the row and against each other —
   an "Earlier messages" pill touching the day below it with a 0px gap,
   measured. The same collision zeroed the NEW divider's space on a
   repository's channel, which is where this rule was first wrong. */
.chatlog>li.chat-divider,.chatlog>li.chat-date{margin:var(--sp-4) 0}
.chatlog>li.chat-more{margin:var(--sp-2) 0}
.chat-divider::before,.chat-divider::after{content:"";flex:1;height:1px;background:var(--accent)}
.chat-date::before,.chat-date::after{content:"";flex:1;height:1px;background:var(--rule)}
.chat-divider>span,.chat-date>span{border-radius:var(--r-4);padding:var(--sp-0) var(--sp-2);
  font-size:var(--fs-xs);font-weight:600;letter-spacing:0.05em;text-transform:uppercase;
  white-space:nowrap}
.chat-divider>span{background:var(--accent);color:var(--bg)}
.chat-date>span{background:var(--rule);color:var(--fg)}
{{- /* Jump-to-latest is drawn only when the server KNOWS the reader is
       not at the latest — an older page. Without script that is the
       only honest answer, so with script off the pill is simply
       absent rather than wrong. */ -}}
/* The floating pill hovers over the log, so the log keeps a lane of
   padding at the bottom for it to hover IN. Without that it covers the
   newest message — the one the reader is most likely reading, and the
   one the pill exists to take them to. */
.chatlog{padding-bottom:var(--sp-6)}
{{- /* Glass, not paint. The accent is the page's loudest colour and it
       was spending it on a control that is, by definition, in the way:
       it floats over the message somebody is reading. A blurred, muted
       disc reads as an overlay on the log rather than a call to act. */ -}}
.chat-jump{position:absolute;left:50%;transform:translateX(-50%);bottom:var(--sp-3);
  display:grid;place-items:center;width:2rem;height:2rem;border-radius:var(--r-4);
  background:var(--surface);
  background:color-mix(in srgb, var(--bg) 65%, transparent);
  -webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);
  border:1px solid var(--rule);color:var(--muted);text-decoration:none;
  box-shadow:var(--shadow-1)}
.chat-jump:hover{color:var(--fg)}
{{- /* It waits. Scrolling back a line, or a scroll that overshoots and
       settles, is not a request for a control — and a pill that blinks
       in on every flick of the thumb is noise. Appearing is delayed;
       LEAVING is immediate, because once the reader is at the end the
       control is already wrong. The delay lives on the shown rule, so
       only the transition INTO "shown" carries it. */ -}}
.chat-jump-live{opacity:0;visibility:hidden;
  transition:opacity 120ms ease, visibility 0s linear 120ms}
{{- /* The pill floats over the log, so it floated over an open menu
       too — a caret sitting on top of "Delete". While a disclosure is
       open the reader is working with one message, not looking for the
       end of the channel, so the pill stands down. */ -}}
.chatwrap:has(.chatmenu[open]) .chat-jump,
.chatwrap:has(.chatedit[open]) .chat-jump{opacity:0;visibility:hidden;transition:none}
.chat-jump-live.show{opacity:1;visibility:visible;
  transition:opacity 200ms ease 700ms, visibility 0s linear 700ms}
.chat-jump .ic{width:1rem;height:1rem}
/* The count rides on the caret rather than replacing it, so the control
   is the same shape whether or not anything arrived. */
{{- /* The permalink page. Its whole job is to answer "what were they
       talking about", so the linked message is MARKED rather than
       merely first — with context on both sides, position no longer
       says which message the reader followed. */ -}}
.perma-sub{margin:var(--sp-2) 0 var(--sp-3);font-size:var(--fs-sm);color:var(--muted)}
{{- /* The linked message is ONE card, not a stack of shapes. It used to
       draw an accent stripe down its left edge — beside the author rail
       every message already has — inside a tinted band, around a row
       that kept its own rounded corners and hover wash: four edges and
       three surfaces for one message, which is what read as striated
       and cheap.

       Now: a single hairline in a tint of the accent, all the way
       round, on a surface one step off the page. The message inside
       gives up its own rail and its own corners, because the card is
       already saying everything they said. */ -}}
.chat-anchor{position:relative;margin:var(--sp-3) 0;padding:var(--sp-2) 0 var(--sp-1);
  border:1px solid color-mix(in srgb, var(--accent) 38%, var(--rule));
  border-radius:var(--r-3);
  background:color-mix(in srgb, var(--accent) 5%, var(--surface))}
.chat-anchor>.chatmsg{border-left-color:transparent;border-radius:0;background:none}
.chat-anchor>.chatmsg:hover,.chat-anchor>.chatmsg:focus-within{background:none}
{{- /* The label rides ON the border, the way a fieldset legend does:
       it names the card without taking a band of its own above the
       message. */ -}}
.chat-anchor-mark{position:absolute;top:0;left:var(--sp-3);transform:translateY(-50%);
  margin:0;padding:0 var(--sp-1);font-size:var(--fs-xs);font-weight:700;letter-spacing:0.08em;
  text-transform:uppercase;color:var(--accent);
  background:var(--bg);border-radius:var(--r-1)}
{{- /* Growing the window is a LINK, so it works with no script and the
       browser's own anchor jump keeps the linked message under the
       reader's eye across the reload. */ -}}
.chat-more{list-style:none;text-align:center;margin:var(--sp-2) 0}
.chat-more a{display:inline-block;padding:var(--sp-0) var(--sp-3);border:1px solid var(--rule);
  border-radius:var(--r-4);font-size:var(--fs-xs);color:var(--muted);text-decoration:none}
.chat-more a:hover{color:var(--fg);border-color:var(--muted)}
.chat-gone{border:1px solid var(--rule);border-radius:var(--r-2);padding:var(--sp-3)}
{{- /* Arriving at a message by its fragment — from a permalink's "open
       in the channel" — flashes it. Without this the browser scrolls
       the log to a row that looks exactly like every other row, and
       the reader has to guess which one they were sent to. */ -}}
.chatmsg:target{background:color-mix(in srgb, var(--accent) 14%, transparent);
  box-shadow:inset 2px 0 0 var(--accent);border-radius:var(--r-2)}
@media (prefers-reduced-motion: no-preference){
  .chatmsg:target{animation:chat-found 1.6s ease-out 1}
}
.chatmsg.chat-found{animation:chat-found 1.6s ease-out 1}
@keyframes chat-found{
  0%{background:color-mix(in srgb, var(--accent) 30%, transparent)}
  100%{background:color-mix(in srgb, var(--accent) 14%, transparent)}
}
.chat-jump-n{position:absolute;top:-4px;right:-4px;min-width:1.05rem;height:1.05rem;
  border-radius:var(--r-4);background:var(--fg);color:var(--bg);
  font-size:var(--fs-xs);font-weight:700;line-height:1.05rem;text-align:center;
  padding:0 var(--sp-1);font-variant-numeric:tabular-nums}
{{- /* The composer: one rounded field with a toolbar strip inside its
       own border — the note on the left, the key hint and Send on the
       right, mark-read as a text link between them. The strip is not a
       second control bar under a box; it is part of the box. */ -}}
/* One soft card, and the controls live INSIDE it rather than in a
   compartment ruled off below. The divider made two boxes out of one
   object — the field and "the strip under the field" — when what the
   reader is looking at is a single place where a message is written.
   The border marks the field, and only while it is being used: the
   whole composer used to take a 2px accent ring on focus, loud enough
   to be the brightest thing on the screen while you typed. */
.composer{display:block;min-width:0;margin:0 0 var(--sp-2);background:var(--surface);
  border:1px solid var(--rule);border-radius:var(--r-panel);padding:var(--sp-1) 0 var(--sp-1)}
{{- /* No focus ring on the composer, at either level. Focusing it drew
       TWO: the sheet's global textarea:focus-visible ring inside, and
       the panel's own border outside. A box inside a glowing box,
       every time you tapped to type.

       The global rule for text fields is THREE declarations, not one —
       outline:none, border-color:accent, and a 3px box-shadow halo —
       because a field with a border states focus by colouring the
       border it has rather than drawing a ring outside it. Cancelling
       only the outline left the HALO, which is the ring dmestas
       photographed: .composer-in sets border:0, so the accent border
       was invisible and the shadow was the whole visible ring. Both go.

       A text field states its focus with a caret, and this one is the
       largest, most obvious control on the page; every chat client
       leaves it unringed for the same reason. The rule is scoped to
       THIS field, so every other input in the forge keeps the ring it
       needs to say which of several it is. */ -}}
.composer-in:focus,.composer-in:focus-visible{outline:none;box-shadow:none}
.composer-in{display:block;width:100%;box-sizing:border-box;border:0;background:none;
  color:var(--fg);font:inherit;padding:0.5em 1.05em;resize:none;outline:none;
  field-sizing:content;min-height:2.4em;max-height:9.5em;overflow-y:auto}
.composer-bar{display:flex;align-items:center;gap:var(--sp-1);padding:var(--sp-0) var(--sp-2) 0 var(--sp-2)}
.composer-spring{flex:1}
body:has(.chatlog.js) .chat-read-btn{display:none}
{{- /* The strip is a row of ROUND controls: insert and info at the
       left, preview and Send at the right, the key hint as the quiet
       text between. Send is the one filled circle, because it is the
       only thing here anybody came to do. The retention note left the
       strip entirely — prose in a control row read as neither — and
       lives behind the info button as a disclosure. */ -}}
.cbtn{display:grid;place-items:center;width:34px;height:34px;border-radius:var(--r-4);
  border:1px solid var(--rule);background:none;color:var(--muted);cursor:pointer;
  padding:0;font:inherit;flex:none}
.cbtn:hover,.cbtn:focus-visible{color:var(--fg);background:var(--surface)}
.cbtn .ic{width:16px;height:16px}
.cbtn[disabled]{opacity:0.45;cursor:default}
.cbtn[aria-pressed="true"]{color:var(--fg);background:var(--surface);border-color:var(--muted)}
.cbtn-send{background:var(--accent);border-color:transparent;color:var(--bg)}
.cbtn-send:hover:not(:disabled),.cbtn-send:focus-visible:not(:disabled){filter:brightness(1.08);background:var(--accent);color:var(--bg)}
{{- /* The preview pane sits where the field sits, at the field's own
       padding, so toggling reads as the SAME surface changing state
       rather than a new box appearing. */ -}}
.chat-preview{padding:0.5em 1.05em;font-size:var(--fs-body);min-height:2.4em}
.chat-preview[hidden]{display:none}
.composer-in[hidden]{display:none}
/* The reply band: a band OF the composer, sharing its surface and
   width, a hairline under it the only structure. It copies the shape a
   quote already has in the log — mark, name, seq, one truncated line —
   because it produces one. */
.reply-band{display:flex;align-items:center;gap:var(--sp-2);min-width:0;
  padding:var(--sp-2) var(--sp-3) var(--sp-2) var(--sp-3);border-bottom:1px solid var(--rule)}
.reply-band[hidden]{display:none}
.rb-arw{width:13px;height:13px;color:var(--muted);flex:none}
.rb-idn{flex:none;display:inline-flex}
.rb-idn .idn{width:16px;height:16px;border-radius:var(--r-1);vertical-align:0}
.rb-name{font-size:var(--fs-sm);font-weight:600;white-space:nowrap;flex:none}
.rb-seq{font-size:var(--fs-sm);color:var(--muted);flex:none}
.rb-txt{font-size:var(--fs-sm);color:var(--muted);min-width:0;flex:1;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
{{- /* Rendered, not source — the same renderer as the log — but flat:
       every block the message had becomes inline so the band stays
       one line, and headings give up their size. What survives is
       emphasis, code, links; what is dropped is only the geometry. */ -}}
.rb-txt *{display:inline;margin:0;padding:0;font-size:inherit;font-weight:inherit;
  line-height:inherit;border:0;background:none;white-space:nowrap}
.rb-txt br{display:none}
.rb-txt strong,.rb-txt b{font-weight:600}
.rb-txt code{font-family:var(--font-mono);font-size:0.95em}
.rb-txt p+p::before,.rb-txt blockquote::before{content:" · "}
.rb-x{color:var(--muted);text-decoration:none;flex:none;display:grid;
  place-items:center;width:26px;height:26px;border-radius:var(--r-4);font-size:var(--fs-md)}
.rb-x:hover,.rb-x:focus-visible{color:var(--fg);background:var(--surface)}
.mentions{list-style:none;margin:0 0 var(--sp-1);padding:0 1em;display:flex;
  gap:var(--sp-1);flex-wrap:wrap}
.mentions[hidden]{display:none}
{{- /* The roster's list wraps under the whole row rather than beside
       the field: on a phone the row is already a column. */ -}}
.roster-suggest{flex-basis:100%;padding:0}
.roster-suggest li button{background:none;border:1px solid var(--rule);border-radius:var(--r-1);
  padding:var(--sp-0) var(--sp-2);font:inherit;font-size:var(--fs-sm);color:var(--fg);cursor:pointer}
.roster-suggest li button:hover,.roster-suggest li button:focus-visible{border-color:var(--accent);color:var(--accent)}
.roster-suggest li button .r{color:var(--muted)}
.inbox-bar{display:flex;gap:var(--sp-4);align-items:baseline;font-size:var(--fs-md);
  border-bottom:1px solid var(--rule);padding-bottom:var(--sp-1);margin-bottom:var(--sp-1);
  flex-wrap:wrap}
.inbox-f{color:var(--muted);font-weight:400;text-decoration:none;padding-bottom:var(--sp-1)}
.inbox-f:hover{color:var(--accent)}
.inbox-f.on{color:var(--fg);font-weight:600;box-shadow:inset 0 -2px 0 var(--accent)}
.inbox-n{color:var(--muted);font-weight:400}
.inbox-spacer{flex:1}
.inbox-allread{background:none;border:1px solid var(--rule);border-radius:var(--r-1);
  padding:var(--sp-0) var(--sp-2);font:inherit;font-size:var(--fs-sm);color:var(--muted);cursor:pointer}
.inbox-allread:hover:enabled{color:var(--accent);border-color:var(--accent)}
.inbox-allread:disabled{opacity:0.45;cursor:default}
.attn-rows{list-style:none;margin:0;padding:0}
{{- /* 56px of row, so a thumb has something to hit and the eye has a
       rhythm to follow: the dot, two lines, the time, the actions. */ -}}
.attn-row{display:grid;grid-template-columns:var(--sp-3) minmax(0,1fr) 3.2rem 5.3rem;
  gap:var(--sp-2);align-items:center;min-height:56px;
  padding:var(--sp-2) var(--sp-2);border-bottom:1px solid var(--rule);
  border-radius:var(--r-row);transition:background var(--t-fast)}
.attn-row:last-child{border-bottom:0}
.attn-row:hover{background:var(--surface)}
.attn-row:hover{background:color-mix(in srgb, var(--rule) 35%, transparent)}
.attn-dot{width:8px;height:8px;border-radius:var(--r-4);background:var(--accent)}
{{- /* The row's width belongs to the page: a long reason takes two
       lines and stops, the why-line takes one, and the stamp and the
       control keeps its column whatever the words are, and keeps it
       on a row that has no control at all — a row with nothing to
       press must not shift the stamp of the row above it.
       Before this an eleven-hour-old landing was wider than a
       five-minute-old one and every row sat somewhere different. */ -}}
.attn-reason{font-size:var(--fs-body);font-weight:400;color:var(--fg);text-decoration:none;
  display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;
  overflow-wrap:anywhere}
.attn-row[data-unread="1"] .attn-reason{font-weight:600}
a.attn-reason:hover{color:var(--accent);text-decoration:underline}
.attn-why{font-size:var(--fs-sm);color:var(--muted);margin-top:var(--sp-0);
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.attn-repo{background:var(--rule);color:var(--fg);border-radius:var(--r-1);padding:0 var(--sp-1)}
.attn-when{font-variant-numeric:tabular-nums;font-size:var(--fs-sm);color:var(--muted);
  text-align:right;white-space:nowrap}
.attn-acts{display:flex;gap:var(--sp-1);align-items:center;justify-content:flex-end;
  font-size:var(--fs-sm);white-space:nowrap}
.attn-acts form{display:inline}
{{- /* Done is the action this page exists for, so it is the only
       filled thing on the row; Unfollow is a text link beside it, not
       a second button of equal weight. */ -}}
.attn-done{display:block;width:100%;padding:var(--sp-0) var(--sp-1);background:none;
  border:1px solid var(--rule);border-radius:var(--r-row);
  font:inherit;font-size:var(--fs-xs);color:var(--muted);cursor:pointer;white-space:nowrap}
.attn-done:hover,.attn-done:focus-visible{border-color:var(--accent);color:var(--accent)}

{{- /* Quiet until the row is reached — the chat log's own rule, so a
       long list reads as a list and not as a wall of buttons. Where
       there is no hover there is no "point at it", so they stay. */ -}}
@media (hover: hover) and (pointer: fine){
  .attn-acts{opacity:0;pointer-events:none;transition:opacity var(--t-fast)}
  .attn-row:hover .attn-acts,.attn-row:focus-within .attn-acts{opacity:1;pointer-events:auto}
}
@media (prefers-reduced-motion: reduce){.attn-acts,.attn-row{transition:none}}
{{- /* A fault reached from the inbox is the one the reader was told
       about, so the page says which of its rows that is rather than
       leaving them to find it in a list. */ -}}
.exc-item:target{background:color-mix(in srgb, var(--accent) 10%, transparent);
  border-radius:var(--r-row);scroll-margin-top:var(--sp-6)}
.attn-none{color:var(--muted)}
.inbox-older{margin-top:var(--sp-3);font-size:var(--fs-md)}
.inbox-settings{margin-top:var(--sp-6);border-top:1px solid var(--rule);
  padding-top:var(--sp-3);font-size:var(--fs-body)}
.inbox-settings>summary{cursor:pointer;color:var(--muted);font-size:var(--fs-body)}
.inbox-settings .inbox-pref{margin:var(--sp-3) 0}
.inbox-settings .inbox-pref small{display:block;color:var(--muted);margin-top:var(--sp-1)}
body { color: var(--fg); background: var(--bg); font: var(--fs-base)/1.5 var(--font-sans);
       margin: 0; padding: 0; }
/* The bars span the viewport; their contents, and the page below them,
   share one column. --page is the single place that column's width is
   decided. */
:root { --page: 76rem; }
/* The measure the newer surfaces are drawn on: one spacing scale, one
   radius scale, one shadow, one transition. Written once here so that
   "a bit tighter" is an edit to a number rather than a sweep through
   forty rules, and so two surfaces cannot disagree about what "the
   small gap" is. No colour lives here — colour is the palette's. */
:root {
  /* --sp-0 is the hairline step: the vertical padding of a chip or a
     code span, where 4px would already read as loose. */
  --sp-0: 2px; --sp-1: 4px; --sp-2: 8px; --sp-3: 12px; --sp-4: 16px; --sp-5: 24px; --sp-6: 32px; --sp-7: 48px;
  /* TYPE. One face for prose, one for code, and seven sizes. The app
     had sixty-eight distinct font sizes, most of them between 11 and
     14 px, so nothing stood out from anything; TestTypeStaysOnTheScale
     keeps every size a step here (or an em, relative to its parent). */
  --font-sans: "IBM Plex Sans", system-ui, -apple-system, "Segoe UI", sans-serif;
  --font-mono: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
  --mono: var(--font-mono);
  --fs-xs: 0.75rem; --fs-sm: 0.8125rem; --fs-md: 0.875rem; --fs-body: 0.9375rem;
  --fs-base: 1rem; --fs-lg: 1.25rem; --fs-xl: 1.75rem;
  --r-1: 4px; --r-2: 8px; --r-3: 12px; --r-4: 999px;
  /* A soft panel: rounder than a card, nothing like a pill. */
  --r-panel: 20px;
  /* The in-between radius the rows and their toolbar share: smaller
     than a card, larger than a code span. */
  --r-row: 6px;
  --shadow-1: 0 2px 8px rgba(0, 0, 0, 0.2);
  --t-fast: 150ms;
}
/* One source for the page's horizontal gutter. A full-bleed strip
   subtracts it back out, so a literal that disagrees with this one
   hangs the strip over the edge — which is exactly what the rail did
   at phone width, where the gutter halves and the bleed did not. */
body { --gutter: 1rem; }
main { max-width: var(--page); margin: 0 auto;
  padding-left: var(--gutter); padding-right: var(--gutter); }
/* Room for the fixed footer, so the last line of a page is readable. */
/* One source for the footer's height, so the space reserved for it and
   the bar itself cannot disagree. The gap is deliberate: clearing the
   footer by a hair still reads as content running into it. */
:root { --footer-h: 2.25rem; }
body { padding-bottom: calc(var(--footer-h) + 2rem + env(safe-area-inset-bottom)); }
/* Centred rather than baseline-aligned, so the two halves sit on one
   optical line inside a bar of fixed height. */
footer { font-size: var(--fs-sm); }
/* One line, never two: the page reserves a fixed height for this bar, so
   a bar that wraps overlaps the content it was supposed to sit below.
   The left half truncates instead — version and render time are
   diagnostics, and they are what a narrow screen can best afford to
   lose. The links never shrink. */
footer .bar { display: flex; flex-wrap: nowrap; gap: var(--sp-4); align-items: center;
  min-height: var(--footer-h); padding-top: var(--sp-1); padding-bottom: var(--sp-1); }
.foot-l { min-width: 0; display: flex; white-space: nowrap; }
/* The timing never shrinks and never truncates; the two ahead of it do,
   and drop out entirely when even truncated they would crowd it.
   The per-store BREAKDOWN is not in that protected set. On a phone the
   whole string outruns the viewport on its own, and being flex:none it
   then printed straight over the links rather than giving way — so the
   detail drops at the same width the brand does, and the total, which
   is what the footer is for, survives. */
.f-time { flex: none; }
.f-brand, .f-ver { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
@media (max-width: 34rem) { .f-brand, .f-ops { display: none; } }
@media (max-width: 26rem) { .f-ver { display: none; } }
.foot-r { flex: none; }
.foot-r { margin-left: auto; display: flex; gap: var(--sp-4); white-space: nowrap; }
details > summary { cursor: pointer; padding: var(--sp-2) 0; }
pre.licence { max-height: 24rem; overflow: auto; font-size: var(--fs-sm); white-space: pre-wrap; }
/* The bars use the viewport, not the content column: forgejo puts the
   forge name at the far left and the account at the far right, which
   reads as chrome. Content stays in the column below them. */
.bar { padding-left: var(--gutter); padding-right: var(--gutter); }
.repobar .bar { max-width: var(--page); margin: 0 auto; }
main { padding-top: var(--sp-4); }
/* Present to the document and to a screen reader, absent to the eye:
   a page still needs one h1 even when repeating it on screen would say
   nothing a reader cannot already see. */
/* Explore tabs: the same pill vocabulary as the repo nav, so the two
   navigations do not look like different systems. */
nav.tabs { display: flex; flex-wrap: wrap; gap: var(--sp-1); margin: 0 0 var(--sp-4); }
nav.tabs a { display: inline-flex; align-items: center; gap: var(--sp-1);
  padding: var(--sp-1) var(--sp-3); border-radius: var(--r-4); text-decoration: none;
  border: 1px solid transparent; color: var(--fg); }
nav.tabs a:hover { border-color: var(--rule); }
nav.tabs a.here { background: var(--surface); border-color: var(--rule); font-weight: 600; }
nav.tabs .n { color: var(--muted); font-size: var(--fs-xs); font-variant-numeric: tabular-nums; }
ul.people { margin: 0; }
ul.people li { display: flex; align-items: center; gap: var(--sp-2);
  padding: var(--sp-2) var(--sp-4); }
.avatar.sm { width: 1.75rem; height: 1.75rem; font-size: var(--fs-xs); margin: 0; flex: none; }
.p-main { min-width: 0; }
.p-meta { color: var(--muted); font-size: var(--fs-md); }
.person-head { display: flex; align-items: center; gap: var(--sp-4); margin-bottom: var(--sp-4); }
.person-head h1 { margin: 0; }
/* Said plainly, once, where the conflation would otherwise be read as
   a fact about a person. */
.p-warn { border-left: 3px solid var(--accent); padding: var(--sp-2) var(--sp-3);
  margin: 0 0 var(--sp-4); color: var(--muted); font-size: var(--fs-body); }
.p-warn strong { color: var(--fg); }
.avatar.lg { width: 3rem; height: 3rem; font-size: var(--fs-base); margin: 0; flex: none; }
/* Headings SCALE. The browser default is 2em at every width, which on a
   phone turns a real subject line into three lines of shouting and made
   clipping look necessary — it was not: the clip only hid the words. A
   heading that fits can say the whole thing, and the pathological case
   (a message with no subject line at all, where the title IS the body)
   is already handled by dropping the visible heading entirely. */
h1 { font-size: clamp(var(--fs-lg), 5vw, var(--fs-xl)); line-height: 1.2;
  font-weight: 700; letter-spacing: -0.01em; overflow-wrap: break-word; }
/* The browser's h2 and h3 are 1.5em and 1.17em of whatever they sit in,
   so the same heading measured differently on every page. One step
   each: a section is lg, a subsection base, both semibold. */
h2 { font-size: var(--fs-lg); line-height: 1.3; font-weight: 600; }
h3 { font-size: var(--fs-base); line-height: 1.4; font-weight: 600; }
/* Controls and code otherwise take the browser's own faces: buttons
   and inputs its system UI font, code its default monospace. */
button, input, select, textarea { font-family: inherit; }
code, kbd, samp, pre { font-family: var(--font-mono); }
/* A name inside an uppercase label keeps its own case: branch names
   are case-sensitive, and "A-RUNNER-YOU-DOWNLOAD" names nothing. */
/* A div, not a p: a form inside a <p> closes the paragraph, and the
   'gate history' link fell to a line of its own behind a stray dot. */
.ci-gate-foot { display: flex; align-items: center; flex-wrap: wrap; gap: var(--sp-2); }
.keep-case { text-transform: none; letter-spacing: normal; }
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0; }
.topbar { background: var(--surface); border-bottom: 1px solid var(--rule); }
/* Three columns rather than a flex row: the search belongs in the
   middle of the bar, and centring it with auto margins fights the
   avatar's own push to the right. The side columns take equal width so
   the box is centred on the VIEWPORT, not on whatever is left after the
   forge name — which would move it every time the name changed. */
.topbar .bar { display: grid; grid-template-columns: 1fr minmax(7rem, 34rem) 1fr;
  gap: var(--sp-3); align-items: center; padding-top: var(--sp-3); padding-bottom: var(--sp-3); }
.topbar .site { justify-self: start; }
.bar-acts { justify-self: end; display: flex; align-items: center; gap: var(--sp-2); }
.topbar .find { justify-self: stretch; }
.repobar { border-bottom: 1px solid var(--rule); }
/* One row, always: the collapse exists precisely so the bar never has
   to wrap. The repository name yields (truncating with an ellipsis)
   before the tabs do — the tabs are where the reader is going. */
.repobar .bar { display: flex; gap: var(--sp-3); align-items: baseline; flex-wrap: nowrap;
  padding-top: var(--sp-2); padding-bottom: var(--sp-2); min-width: 0; overflow: clip; }
/* Fixed and full width, like the bar at the top: the page scrolls under
   it. body carries matching bottom padding so the last line of content
   is never hidden behind it. */
footer { position: fixed; bottom: 0; left: 0; right: 0; z-index: 5;
         background: var(--surface); backdrop-filter: blur(6px);
         color: var(--muted); border-top: 1px solid var(--rule); margin: 0;
         /* .bar owns the inner padding. The bottom inset is the iPhone
            home-indicator strip: with Safari's chrome collapsed it
            overlays a bottom:0 bar, and the render time sat under it.
            viewport-fit=cover (the meta tag) is what makes the env()
            non-zero there; everywhere else it is 0 and inert. */
         padding: 0 0 env(safe-area-inset-bottom) 0; }
table { border-collapse: collapse; width: 100%; }
th, td { text-align: left; padding: var(--sp-1) var(--sp-3) var(--sp-1) 0; vertical-align: top; }
/* An action cell is a row of buttons, not a stack of forms. */
td.acts { display: flex; gap: var(--sp-2); align-items: center; padding-top: var(--sp-1); }
td.acts form { display: contents; }
/* A link dressed as the buttons beside it, for actions that are a
   page (edit) rather than a post. */
td.acts a.btn { display: inline-block; background: var(--code-bg); font-weight: 600; font: inherit; font-weight: 600;
  color: var(--fg); text-decoration: none; border: 1px solid var(--rule); border-radius: var(--r-row);
  padding: var(--sp-1) var(--sp-2); line-height: 1.2; }
td.acts a.btn:hover { border-color: var(--fg); }
td.acts button, td.acts a.btn { white-space: nowrap; }
/* The mirrors table: host-only upstream, one-line truncated error, and
   a kebab disclosure for the actions that are not "pull". */
.m-up a { text-decoration: none; }
.m-state .m-err { display: block; max-width: 22rem; overflow: hidden; text-overflow: ellipsis;
  white-space: nowrap; color: var(--muted); }
tr.m-failing .m-state strong { color: var(--accent); }
.kebab-btn { font-weight: 700; line-height: 1.2; padding: var(--sp-1) var(--sp-2); }
/* Without anchor positioning the row menu falls back to the account
   menu's corner; with it, it hangs off its own button. */
@supports (anchor-name: --a) {
  .menu.km { top: anchor(bottom); right: anchor(right); margin-top: var(--sp-1); }
}
form.confirm { margin-top: var(--sp-4); }
/* The rows list: GitHub's list-row shape. Wide, a row is a grid of
   main | actions with the meta line under the name; narrow, the same
   row stacks and the actions drop beneath. Problems get a left rule
   in the failure colour so they read before they are read. */
.fleet-head { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2) var(--sp-4); margin: var(--sp-1) 0 var(--sp-3); }
.fleet-sum { margin: 0; flex: 1 1 14rem; }
.fleet-bulk { display: flex; gap: var(--sp-2); }
.ci-fail-text { color: var(--accent); }
ul.rows { margin: 0 0 var(--sp-4); }
ul.rows > li.row { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: var(--sp-1) var(--sp-4);
  align-items: start; padding: var(--sp-2) var(--sp-4); }
ul.rows > li.row-problem { border-left: 3px solid var(--accent); padding-left: var(--sp-2); }
.row-main { min-width: 0; display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-1) var(--sp-2); }
.row-name { font-weight: 600; text-decoration: none; }
.row-meta { flex-basis: 100%; color: var(--muted); font-size: var(--fs-md); overflow-wrap: anywhere; }
.row-meta .n { font-variant-numeric: tabular-nums; }
.row-err { flex-basis: 100%; color: var(--muted); font-size: var(--fs-md); overflow: hidden;
  text-overflow: ellipsis; white-space: nowrap; }
.row-acts { display: flex; gap: var(--sp-2); align-items: center; white-space: nowrap; }
.row-acts form { display: contents; }
.row-acts button, .row-acts a.btn { font-size: var(--fs-md); padding: var(--sp-1) var(--sp-2); }
.row-acts a.btn { display: inline-block; background: var(--code-bg); font: inherit; font-size: var(--fs-md); font-weight: 600;
  color: var(--fg); text-decoration: none; border: 1px solid var(--rule); border-radius: var(--r-row); line-height: 1.2; }
.tl-badge.m-failing, .tl-badge.ci-fail { border-color: var(--accent); color: var(--accent); }
.tl-badge.m-lagging { color: var(--muted); }
details.rows-fold > summary { cursor: pointer; color: var(--muted); padding: var(--sp-2) var(--sp-1); }
@media (max-width: 40rem) {
  ul.rows > li.row { grid-template-columns: minmax(0, 1fr); }
  .row-acts { justify-content: flex-start; }
  .fleet-bulk { width: 100%; }
  .fleet-bulk form { flex: 1; display: contents; }
  .fleet-bulk button { flex: 1; }
  /* The page must never scroll sideways; a wide table scrolls inside
     its own box. */
  main { overflow-x: clip; }
}
/* A labelled control is a column: label over input. Side-by-side put
   the trailing unit on its own line and made the row unreadable. */
form.addform { display: flex; flex-wrap: wrap; gap: var(--sp-2); align-items: end;
  margin-top: var(--sp-4); padding-top: var(--sp-4); border-top: 1px solid var(--rule); }
form.addform label { display: flex; flex-direction: column; gap: var(--sp-1);
  font-size: var(--fs-md); color: var(--muted); min-width: 0; }
form.addform label.grow { flex: 1 1 16rem; }
form.addform input { max-width: 100%; }
/* Narrow: the row becomes a column. Wrapping alone is not enough,
   because a text input keeps its size attribute's width and
   carries the controls after it off the screen. */
@media (max-width: 34rem) {
  form.addform { flex-direction: column; align-items: stretch; }
  form.addform label, form.addform label.grow { flex: none; }
  form.addform input[type="text"], form.addform input[type="email"],
  form.addform input:not([type]) { width: 100%; }
  form.addform label.tl-meta-inline { flex-direction: row; align-items: center; gap: var(--sp-2); }
}
form.addform input { color: var(--fg); }
form.rowform { margin: var(--sp-3) 0 0; }
th { border-bottom: 1px solid var(--rule); }
/* Links carry the accent rather than inheriting the body colour: with
   inherit the only cue was the underline, and a page of prose and a page
   of links looked identical at a glance. */
a { color: var(--accent); }
/* A focus ring says "here you are", not "look at this". The accent made
   every tab stop shout in the brand colour; --muted is neutral and
   still clears WCAG 1.4.11's 3:1 against both grounds (8.2:1 dark,
   5.5:1 light) — where --rule, the obvious "lighter" choice, is 1.9:1
   and would not legally be an indicator at all. */
a:focus-visible, button:focus-visible, input:focus-visible, select:focus-visible,
textarea:focus-visible { outline: 2px solid var(--muted); outline-offset: 1px; }
/* A text field already has a border, so a ring drawn outside it reads
   as two boxes. Focus colours the border it has and adds a soft halo. */
input:not([type=checkbox]):not([type=radio]):not([type=submit]):not([type=button]):focus-visible,
select:focus-visible, textarea:focus-visible {
  outline: none; border-color: var(--accent);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 28%, transparent);
}
.topbar .site { font-weight: bold; text-decoration: none; display: inline-flex; align-items: center; }
/* The wordmark: QUARRY set in Iosevka Fixed Semibold, traced to
   outlines so no reader downloads a font for six letters. The Q's tail
   descends below the baseline by design — the viewBox keeps it. The
   phone keeps only the round mark. */
/* The box is CAP HEIGHT only — the Q's tail overflows below it, as in
   the source design, so the mark centres on the letters and the tail
   costs no layout space. */
/* Cap height equals the ring's outer diameter: the mark is 1.75rem
   over a 24-unit box whose ring spans 17.2 units (r 7.2 + half the
   2.8 stroke each side), so 1.75 x 17.2/24. */
.topbar .site .wordmark { height: 1.254rem; width: auto; display: block;
  margin-left: var(--sp-2); overflow: visible; align-self: center; }
@media (max-width: 34rem) { .topbar .site .wordmark { display: none; } }
/* Sized in CSS as well as in the attributes: the attributes reserve the
   space before the image arrives (no reflow), the CSS keeps it right if
   the asset is ever replaced at another resolution. */
.topbar .site .mark { width: 1.75rem; height: 1.75rem; display: block; }
/* The repository is the subject of every page under it, so it reads as
   the heading and "quarry" recedes to the crumb it is. Without this the
   two sit at equal weight and the forge name — identical on every page —
   competes with the one word that changes. */
.repobar .repo { font-weight: bold; font-size: var(--fs-lg); text-decoration: none;
  min-width: 3ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* The posture badge: a chip beside the name, GitHub's "forked from"
   shape. failing paints its border, nothing else — the notice below
   the bar carries the words. */
.repobar .posture { display: inline-flex; align-items: center; gap: var(--sp-1); flex: none;
  font-size: var(--fs-sm); color: var(--muted); text-decoration: none; border: 1px solid var(--rule);
  border-radius: var(--r-4); padding: var(--sp-0) var(--sp-2); white-space: nowrap; }
.repobar .posture:hover { color: var(--fg); border-color: var(--muted); }
.repobar .posture-failing { border-color: var(--accent); color: var(--fg); }
.repobar .posture-warn { font-size: var(--fs-md); color: var(--muted); padding-top: 0; padding-bottom: var(--sp-2); }
/* Narrow: the chip keeps its icon and drops its words — the bar's one
   line belongs to the nav, and the title attribute still says it all. */
@media (max-width: 48rem) { .repobar .posture .words { display: none; }
  .repobar .posture { padding: var(--sp-1) var(--sp-2); } }
/* Explore names the posture in a word, the same chip family as
   "private": a glyph alone read as decoration. */
.fleet .r-name .tl-badge.posture-badge { margin-left: var(--sp-1); color: var(--muted); }
/* Search sits with the chrome now rather than under it. It takes the
   leftover width so it is the element that shrinks when the header is
   tight, which is right: a repository's name must stay readable and a
   search box is usable at any width. */
.topbar .find { min-width: 0; margin: 0; }
.topbar .find input { width: 100%; }
/* The avatar is initials, not an image: quarry has no avatar store and
   inventing one to fetch a gravatar would send every reader's identity
   to a third party. */
/* A tooltip for controls that are genuinely icon-only, after Basecoat:
   a ::after carrying content: attr(data-tip), positioned against the
   trigger, no script at all.

   Two deliberate departures from Basecoat. It shows on :focus-visible
   as well as :hover — Basecoat explicitly hides its tooltip for
   keyboard users, which makes an icon-only control unnameable without a
   mouse. And it is anchored where the browser can do it, so an
   overflow:hidden ancestor cannot clip it; the absolute fallback is
   what Basecoat does everywhere.

   What this still cannot do is touch: there is no hover on a phone.
   popover=hint with interestfor handles hover, focus AND long-press
   declaratively, but Safari does not implement it, so it cannot be the
   mechanism. That is why visible labels remain the default everywhere
   there is room, and this is reserved for controls whose meaning is
   also carried by aria-label. */
[data-tip] { position: relative; }
[data-tip]::after {
  content: attr(data-tip);
  position: absolute; z-index: 60; bottom: calc(100% + 0.35rem); left: 50%;
  translate: -50% 0.25rem;
  padding: var(--sp-0) var(--sp-2); border: 1px solid var(--rule); border-radius: var(--r-1);
  background: var(--bg); color: var(--fg);
  font-size: var(--fs-xs); font-weight: normal; white-space: nowrap;
  opacity: 0; visibility: hidden; pointer-events: none;
  transition: opacity 100ms ease, translate 100ms ease, visibility 100ms;
}
[data-tip]:hover::after {
  opacity: 1; visibility: visible; translate: -50% 0;
}
@media (prefers-reduced-motion: reduce) { [data-tip]::after { transition: none; } }
/* No anchor positioning here, on evidence: the implicit-anchor form
   resolved a column off inside table layout (Chrome, fleet headers),
   and a NAMED anchor on a class of trigger resolves every tip to the
   last trigger on the page. Absolute against the trigger is the only
   arrangement that cannot point at the wrong thing. Its cost is
   clipping under overflow ancestors, which each placement below pays
   deliberately instead of accidentally. */

/* A column header names itself when the table is wide enough to say it,
   and falls back to the icon alone when it is not. The label is real
   text, so it is readable by touch, by a screen reader and by anyone
   who does not know the glyph. */
.colhead { display: inline-flex; align-items: center; gap: var(--sp-1); justify-content: flex-end; }
/* Narrow: visually hidden, NOT display:none — the label is the header's
   accessible name, and none would silence it for a screen reader. The
   tooltip carries it for sighted pointer users instead. Wide: the label
   is visible, so the tooltip would repeat it and stays off. */
.collabel { position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap;
  font-weight: normal; color: var(--muted); font-size: var(--fs-sm); }
.colhead[data-tip]::after { bottom: auto; top: calc(100% + 0.35rem);
  left: auto; right: 0; translate: 0 -0.25rem; }
.colhead[data-tip]:hover::after { translate: 0 0; }
@media (min-width: 56rem) {
  .collabel { position: static; width: auto; height: auto; overflow: visible;
    clip-path: none; }
  .colhead[data-tip]::after { display: none; }
}
.avatar { flex: none; width: 2.25rem; height: 2.25rem; padding: 0;
  border-radius: var(--r-4); background: var(--bg); border: 1px solid var(--rule);
  font-size: var(--fs-xs); font-weight: 700; cursor: pointer; display: grid; place-items: center; }
.avatar:hover { border-color: var(--fg); }
/* A BASE size for the mark. Every other .idn rule is scoped to a
   parent, so an identicon used anywhere new inherited no size at all
   and an SVG with no size fills its container — the wiki history page
   rendered one person's mark at the width of the screen. The helper is
   meant to be usable wherever a person is named; that is only true if
   it is safe by default. Scoped rules below are more specific and still
   win. */
.idn { width: 14px; height: 14px; vertical-align: -2px; flex: none; }
/* An uploaded picture fills the same square without being squashed. */
.idn-img { object-fit: cover; }
/* The mark FILLS its avatar. It sat at 78%, so the avatar's own
   background showed as a ring around a rounded square — two shapes and
   two colours where a reader expects one disc. Filling it also makes
   the mark's colour the avatar's colour, which is the point of an
   identicon: the hue is the recognisable part at 24px, long before any
   pattern is legible.

   overflow: hidden is what lets the square fill a circle — the
   avatar's radius does the shaping, so the mark needs no radius of its
   own and the SVG's rounded corners fall outside the clip. */
.avatar { overflow: hidden; }
.avatar .idn { width: 100%; height: 100%; border-radius: 0; display: block; }
button.avatar[popovertarget="acct"] { anchor-name: --avatar; justify-self: end; }
.menu {
  /* One inset declaration, not a shorthand plus overrides: the
     shorthand resets the sides the longhands set, which is how this
     first landed pinned to the top of the viewport. */
  position: fixed; inset: 3.25rem 1rem auto auto; margin: 0;
  min-width: 11rem; padding: var(--sp-2); border: 1px solid var(--rule); border-radius: var(--r-2);
  background: var(--bg); color: var(--fg);
  /* display is deliberately NOT set here. The UA stylesheet closes a
     popover with [popover]:not(:popover-open) { display: none }, and an
     author rule beats it — declaring display:flex on the base left the
     menu laid out and hit-testable at opacity 0, an invisible 185x111
     region swallowing clicks on every page whether or not it had ever
     been opened. It is set only in the open state below. */
  flex-direction: column; gap: var(--sp-0);
}
/* Anchored to the avatar where the browser can do it, so the menu keeps
   its relationship to the button at any viewport; the fixed corner above
   is the fallback and lands in the same place at these sizes. */
@supports (anchor-name: --a) {
  .menu { position-anchor: --avatar; inset: auto; top: anchor(bottom); right: anchor(right);
    margin-top: var(--sp-2); }
}
/* The menu grows out of the button rather than appearing. allow-discrete
   is what lets display participate, so the transition survives the
   popover being display:none between states. */
.menu {
  opacity: 0; transform: translateY(-0.25rem) scale(0.97); transform-origin: top right;
  transition: opacity 120ms ease, transform 120ms ease, overlay 120ms allow-discrete,
              display 120ms allow-discrete;
}
.menu:popover-open { display: flex; opacity: 1; transform: none; }
@starting-style {
  .menu:popover-open { opacity: 0; transform: translateY(-0.25rem) scale(0.97); }
}
@media (prefers-reduced-motion: reduce) { .menu { transition: none; } }
.menu-switch button .idn { width: 16px; height: 16px; vertical-align: -3px; margin-right: var(--sp-1); }
.menu a, .menu button {
  /* border-box, or "width: 100%" means 100% PLUS the padding and every
     row hangs over the menu's right edge — which reads as a clipped,
     scrolling box even when the list is one item long. */
  display: flex; align-items: center; gap: var(--sp-2); width: 100%; box-sizing: border-box;
  /* A button's UA font is smaller than the page's; in a menu every
     row must read the same, whatever element it is. */
  font: inherit;
  padding: var(--sp-1) var(--sp-2); border: 0; border-radius: var(--r-1); background: none;
  color: var(--fg); text-decoration: none; text-align: left; font-weight: normal;
}
.menu a:hover, .menu button:hover { background: var(--surface); }
/* The visitor's "what is this": prose in a menu's frame, so its links
   are links in a sentence, not rows. */
.menu.about-pop { box-sizing: border-box; width: min(24rem, calc(100vw - 2rem)); padding: var(--sp-3) var(--sp-4); gap: var(--sp-2); }
.menu.about-pop p { margin: 0; line-height: 1.5; }
.menu.about-pop a { display: inline; width: auto; padding: 0; border-radius: 0;
  color: var(--accent); text-decoration: underline; }
.menu.about-pop a:hover { background: none; }
.menu form { margin: 0; }
.menu-who { padding: var(--sp-1) var(--sp-2) var(--sp-2); color: var(--muted); font-size: var(--fs-md);
  border-bottom: 1px solid var(--rule); margin-bottom: var(--sp-1); }
/* Without the Popover API the menu renders as a plain inline row rather
   than vanishing: losing the popover is a cosmetic loss, losing the way
   to sign out is not. */
@supports not selector(:popover-open) {
  .avatar { display: none; }
  .menu { position: static; display: flex; flex-direction: row; align-items: center;
    margin-left: auto; border: 0; padding: 0; min-width: 0; gap: var(--sp-2); }
  .menu a, .menu button { width: auto; padding: var(--sp-0) var(--sp-2); }
  .menu-who { border: 0; margin: 0; padding: 0; }
}
form.theme button { padding: var(--sp-1) var(--sp-2); line-height: 0; background: none;
  border-color: transparent; color: var(--muted); }
form.theme button:hover { border-color: var(--rule); color: var(--fg); }
form.theme .ic { opacity: 1; width: 1.05em; height: 1.05em; }
/* Scope pills reuse the nav's shape: the reader is choosing among the
   same sections, so they should not look like a different control. */
nav.scopes { display: flex; flex-wrap: wrap; gap: var(--sp-1); margin: var(--sp-2) 0 var(--sp-4); }
nav.scopes a { display: inline-block; padding: var(--sp-0) var(--sp-2); border-radius: var(--r-4);
  border: 1px solid var(--rule); text-decoration: none; font-size: var(--fs-body); }
nav.scopes a.here { background: var(--code-bg); color: var(--fg); font-weight: 600; }
ul.hits { margin: 0; }
ul.hits li { padding: var(--sp-2) var(--sp-4); }
.snip { color: var(--muted); font-size: var(--fs-body); overflow-wrap: anywhere; }
/* Fleet-wide results lead with the repository: the same wiki title
   exists in several stores, so a hit without its repo is ambiguous. */
.hit-repo { color: var(--muted); font-size: var(--fs-md); }
.hit-repo::after { content: " /"; }
nav.scopes.facets a .n { color: var(--muted); font-size: var(--fs-sm); }
form.find.wide { max-width: none; display: flex; gap: var(--sp-2); margin-bottom: var(--sp-2); }
form.find.wide input { flex: 1 1 auto; min-width: 0; }
/* Chrome links are navigation, not references: colouring the whole nav
   with the accent would make the accent mean nothing. */
.topbar .site, .repobar .repo, nav a, .crumbs a, table.tree a { color: var(--fg); }
header form { margin-left: auto; }
.tw { overflow-x: auto; }
/* Timeline card/detail metrics adapted from fossil src/default.css
   (BSD-2-Clause; see third_party/PROVENANCE.md). The graph tiles
   seamlessly: a fixed 36px head band (node + curves) and a stretching
   tail fill each row edge-to-edge at identical pixel x positions, and
   date rows carry the active rails through. */
table.tl td { border: 0; padding: 0; vertical-align: top; }
table.tl td.tl-graph { position: relative; }
svg.tl-head { position: absolute; top: 0; left: 0; display: block; }
svg.tl-tail { position: absolute; top: 36px; left: 0; height: calc(100% - 36px); display: block; }
tr.tl-date svg.tl-tail, tr.tl-flat svg.tl-tail { top: 0; height: 100%; }
.tl-card-flat { padding: 0.45em 0.75em; font-size: 0.95em; }
/* A technote is the one row on this page somebody WROTE to be read
   here, and the one whose position in time is authored rather than
   observed — the E card is a moment its author chose. Every other flat
   row is a record that something happened. They were pixel-identical
   apart from one word of badge text, so the distinction the reader
   needs was carried by four characters.
   The rule and the warmer ground say "editorial"; the excerpt does the
   heavier lifting, because a difference in SHAPE reads faster than a
   difference in colour and survives greyscale and a phone. */
/* The forum's post edge is this colour at 30% saturation over 3px.
   One pixel of the same value is a grey hairline — the width that
   carried it is gone, so the saturation has to do the work instead.
   55% is the identicon's own range (its two steps are 62% and 60%),
   which is the point: the rule and the mark beside it are one person. */
.tl-card-note { border-left: 1px solid oklch(var(--idn-l) var(--uc, var(--idn-c0)) var(--uh, 0) / 0.9);
  border-top-left-radius: 0; border-bottom-left-radius: 0;
  padding: 0.55em 0.75em; }
.tl-card-note .tl-note-t { font-weight: 500; }
.tl-note-x { margin: 0.3em 0 0; color: color-mix(in srgb, var(--fg) 72%, var(--muted));
  font-size: 0.93em; max-width: 62ch; }

/* ---- notes: the feed, the note, its history ---- */
.tn-head { display: flex; align-items: baseline; justify-content: space-between;
  gap: var(--sp-4); max-width: var(--measure); }
.tn-head h1 { margin: 0; }
.tn-new { white-space: nowrap; font-size: var(--fs-body); }
.tn-lede { color: var(--muted); font-size: var(--fs-body); max-width: var(--measure);
  margin: var(--sp-2) 0 var(--sp-5); padding-bottom: var(--sp-4); border-bottom: 1px solid var(--rule); }
.tn-h { margin: 0 0 var(--sp-1); }
/* A numbered list without the numbers: ordered because a news feed IS
   a sequence, unnumbered because the dates already say where in it. */
.tn-feed { list-style: none; padding: 0; margin: 0;
  display: flex; flex-direction: column; gap: var(--sp-5); }
.tn-item { max-width: var(--measure); }
.tn-title { font-size: var(--fs-lg); font-weight: 500; text-decoration: none; }
.tn-title:hover { text-decoration: underline; }
.tn-x { margin: var(--sp-1) 0 0; color: color-mix(in srgb, var(--fg) 74%, var(--muted)); }
.tn-m { margin: var(--sp-1) 0 0; font-size: var(--fs-sm); color: var(--muted); }
.tn-m a { text-decoration: none; }
.tn-m a:hover { text-decoration: underline; }
.tn-empty { color: var(--muted); max-width: var(--measure); }
.about-note-x { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden; }
.tn-hist { list-style: none; padding: 0; margin: 0;
  display: flex; flex-direction: column; gap: var(--sp-2); }
.tn-hrow { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--sp-2);
  max-width: var(--measure); }
.tn-hrow a { text-decoration: none; }
.tn-hm { font-size: var(--fs-sm); color: var(--muted); }
/* The one history fact worth colour: an edit that changed the TITLE is
   why a superseded row and its correction can read as two notes. */
.tn-retitle { color: color-mix(in srgb, var(--accent) 70%, var(--muted)); }
.tn-form { max-width: var(--measure); }
.tn-form label { display: block; font-weight: 500; }
.tn-form input, .tn-form textarea { width: 100%; box-sizing: border-box; margin-top: var(--sp-1); }
.tn-form input[type="datetime-local"] { width: auto; min-width: 15rem; }
.tn-hint { display: block; margin-top: var(--sp-1); font-size: var(--fs-sm); color: var(--muted); }
.tn-acts { display: flex; align-items: center; gap: var(--sp-4); }
/* ---- operations ---- */
/* The phase banner. Its colour is the state, so an operator glancing at
   a wall display reads it without reading it: amber while stores are
   still coming up, accent while work moves, quiet at rest. */
.ops-phase { background: var(--surface); border-radius: var(--r-3);
  padding: var(--sp-4) var(--sp-4); margin: 0 0 var(--sp-4); border-left: 3px solid var(--rule); }
.ops-phase.ph-starting, .ops-phase.ph-indexing { border-left-color: var(--accent); }
.ops-phase.ph-syncing { border-left-color: var(--add); }
.ops-phase-h { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--sp-2); }
.ops-phase-n { font-size: var(--fs-lg); font-weight: 600; text-transform: uppercase;
  letter-spacing: 0.06em; }
.ops-phase-c { color: var(--muted); font-size: var(--fs-md); }
.ops-phase-l { margin: var(--sp-2) 0 0; font-size: var(--fs-sm); color: var(--muted); }
.ops-held { list-style: none; margin: var(--sp-1) 0 0; padding: 0; font-size: var(--fs-sm); }
.ops-held-i { color: var(--fg); padding: var(--sp-0) 0; }
.ops-held-q { color: var(--muted); }
.ops-held-v { font-weight: 600; }
.ops-held-a { color: var(--muted); }
/* One track, two segments: ready and stalled are both done waiting, and
   a bar that counted a stalled store as pending would never fill. */
.ops-bar { display: flex; height: 6px; border-radius: var(--r-1); overflow: hidden;
  background: color-mix(in srgb, var(--fg) 12%, transparent); margin-top: var(--sp-2); }
.ops-bar-ok { background: var(--accent); }
.ops-bar-bad { background: var(--del); }
@media (prefers-reduced-motion: no-preference) {
  .ops-bar-ok, .ops-bar-bad { transition: width 0.4s ease; }
}
.ops-cards { display: flex; flex-wrap: wrap; gap: var(--sp-4); margin: 0 0 var(--sp-5); }
/* The attribution column: two lines, repositories over causes, both
   small enough that the numbers beside them stay the row's subject. */
.ops-from { font-size: var(--fs-sm); color: var(--muted); line-height: 1.5; }
.ops-from b { color: var(--fg); font-weight: 600; }
.ops-cause { font-family: var(--font-mono); font-size: 0.95em; }
.ops-top { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--sp-2); }
/* Small on purpose: a glance, not a chart. It takes the card's spare
   width and a fixed height, so a long series and a short one draw the
   same size box and the cards stay on one grid. */
.ops-spark { width: 4.5rem; height: 1.4rem; flex: none; fill: none;
  stroke: var(--accent); stroke-width: 1.5; stroke-linejoin: round; stroke-linecap: round;
  vector-effect: non-scaling-stroke; opacity: 0.85; }
.ops-card { flex: 1 1 12rem; background: var(--surface); border-radius: var(--r-3);
  padding: var(--sp-4) var(--sp-4); display: flex; flex-direction: column; gap: var(--sp-0); }
.ops-n { font-size: var(--fs-xl); font-weight: 600; font-variant-numeric: tabular-nums; line-height: 1; }
.ops-l { font-size: var(--fs-md); }
.ops-sub { font-size: var(--fs-sm); color: var(--muted); }
.ops-sub a { text-decoration: none; }
table.ops-t { width: 100%; border-collapse: collapse; font-size: var(--fs-md); }
table.ops-t th { text-align: left; font-weight: 500; color: var(--muted);
  font-size: var(--fs-sm); text-transform: uppercase; letter-spacing: 0.05em;
  padding: 0 var(--sp-2) var(--sp-1) 0; border-bottom: 1px solid var(--rule); }
table.ops-t td { padding: var(--sp-1) var(--sp-2) var(--sp-1) 0; border-bottom: 1px solid var(--rule); }
/* Digits line up in columns, which is the whole reason these are tables. */
table.ops-t th.n, table.ops-t td.n { text-align: right; font-variant-numeric: tabular-nums;
  white-space: nowrap; }
table.ops-t code { font-size: 0.95em; }
table.ops-t a { text-decoration: none; }
table.ops-t a:hover { text-decoration: underline; }
.ops-route-p { margin: -0.6rem 0 var(--sp-5); color: var(--muted); }
/* One request, then what it asked the store for. Indented under it
   because the calls belong to the request, not beside it. */
.ops-reqs { list-style: none; padding: 0; margin: 0;
  display: flex; flex-direction: column; gap: var(--sp-4); }
.ops-req { background: var(--surface); border-radius: var(--r-2); padding: var(--sp-2) var(--sp-3); }
.ops-req-h { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--sp-2); }
.ops-req-t { font-weight: 600; font-variant-numeric: tabular-nums; }
.ops-req-r { text-decoration: none; font-weight: 500; }
.ops-req-m { color: var(--muted); font-size: var(--fs-sm); }
/* Pushed to the end of the row: when a request happened matters less
   than what it cost, and a column of times would draw the eye first. */
.ops-req-w { margin-left: auto; color: var(--muted); font-size: var(--fs-sm); }
.ops-calls { margin: var(--sp-2) 0 0; padding-left: var(--sp-5); color: var(--muted);
  font-size: var(--fs-md); display: flex; flex-direction: column; gap: var(--sp-0); }
.ops-calls .n { font-variant-numeric: tabular-nums; }
.hit-meta { font-size: var(--fs-sm); color: var(--muted); margin: var(--sp-0) 0 0; }
.hit-meta code { font-size: 0.95em; color: var(--accent); }
.hit-branch { color: color-mix(in srgb, var(--fg) 70%, var(--muted)); }
.role-badge { margin-left: var(--sp-2); color: var(--accent); border-color: var(--accent); }
.w-notes { display: flex; align-items: baseline; gap: var(--sp-2); margin: 0 0 var(--sp-4); }
.w-notes a { text-decoration: none; font-weight: 500; }
.w-notes .tn-hint { display: inline; margin: 0; }
.tl-meta-inline { color: var(--muted); font-size: 85%; }
table.tl td.tl-time { white-space: nowrap; width: 3.4rem; padding: var(--sp-3) var(--sp-2) 0 0; text-align: right; }
table.tl td.tl-time a { color: var(--muted); text-decoration: none; }
table.tl td.tl-time a:hover { text-decoration: underline; }
table.tl td.tl-slot { width: 100%; padding: 0 0 var(--sp-1) var(--sp-2); }
table.tl td.tl-datecell { padding: var(--sp-1) 0 var(--sp-1) var(--sp-2); }
.tl-card { background: var(--surface); border-radius: 1em; padding: 0.75em; }
.tl-title { font-weight: 500; }
/* The kind glyph is chrome: it says what a row IS, so it sits with the
   muted metadata rather than competing with the row's own words. */
.tl-kind { color: var(--muted); flex: none; }
.tl-badge .tl-kind { color: inherit; vertical-align: -0.1em; }
.tl-p { margin: 0.35em 0 0; color: color-mix(in srgb, var(--fg) 78%, var(--muted)); }
/* The landing line is provenance, not prose: quieter than the check-in
   comment it sits under, and the link carries the weight. */
.tl-landed { font-size: 0.92em; color: var(--muted); }
/* The route beside an actual path: present for the drilldown link, and
   quiet enough that the path is what a reader scans. */
.ops-route-l { display: block; font-size: 0.82em; color: var(--muted); }
/* A strata: the rungs under a base row, and the map on a record. */
/* A ticket's number (54c5175f): quieter than the title it names. */
.t-num { color: var(--muted); font-variant-numeric: tabular-nums; font-weight: inherit; }
.strata summary { cursor: pointer; color: var(--muted); font-size: 0.92em; margin-top: 0.3em; }
.strata-rungs { list-style: none; margin: 0.4em 0 0; padding: 0 0 0 0.2em; border-left: 2px solid var(--rule); }
/* One rung, one row: position, title, branches, verdict. The title is
   clipped to a line (its first sentence already, clipped again here to
   the row) — a landing's description is prose, and printing all of it
   as a link made the fold a wall of underlined text. */
.strata-rungs li { padding: 0.2em 0 0.2em 0.6em; display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.2em 0.5em; }
.strata-rungs .rung-t { flex: 1 1 16rem; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.strata-rungs a.rung-t { color: var(--fg); text-decoration: none; }
.strata-rungs a.rung-t:hover { color: var(--accent); text-decoration: underline; }
.strata-rungs .rung-br { flex: none; font-size: 0.9em; }
.strata-rungs li.here { background: var(--surface); border-radius: var(--r-1); }
.strata-map { margin: 0.6em 0 0.2em; }
.l-steps { font-size: 0.9em; margin: 0.4em 0; padding: 0.5em 0.7em; background: var(--code-bg); border-radius: var(--r-1); }
.l-steps em { color: var(--muted); font-style: normal; }
.tl-meta { color: var(--muted); font-size: 80%; text-align: right; opacity: 0.85; margin-top: 0.5em; }
.tl-meta a { text-decoration: none; }
.tl-meta a:hover { text-decoration: underline; }
@media (max-width: 640px) {
  /* main carries the page's horizontal padding now, not body. This was
     "body { padding: var(--sp-2) }" — a shorthand, which silently reset the
     bottom reserve the fixed footer depends on and let content run
     underneath it on exactly the screens where that hurts most. */
  body { --gutter: 0.5rem; }
  table.tl td.tl-time { width: 2.6rem; font-size: var(--fs-sm); padding-top: var(--sp-4); }
  .tl-card { padding: 0.75em 0.55em; border-radius: 0.6em; }
  .tl-meta { font-size: 72%; }
  /* Phone widths: the card's widest line is the metadata run, and its
     max-content is what pushed the table past the viewport (auto table
     layout prefers it, and .tw then scrolls sideways, hiding the
     right-aligned meta). Stacking the items drops that preference to
     the longest single item, so the table fits with the cards still
     wide. */
  .tl-metaitem { display: block; }
  .tl-card { overflow-wrap: anywhere; }
}
/* File listing. The old one spent ~52px of height and three columns on
   one short filename; fossil fits roughly 60 entries in the space that
   showed 25. Density here comes from tight rows and dropping the
   repeated per-row "history" link, not from shrinking the type. */
table.tree { table-layout: auto; }
/* GitHub's middle column: the last check-in to touch each entry, muted,
   one line, eating the slack between name and age. Its collapse is
   GitHub's too — the message yields on a phone, the age stays. The
   size column belongs to the no-data layout; GitHub's listing never
   had one, and the message is worth more. */
table.tree.hc { width: 100%; }
table.tree.hc td.t-name { white-space: nowrap; max-width: 22rem;
  overflow: hidden; text-overflow: ellipsis; padding-right: var(--sp-4); }
td.t-change { color: var(--muted); max-width: 0; width: 100%;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  padding-right: var(--sp-4); }
td.t-change a { color: var(--muted); }
td.t-change a:hover { color: var(--accent); }
td.t-age { color: var(--muted); text-align: right; white-space: nowrap; }
@media (max-width: 44rem) {
  td.t-change { display: none; }
}
table.tree td { padding: var(--sp-0) 0; border: 0; line-height: 1.45; }
table.tree tr:hover { background: var(--surface); }
/* Thirty underlined names in a column is a wall of rules, and the
   underline carries no information here — every name in the listing is
   a link, so marking them all marks nothing. Hover restores it. */
table.tree a, .tree-bar .crumbs a { text-decoration: none; }
table.tree a:hover, .tree-bar .crumbs a:hover { text-decoration: underline; }
/* The icon and the name are one label, so they get a gap rather than
   sitting flush: without it the glyph reads as the first character of
   the filename. */
td.t-name { white-space: nowrap; }
td.t-name .ic { margin-right: var(--sp-2); }
/* Sizes are only comparable when their digits line up, which means
   right-aligned and tabular figures. The column takes only what it
   needs so the name column is not pushed across the viewport. */
td.t-size { text-align: right; white-space: nowrap; width: 1%;
  padding-left: var(--sp-5); color: var(--muted); font-variant-numeric: tabular-nums; }
/* Icons are an SVG sprite (see iconSprite), referenced by <use>. They
   were CSS masks with inline data: URIs, which Chrome accepts and iOS
   Safari does not: the raw <, > and quotes make the URI invalid, the
   mask silently fails, and every icon renders as a solid block. A
   sprite has no encoding to get wrong. */
.sprite { display: none; }
.ic { width: 1em; height: 1em; vertical-align: -0.125em; fill: currentColor;
  opacity: 0.55; flex: none; }
/* Flags replace ls(1)'s trailing punctuation. "compile*" reads as part
   of the filename; a labelled chip does not. */
.flag { font-size: var(--fs-xs); color: var(--muted); border: 1px solid var(--rule);
  border-radius: var(--r-1); padding: 0 var(--sp-1); vertical-align: 0.05em; }
/* A strip of repository facts, the way forgejo leads a repo page. Each
   is a link to the surface that explains it. */
/* The activity profile. Dots take their colour from --c on the
   author group; the stylesheet only knows shapes. */
.act, .slice { display: block; width: 100%; height: auto; font-size: var(--fs-xs); }
.act text, .slice text { fill: var(--muted); }
.act .ax text { text-anchor: end; }
.act .ax-x text { text-anchor: middle; }
.act .band-l { text-anchor: end; font-size: var(--fs-xs); }
.act .band { fill: var(--surface); }
/* Nothing is written on the plot (§3): the tree axis's directory bands
   and its date rail are shapes, and the hover carries their names. */
.act .ax line, .act .rail line { stroke: var(--rule); }
.act .mx rect, .act .my rect { fill: var(--muted); opacity: 0.45; }
/* Under a person filter everyone stays drawn and everyone but them goes
   grey. A per-index rule cannot be written in a static sheet, so the
   chosen group carries .mine and this greys its siblings. */
.act[data-user] .au:not(.mine) { fill: var(--muted); opacity: 0.15; }
.act[data-user] .au:not(.mine) .dot.merge { stroke: var(--muted); }
/* The marginals keep the project's grey bars; the person's own are
   drawn over them, on the project's scale, in their colour. */
.act .mx.mine rect, .act .my.mine rect { fill: var(--c); opacity: 0.9; }
.act .au { fill: var(--c); }
/* The radius is on the mark, not here: on the tree axis it follows the
   column width, and a dot too small to be a circle is drawn square. */
.act .dot { fill-opacity: 0.85; }
.act .dot.merge { fill: none; stroke: var(--c); stroke-width: 1.2; }
.act a:hover .dot { fill-opacity: 1; r: 3.4; }
.act-sample { overflow-x: auto; position: relative; }
/* activity.js draws the same picture on a canvas and hides the SVG; the
   tip is its hover readout, styled like the reference card. */
.act-canvas { display: block; width: 100%; height: auto; cursor: crosshair; touch-action: none; }
.act-sample > .act[hidden] { display: none; }
.act-tip { position: absolute; pointer-events: none; z-index: 2; font-size: var(--fs-sm); line-height: 1.3;
  padding: var(--sp-1) var(--sp-2); border: 1px solid var(--rule); border-radius: var(--r-1); background: var(--bg);
  color: var(--fg); box-shadow: 0 2px 8px rgba(0,0,0,0.15); max-width: 24rem; white-space: nowrap;
  overflow: hidden; text-overflow: ellipsis; }
.act-tip .m { color: var(--muted); }
.act-reset { position: absolute; top: 0.4rem; right: 0.6rem; z-index: 2; font: inherit; font-size: var(--fs-sm);
  padding: var(--sp-0) var(--sp-2); border: 1px solid var(--rule); border-radius: var(--r-1); background: var(--bg);
  color: var(--fg); cursor: pointer; }
/* The slice's hover readout is Datastar-driven (sliceHoverAttr); it
   wraps rather than clips because a busy day names several people. */
.slice-tip { white-space: normal; }
.slice-tip:empty { display: none; }
.act-controls { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2) var(--sp-4); margin: 0 0 var(--sp-2); }
.act-win { flex: none; }
.act-window { color: var(--muted); margin: 0 0 var(--sp-2); }
/* The legend under the picture is also the chooser (§7): a wrapping
   row of people, each a link. Under a filter the dots grey with the
   plot and only the chosen one keeps its colour — the hook is on the
   list, so no entry needs an inline decision. */
.act-legend { list-style: none; margin: var(--sp-2) 0 var(--sp-4); padding: 0; display: flex; flex-wrap: wrap; gap: var(--sp-1) var(--sp-4); }
.act-legend a { display: inline-flex; align-items: center; gap: var(--sp-1); text-decoration: none; }
.act-legend a[aria-current] { font-weight: 600; }
.act-legend .idn { width: 0.9rem; height: 0.9rem; border-radius: var(--r-1); flex: none; }
.act-legend .act-n { color: var(--muted); font-variant-numeric: tabular-nums; }
.act-legend[data-user] a:not([aria-current]) .idn { filter: grayscale(1); opacity: 0.55; }
.act-clear { color: var(--muted); }
.act-h { margin: var(--sp-5) 0 var(--sp-2); }
.act-key { color: var(--muted); font-size: var(--fs-md); }
.act-strata { position: relative; }
.act-slice .slice { max-width: 980px; }
.slice .au { fill: var(--c); }
.slice .c { fill: var(--c); }
.slice .rel { fill: var(--fg); }
/* Every day of the span is a cell; an empty one is drawn so the year
   keeps its shape. Nothing is drawn INSIDE a cell — the whole cell is
   the day's person and its opacity is the day's weight. */
.slice .c.empty { fill: var(--slice-empty); }
.slice .c.other { fill: var(--muted); }
.slice .n, .slice .m { font-size: var(--fs-xs); font-variant-numeric: tabular-nums; }
.slice .n { fill: var(--fg); }
.slice .yl { fill: var(--fg); font-size: var(--fs-xs); font-weight: 600; font-variant-numeric: tabular-nums; }
.act-years { white-space: nowrap; }
.home-slice { display: block; margin: 0 0 var(--sp-3); position: relative; }
.home-slice .slice { height: 1.4rem; width: 100%; }
.repo-stats { display: flex; flex-wrap: wrap; gap: var(--sp-1) var(--sp-4); margin: 0 0 var(--sp-3);
  padding: var(--sp-2) var(--sp-3); background: var(--surface); border: 1px solid var(--rule);
  border-radius: var(--r-2); font-size: var(--fs-body); }
.repo-stats a { display: inline-flex; align-items: center; gap: var(--sp-2);
  color: var(--fg); text-decoration: none; }
.repo-stats a:hover { text-decoration: underline; }
/* Reader traffic. The bars are relative to the biggest row in their
   group, never to the total: on a public repository the unidentified
   row is normally an order of magnitude larger than every named one,
   and shares would draw every real reader as nothing at all. */
.tf-head { display: flex; flex-wrap: wrap; align-items: baseline;
  justify-content: space-between; gap: var(--sp-3) var(--sp-5); margin: 0 0 var(--sp-4); }
.tf-totals { display: flex; flex-wrap: wrap; gap: var(--sp-1) var(--sp-5); font-size: var(--fs-body);
  color: var(--muted); }
.tf-total b { font-size: var(--fs-lg); font-weight: 600; color: var(--fg);
  font-variant-numeric: tabular-nums; margin-right: var(--sp-1); }
.tf-verbs { display: flex; gap: var(--sp-3); font-size: var(--fs-body); }
.tf-verbs a { color: var(--muted); text-decoration: none; padding-bottom: var(--sp-0);
  border-bottom: 2px solid transparent; }
.tf-verbs a:hover { color: var(--fg); }
.tf-verbs a.here { color: var(--fg); border-bottom-color: var(--accent); }
.tf-split { display: grid; grid-template-columns: 1fr; gap: var(--sp-5); }
@media (min-width: 48rem) { .tf-split { grid-template-columns: 1fr 1fr; gap: var(--sp-6); } }
.tf-group h2 { font-size: var(--fs-body); font-weight: 600; margin: 0 0 var(--sp-2); }
.tf-bars { list-style: none; margin: 0; padding: 0;
  display: flex; flex-direction: column; gap: var(--sp-2); }
.tf-row { display: grid; grid-template-columns: minmax(0, 9rem) 1fr 3.5rem;
  align-items: center; gap: var(--sp-2); font-size: var(--fs-md); }
.tf-lab { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tf-track { height: 0.6rem; background: var(--surface); border-radius: var(--r-1);
  overflow: hidden; }
.tf-fill { display: block; height: 100%; background: var(--accent); opacity: 0.35; }
.tf-val { text-align: right; font-variant-numeric: tabular-nums; color: var(--muted); }
/* A claimed name and an unidentified reader are both drawn APART from
   verified accounts, because neither is one. */
.tf-anon .tf-lab, .tf-anon .tf-val { color: var(--muted); font-style: italic; }
.tf-anon .tf-fill { opacity: 0.18; }
.tf-claim .tf-lab { color: var(--muted); }
/* A glyph, never a tooltip: the legend below the panel carries the
   meaning in words, and a screen reader gets it inline. */
.tf-tilde { color: var(--accent); font-weight: 600; }
.tf-why, .tf-none { color: var(--muted); font-size: var(--fs-md); max-width: 34rem;
  margin: var(--sp-3) 0 0; }
.tf-none { max-width: 40rem; }
/* The diff has not arrived yet. Deliberately quiet and deliberately
   not a spinner: the page is complete except for one region, and an
   animation would claim more is happening than is. */
.diff-waiting { color: var(--muted); }
/* What a landing settles. Quiet on the row, plainer on the record —
   it is a fact about the proposal, not a control. */
/* A record's facts: what it closes, what it addresses, what it landed
   as. One row each, one glyph each, aligned on the glyph — they were
   three paragraph shapes and read as three unrelated remarks. */
.l-fact { margin: var(--sp-1) 0 0; font-size: var(--fs-body); color: var(--muted);
  display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; }
.l-fact .ic { flex: none; opacity: 0.75; }
.l-fact a { color: var(--fg); }
.l-fact-last { margin-bottom: var(--sp-4); }
/* Each fragment wraps as a unit, so a separator never starts a line
   and "by" never leaves the name behind on the next one. */
.l-bit { display: inline-flex; align-items: center; gap: var(--sp-1);
  white-space: nowrap; }
/* t-sub is ordinary inline flow, so the groups need their own air —
   a text-node space between them would be a wrap opportunity, which
   is the thing these groups exist to remove. */
.t-sub .l-bit { margin-right: var(--sp-2); }
/* Teams: the public directory and one team's page (RFC 0018). The rows
   borrow the people list's shape on purpose — both answer "who is on
   this forge", one of them in groups. */
.teams { list-style: none; margin: 0; padding: 0; }
.teams li { display: flex; align-items: flex-start; gap: var(--sp-3);
  padding: var(--sp-2) var(--sp-3); border-bottom: 1px solid var(--rule); }
.teams li:last-child { border-bottom: 0; }
.teams .t-glyph { flex: none; margin-top: var(--sp-1); color: var(--muted); }
code.t-handle { background: var(--surface); border: 1px solid var(--rule);
  border-radius: var(--r-1); padding: 0 var(--sp-1); font-size: var(--fs-sm); color: var(--muted); }
/* The row's own control sits at the far end of the row, so a list of
   repositories reads as names with an action rather than as a column of
   buttons. .teams li is already a flex row; this just claims the gap. */
.teams li .row-act { margin-left: auto; flex: none; }
/* Add and rename are the same shape: a stack that becomes a row when
   there is width for one. Wrapping rather than a grid, because the
   select is as wide as the longest repository name and nothing useful
   can be said about that in advance. */
.t-add, .t-text { display: flex; flex-wrap: wrap; align-items: center;
  gap: var(--sp-2) var(--sp-3); margin: var(--sp-3) 0 var(--sp-5); }
.t-add label, .t-text label { color: var(--muted); font-size: var(--fs-md); }
.t-add select { max-width: 100%; }
.t-text { flex-direction: column; align-items: stretch; max-width: 34rem; }
.t-text input { width: 100%; }
.t-text label { margin-top: var(--sp-1); }
.t-text .sub { margin: var(--sp-0) 0 var(--sp-1); }
.t-text button { align-self: start; }
/* The room. A line said TO THE TEAM is marked rather than coloured
   differently: the merged view's whole job is to keep the origin of
   every line visible, and a badge says it in words. */
/* The team board. Chips rather than side-by-side lanes: a team's
   assignees are people and there is no upper bound on how many, so the
   per-repository board's fixed columns are the wrong shape here. The
   CARDS are that board's cards, from its own rules, which is what keeps
   the two recognisably one idea. */
/* The heading and its summary are one block, with room under the nav:
   .p-h carries no top margin, so a heading directly after the team bar
   sat against it. */
.t-board-head { margin: var(--sp-5) 0 0; }
.t-board-head .p-h { margin-bottom: var(--sp-1); }
.t-board-head .sub { margin: 0; }
.board-strip { display: flex; flex-wrap: wrap; gap: var(--sp-2); margin: var(--sp-4) 0 var(--sp-4); }
.board-strip a { display: inline-flex; align-items: baseline; gap: var(--sp-1);
  border: 1px solid var(--rule); border-radius: var(--r-4);
  padding: var(--sp-0) var(--sp-3); color: var(--fg); }
.board-strip a:hover { border-color: var(--accent); }
.board-strip a.here { border-color: var(--accent); background: var(--surface); font-weight: 600; }
/* Nobody is a column like any other and is marked as one: dashed says
   "an empty seat", not "an error". */
.board-strip a.nobody { border-style: dashed; }
/* Cards at a readable width rather than the page's. A ticket title is
   forty characters; a card eleven hundred pixels wide for it reads as
   an empty table, and the eye has to travel the whole width to reach
   the badges. auto-fill lets the count follow the window instead of
   being decided here — three across on a desktop, one on a phone, with
   no breakpoint to keep in sync. */
.board-rows { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--sp-2);
  grid-template-columns: repeat(auto-fill, minmax(21rem, 1fr)); }
/* The grid spaces them, so the card's own bottom margin would double
   the gap on every row but the last. A column layout with the act row
   pushed to the bottom keeps the cards in a row the same height, which
   is what makes a grid read as a grid. */
.board-rows > li.card { margin-bottom: 0; display: flex; flex-direction: column;
  gap: var(--sp-0); padding: var(--sp-3) var(--sp-3); }
.board-rows .t-badges { display: flex; flex-wrap: wrap; align-items: baseline;
  gap: var(--sp-1); margin-top: var(--sp-1); }
.board-rows .row-act { margin-top: auto; padding-top: var(--sp-2); }
/* The repository is the fact this board has and the per-repository one
   cannot, so it is the one badge that is not grey. */
.board-rows .chip.repo { border-color: color-mix(in srgb, var(--accent) 45%, var(--rule));
  color: var(--accent); }
/* Why a card has no reassign control. Per card, because "one of these
   trackers" is not an answer when a board shows several. */
.board-rows .no-assign { color: var(--muted); font-size: var(--fs-sm); margin-top: auto;
  padding-top: var(--sp-2); }
.chip.nobody { border-style: dashed; color: var(--muted); }
/* A label that only a screen reader needs. The select beside it says
   what it does to somebody who can see the card it sits on. */
.vh { position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap; }
.room { list-style: none; margin: var(--sp-2) 0 0; padding: 0; }
.room > li { display: flex; gap: var(--sp-3); padding: var(--sp-3) var(--sp-3); }
/* The body claims the row rather than shrinking to its text, so every
   reply box is the same width. Without this each row's form was as wide
   as that row's message, which reads as a rendering accident. min-width
   is the usual flex escape: a long unbroken word would otherwise set a
   floor wider than the row. */
.room > li > .p-main { flex: 1 1 auto; min-width: 0; }
.room-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--sp-2); }
.room-head a { font-weight: 600; }
.room-text { margin: var(--sp-1) 0 0; overflow-wrap: anywhere; }
.room li.to-team { border-left: 3px solid var(--accent); }
.room-say, .room-reply { display: flex; flex-wrap: wrap; align-items: center;
  gap: var(--sp-2); margin: var(--sp-3) 0 var(--sp-4); }
.room-say label { color: var(--muted); font-size: var(--fs-md); }
.room-say input[name=text] { flex: 1 1 18rem; }
.room-reply { margin: var(--sp-2) 0 0; }
.room-reply input[name=text] { flex: 1 1 14rem; font-size: var(--fs-body); }
.room-more { margin: var(--sp-3) 0 0; }
.t-head { display: flex; align-items: flex-start; gap: var(--sp-4); margin: 0 0 var(--sp-5); }
.t-head h1.t-name { margin: 0; }
.t-handle-l { margin: var(--sp-1) 0 0; }
/* The team's sentence, now the first thing under the bar rather than a
   line inside a heading block — so it needs the room the heading used
   to give it, above AND below. */
.t-descr { margin: var(--sp-4) 0 var(--sp-5); color: var(--muted); max-width: 60ch; }
.avatar-lg { width: 3rem; height: 3rem; flex: none; }
.avatar-lg svg { width: 100%; height: 100%; }
.t-owner { border-color: var(--accent); color: var(--accent); }
/* A route that is not this repository's own string. Linked, because
   the answer to "change this" is on the team, not here. */
.chip.via-team { border-color: var(--accent); color: var(--accent);
  text-decoration: none; }
.chip.via-team:hover { text-decoration: underline; }
.l-closes-chip { color: var(--muted); }
.ops-req-u { color: var(--fg); text-decoration: none; overflow: hidden;
  text-overflow: ellipsis; white-space: nowrap; max-width: 34rem; }
.ops-req-u:hover { text-decoration: underline; }
.exc-acts { display: flex; align-items: center; gap: var(--sp-4); flex-wrap: wrap; }
.exc-clear { margin: 0; }
.exc-clear button { font: inherit; font-size: var(--fs-md); padding: var(--sp-0) var(--sp-2);
  color: var(--muted); background: none; border: 1px solid var(--rule);
  border-radius: var(--r-1); cursor: pointer; }
.exc-clear button:hover { color: var(--fg); border-color: var(--muted); }
.tf-fleet { width: 100%; border-collapse: collapse; font-size: var(--fs-body); }
.tf-fleet th, .tf-fleet td { text-align: left; padding: var(--sp-2) var(--sp-3);
  border-bottom: 1px solid var(--rule); vertical-align: middle; }
.tf-fleet th { font-size: var(--fs-xs); text-transform: uppercase; letter-spacing: 0.06em;
  color: var(--muted); font-weight: 600; white-space: nowrap; }
.tf-fleet td.tf-n, .tf-fleet th.tf-n { text-align: right;
  font-variant-numeric: tabular-nums; white-space: nowrap; }
.tf-fleet tr:last-child td { border-bottom: 0; }
.tf-fleet-bar { display: block; margin-top: var(--sp-1); max-width: 18rem; }
.tf-zero { color: var(--muted); }
.tf-fine { color: var(--muted); font-size: var(--fs-sm); max-width: 46rem;
  margin: var(--sp-6) 0 0; padding-top: var(--sp-4); border-top: 1px solid var(--rule); }
.refbtn, .dlbtn { display: inline-flex; align-items: center; gap: var(--sp-2); cursor: pointer; }
.refname { max-width: 12rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.caret { color: var(--muted); font-size: 0.7em; }
/* Hug the content; only long ref lists scroll, and their scrolling does
   not chain to the page behind them. */
.menu.refs, .menu.dlmenu { position: fixed; inset: auto; max-height: 60vh; overflow-y: auto;
  overscroll-behavior: contain; scrollbar-width: thin; min-width: 13rem; }
.menu.refs a.here, .menu.dlmenu a.here { background: var(--surface); font-weight: 600; }
@supports (anchor-name: --a) {
  .refbtn { anchor-name: --refbtn; }
  .menu.refs { position-anchor: --refbtn; top: anchor(bottom); left: anchor(left); margin-top: var(--sp-1); }
  .dlbtn { anchor-name: --dlbtn; }
  .menu.dlmenu { position-anchor: --dlbtn; top: anchor(bottom); right: anchor(right); margin-top: var(--sp-1); }
}
/* basecoat's input-group, same as the search field: ONE bordered shell
   owns the border and the radius; the segments inside are borderless
   with hairline separators. Per-segment borders put a radius at every
   seam, and the corners never quite meet. The tool choice is still a
   URL (?clone=), so it survives a reload and can be linked to. */
.clonebox { display: inline-flex; align-items: stretch; min-width: 0;
  border: 1px solid var(--rule); border-radius: var(--r-row); background: var(--bg);
  overflow: hidden; }
.clonebox:focus-within { outline: 2px solid var(--muted); outline-offset: 1px; }
.clonepick { display: inline-flex; align-items: center; font-size: var(--fs-sm);
  border-right: 1px solid var(--rule); background: var(--surface); }
.clonepick a { padding: var(--sp-1) var(--sp-2); text-decoration: none; color: var(--muted);
  align-self: stretch; display: inline-flex; align-items: center; }
.clonepick a.here { background: var(--bg); color: var(--fg); font-weight: 600; }
.cloneurl { font-family: var(--font-mono); font-size: var(--fs-sm); min-width: 0; width: 22rem;
  border: 0; border-radius: 0; background: none; }
.cloneurl:focus-visible { outline: none; }
.copybtn { border: 0; border-left: 1px solid var(--rule); border-radius: 0;
  background: none; padding: 0 var(--sp-2); cursor: pointer; }
/* Tree chrome: one row carrying branch, crumbs and clone. */
.tree-bar { display: flex; gap: var(--sp-3); align-items: center; flex-wrap: nowrap;
  margin-bottom: var(--sp-2); min-width: 0; }
.tree-bar .crumbs { font-weight: 600; min-width: 3ch; overflow: hidden;
  text-overflow: ellipsis; white-space: nowrap; }
.tree-bar .tree-acts { margin-left: auto; display: flex; gap: var(--sp-3);
  align-items: center; font-size: var(--fs-md);
  /* The clone input is the compressible member of this row: without
     min-width the actions block refuses to shrink and spills into the
     About column. */
  min-width: 0; flex: 0 1 auto; }
.tree-bar .tree-acts .clonebox { min-width: 0; flex: 0 1 auto; }
.tree-bar .tree-acts .clonepick, .tree-bar .tree-acts .dlbtn { flex: none; }

.tree-bar .dl { white-space: nowrap; }
/* A select is as wide as its widest option, so one long branch name set
   the control's width for every repository — 288px, most of a phone
   row, to show "trunk". The cap truncates the outlier instead; the full
   name is still in the open list, which is where it is read. */
.tree-bar select { max-width: 11rem; }
@media (max-width: 40rem) {
  /* The clone command is the first thing worth losing on a phone — it
     is long and not what a reader came for — and the zit/fossil picker
     goes with it: a tool choice for a command that is not on screen.
     What remains (ref, path, download) holds ONE line. */
  .tree-bar .clonebox, .tree-bar .clonepick { display: none; }
  .tree-bar { gap: var(--sp-2); }
}
/* Form controls inherit the browser's palette, not the page's: a white
   textarea on a dark page, and iOS Safari's blue pill for every button.
   They also default to content-box, so the width:100% on our textareas
   overflowed by exactly the padding and border. Both are the browser
   being reasonable in isolation and wrong here, so they are set once for
   every form rather than per-page. */
input, textarea, select, button {
  font: inherit; color: var(--fg); background: var(--bg);
  border: 1px solid var(--rule); border-radius: var(--r-row);
  padding: var(--sp-1) var(--sp-2); box-sizing: border-box; max-width: 100%;
}
textarea { display: block; width: 100%; resize: vertical; line-height: 1.5; }
/* The UA draws its select caret hard against the border and ignores
   padding. Owning the indicator (a currentColor-matched chevron) is
   how every design system solves it: consistent shape, real breathing
   room, both themes. */
select { appearance: none; -webkit-appearance: none; padding-right: var(--sp-5);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' fill='none' stroke='%23888' stroke-width='1.6' stroke-linecap='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat; background-position: right 0.6rem center; }
button, input[type=submit] {
  -webkit-appearance: none; appearance: none;
  background: var(--code-bg); cursor: pointer; font-weight: 600;
}
button:hover, input[type=submit]:hover { border-color: var(--fg); }
::placeholder { color: var(--muted); opacity: 1; }
/* WebKit draws its own clear button inside a search field, in the OS
   accent colour, and nothing but its size is styleable — a blue X in a
   gruvbox page. Basecoat leaves it alone and inherits the same problem.
   It is removed here and replaced, where there is something to clear,
   by a link that resets the filter: the same colour as everything else
   and, being a link, it works with scripting off. */
input[type=search]::-webkit-search-cancel-button,
input[type=search]::-webkit-search-decoration { -webkit-appearance: none; appearance: none; }
/* basecoat's input-group: one bordered shell, borderless controls
   inside it. An add-on then sits WITHIN the field instead of floating
   beside it, which is what the loose clear link looked like. */
/* Forum: GitHub/Codeberg's comment-thread shape. Each post is a card —
   a muted header strip (author, time, hash, controls) over a padded
   body — so authorship and actions live in chrome, not in the prose. */
.f-actions { display: flex; align-items: center; gap: var(--sp-2); margin: var(--sp-3) 0 var(--sp-4); flex-wrap: wrap; }
.f-actions form { margin: 0; }
.f-post { border: 1px solid var(--rule); border-radius: var(--r-2); margin: var(--sp-4) 0;
  /* The author's hue as a WHISPER on the left edge — same 1px weight
     as every other border, only the color applied. */
  border-left-color: oklch(var(--idn-l) var(--uc, var(--idn-c0)) var(--uh, 0) / 0.55); }
.f-post-h .idn { width: 16px; height: 16px; }
.f-spectrum { display: block; width: 100%; height: 6px; margin: var(--sp-0) 0 var(--sp-4); border-radius: var(--r-1); }
.f-spectrum a rect:hover { fill-opacity: 1; }
/* A mention reads like a BYLINE, not a highlighted link: the mark, then
   the name in the page's own colour. The accent is reserved for hover,
   because a name that is orange wherever it appears turns every
   conversation into a field of links and stops meaning "this is
   clickable" anywhere. */
a.uref { font-weight: 600; color: var(--fg); text-decoration: none; }

/* References and their preview (RFC 0011 §14). The link is styled as a
   link because it IS one — the popover is an accelerator, not the way
   the reference is read. */
a.ref { text-decoration: underline dotted; text-underline-offset: 0.15em; }
#refpop { border: 1px solid var(--rule); border-radius: var(--r-row); padding: var(--sp-2) var(--sp-3);
  max-width: 32rem; background: var(--bg); color: var(--fg); box-shadow: 0 2px 8px rgba(0,0,0,0.15); }
#refpop:not(:popover-open) { display: none; }
/* The card belongs AT the reference that opened it, not centred in the
   viewport where the UA would put it. Anchor positioning is the real
   answer — it survives scroll and reflow — and the opener's measured
   left/top is the inline fallback where it is unknown. inset: auto
   undoes the UA's inset: 0, which would otherwise stretch the card
   across the screen the moment left is set. */
#refpop { position: fixed; inset: auto; margin: 0; }
@supports (anchor-name: --a) {
  #refpop { position-anchor: --qrefpop; top: anchor(bottom); left: anchor(left);
    margin-top: var(--sp-1); position-try-fallbacks: flip-block, flip-inline; }
}
.refcard-sum { color: var(--muted); }
.refcard[data-expired] .refcard-sum { font-style: italic; }
a.chatquote { display: block; border-left: 3px solid var(--rule); padding-left: var(--sp-2);
  color: var(--muted); text-decoration: none; font-size: 0.9em; }
a.chatreplies { color: var(--muted); font-size: 0.85em; }
a.uref:hover { color: var(--accent); text-decoration: underline; }
.f-parent, .f-replyto { color: inherit; }
.f-replyto { font-weight: 600; }
/* Stamp sits in the stats strip because that is where a reader already
   looks for what a repository amounts to. The pressed state rides on
   aria-pressed as well as colour — "is this stamped" must not be a claim
   made only in orange, which a colour-blind reader cannot hear. */
.stamp-form { display: inline-flex; margin: 0; }
.stamp-btn { display: inline-flex; align-items: center; gap: var(--sp-1);
  padding: var(--sp-0) var(--sp-2); font-size: var(--fs-md); font-weight: 600; }
.stamp-btn .ic { width: 0.9em; height: 0.9em; }
/* Stamping COMPLETES the mark: the empty die is the node and its
   rail, and the check-in dot at the centre appears when you stamp.
   A custom property rather than a selector, because <use> puts the
   symbol in a shadow tree that .stamp-btn.on .node cannot reach. */
.stamp-btn.on { border-color: var(--accent); color: var(--accent); --stamp-node: currentColor; }
.stamp-n { font-weight: 400; opacity: 0.85; }
/* The listing. Rows stack on a phone for the same reason the webhook
   rows do: a fixed-width control here would drag the page wide again. */
.ev-list { margin: var(--sp-2) 0; }
.ev-list li { display: flex; flex-wrap: wrap; align-items: baseline;
  gap: var(--sp-1) var(--sp-3); padding: var(--sp-2) var(--sp-4); }
.ev-kind { font-size: 0.85em; background: var(--code-bg); border-radius: var(--r-1);
  padding: var(--sp-0) var(--sp-2); flex: 0 0 auto; }
.ev-sum { flex: 1 1 16rem; min-width: 0; overflow-wrap: anywhere; }
.ev-meta { color: var(--muted); font-size: 0.85em; }
.p-h { font-size: var(--fs-lg); margin: 0 0 var(--sp-2); }
/* .p-h carries no top margin — every use of it so far followed an
   element whose own bottom margin made the gap. A list card has none,
   so a heading after one sat flush against its border. Fixing the
   ADJACENCY rather than either element: a card keeps its spacing
   wherever else it is used, and the next heading placed after one is
   right without anybody noticing this rule exists. */
.list-card + .p-h { margin-top: var(--sp-5); }
.stamped-list { margin: var(--sp-2) 0; }
.stamped-list li { display: flex; flex-wrap: wrap; align-items: baseline;
  gap: var(--sp-2) var(--sp-4); padding: var(--sp-2) var(--sp-4); }
.stamped-list .p-name { font-weight: 600; }
.stamped-list .p-desc { color: var(--muted); font-size: 0.9em;
  flex: 1 1 14rem; min-width: 0; }
.stamped-empty { color: var(--muted); }
/* Webhooks. A table here forced the page wider than a phone: three
   columns, one of them an input beside three buttons, and a table's
   intrinsic width wins over max-width on anything inside it — so every
   field on the page, including the ones above this section, ran off the
   right edge. Rows that stack are the fix, not a narrower table. */
.hook-list { margin: var(--sp-2) 0 var(--sp-4); }
.hook-list li { padding: var(--sp-3) var(--sp-4); }
.hook-head { display: flex; flex-wrap: wrap; align-items: baseline;
  gap: var(--sp-2) var(--sp-2); margin: 0; }
/* A URL has no spaces, so it needs explicit permission to break or it
   is itself a minimum width the page must grow to. */
.hook-url { overflow-wrap: anywhere; min-width: 0; }
.hook-state { font-size: 0.85em; color: var(--muted); }
.hook-state.off { font-style: italic; }
.hook-sub { font-size: 0.85em; color: var(--muted); margin: var(--sp-0) 0 var(--sp-2); }
.hook-form { display: flex; flex-wrap: wrap; align-items: center;
  gap: var(--sp-2); margin: 0; }
/* Grow to fill, shrink to nothing: min-width 0 is what lets a flex item
   actually get smaller than its content, which is the other half of why
   the row used to overflow. */
.hook-form input[name="events"] { flex: 1 1 11rem; min-width: 0; width: auto; }
.hook-acts { display: flex; flex-wrap: wrap; gap: var(--sp-2); }
.hook-lbl { flex: 0 0 auto; }
.hook-add p { margin: var(--sp-2) 0; }
.hook-add input[type="url"] { width: 100%; }
/* A table sizes to its content, and an <input size=28> in a cell cannot
   shrink — so this table's min-content width exceeded a phone and made
   the WHOLE page wider than the viewport. Everything then ran off the
   right edge, which reads as fields having no padding. The table scrolls
   inside its own box instead; the page does not. */
.tablewrap { overflow-x: auto; max-width: 100%; }
.caps-table { border-collapse: collapse; margin: var(--sp-2) 0; }
.caps-table td { padding: var(--sp-1) var(--sp-2) var(--sp-1) 0; vertical-align: middle; }
.caps-table input { font-family: var(--font-mono); }
/* The data table: one shape for every admin listing (fleet People,
   sessions), the way a forge people already know draws them — a
   framed, full-width table, a quiet header row, one line per row,
   the actions right-aligned and small. Tables that were bare and
   narrow with stacked buttons read as unfinished; this is the same
   information with the same controls, composed. */
.dt-head { display: flex; align-items: baseline; gap: var(--sp-2); margin: var(--sp-5) 0 var(--sp-2); }
.dt-head:first-child { margin-top: var(--sp-2); }
.dt-title { font-size: var(--fs-lg); margin: 0; }
.dt-n { font-size: var(--fs-sm); color: var(--muted); border: 1px solid var(--rule); border-radius: var(--r-4); padding: 0 var(--sp-2); font-variant-numeric: tabular-nums; }
.dt-wrap { border: 1px solid var(--rule); border-radius: var(--r-2); overflow-x: auto; background: var(--surface); margin: 0 0 var(--sp-2); }
.dt { width: 100%; border-collapse: collapse; font-size: var(--fs-body); }
.dt th { text-align: left; font-size: var(--fs-xs); font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; color: var(--muted);
  padding: var(--sp-2) var(--sp-4); border-bottom: 1px solid var(--rule); background: color-mix(in srgb, var(--rule) 22%, transparent); white-space: nowrap; }
.dt td { padding: var(--sp-2) var(--sp-4); border-bottom: 1px solid var(--rule); vertical-align: middle; white-space: nowrap; }
.dt tbody tr:last-child td { border-bottom: 0; }
.dt tbody tr:hover td { background: color-mix(in srgb, var(--rule) 14%, transparent); }
.dt tr.muted td { color: var(--muted); }
.dt .who { display: inline-flex; align-items: center; gap: var(--sp-2); font-weight: 600; }
.dt .who a { color: inherit; }
.dt .sub { display: block; color: var(--muted); font-size: var(--fs-sm); font-weight: 400; margin-top: var(--sp-0); }
.dt code.ip { font-size: var(--fs-sm); background: none; padding: 0; color: inherit; }
.dt td.dt-act, .dt th.dt-act { text-align: right; width: 1%; }
.dt td.dt-act form { display: inline-block; margin-left: var(--sp-2); }
.dt td.dt-act .btn { font-size: var(--fs-sm); padding: var(--sp-1) var(--sp-2); }

{{- /* A phone cannot hold a device name and a full IPv6 address side
       by side, and the wrapper's sideways scroll is one nobody finds
       inside a card. Below the fold the session table stops being a
       table: one block per device, each cell on its own line under the
       name it goes by. Only this table — the fleet's own are wider
       still and belong on a wider screen.

       This block sits AFTER the .dt base rules on purpose. It used to
       sit before them, where ".dt-stack td.dt-act { width: auto }" and
       ".dt td.dt-act { width: 1% }" have the same specificity and the
       later one wins: the action cell stayed 1% wide, the button was
       squeezed to its minimum content width, and overflow-wrap:anywhere
       — which is here for IPv6 — then broke "Sign out" one letter per
       line. Keep it last, and do not add .dt rules below it. */ -}}
@media (max-width: 40rem) {
  .dt-stack thead { display: none; }
  .dt-stack, .dt-stack tbody, .dt-stack tr, .dt-stack td { display: block; width: auto; }
  {{- /* "main .dt-wrap > table { display: table }" further down the
         sheet — the rule that spares the forge's own wrapped tables
         from the rendered-content scroll treatment — outranks
         ".dt-stack" (0,1,1 against 0,1,0), so the table stayed a table
         while its rows became blocks. A display:table box shrink-wraps
         its content, so the stacked card was as wide as its widest cell
         and every row separator stopped mid-card. This wins on
         specificity rather than order, so it holds wherever it sits. */ -}}
  main .dt-wrap > table.dt-stack { display: block; }
  .dt-stack tr { padding: var(--sp-2) 0; border-bottom: 1px solid var(--rule); }
  .dt-stack tr:last-child { border-bottom: 0; }
  .dt-stack td { border: 0; padding: var(--sp-0) var(--sp-4); white-space: normal; overflow-wrap: anywhere; }
  .dt-stack td[data-label]::before { content: attr(data-label) " · "; color: var(--muted); }
  .dt-stack td.dt-act { text-align: left; width: auto; padding-top: var(--sp-1); }
  .dt-stack td.dt-act form { display: block; margin-left: 0; }
  {{- /* A control never breaks mid-word, whatever the cell around it
         needs for addresses. */ -}}
  .dt-stack td.dt-act .btn { overflow-wrap: normal; white-space: nowrap; }
}
.invite-card { border: 1px solid var(--rule); border-radius: var(--r-2); background: var(--surface); padding: var(--sp-3) var(--sp-4) var(--sp-4); margin: var(--sp-2) 0 var(--sp-5); }
.invite-card .dt-title { margin-bottom: var(--sp-2); }
.invite-form { display: grid; grid-template-columns: 13rem minmax(0, 1fr) auto auto; gap: var(--sp-3); align-items: end; }
.invite-form label { display: flex; flex-direction: column; gap: var(--sp-1); font-size: var(--fs-sm); color: var(--muted); }
.invite-form label input:not([type=checkbox]) { font-size: var(--fs-body); width: 100%; }
.invite-form .chk { flex-direction: row; align-items: center; gap: var(--sp-2); padding-bottom: var(--sp-2); white-space: nowrap; }
.invite-form .btn-primary { padding: var(--sp-2) var(--sp-4); }
@media (max-width: 40rem) {
  .invite-form { grid-template-columns: 1fr; }
  .invite-form .chk { padding-bottom: 0; }
}
/* The People section and the per-person editor (RFC 0008). A row is a
   name, chips, and one sentence: the chips are the role and the ways a
   person differs from it, so an ordinary row is one chip long. */
.roster { margin: var(--sp-2) 0; }
.roster li { padding: var(--sp-2) var(--sp-4); }
.roster-head { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2); }
.roster-can { margin: var(--sp-0) 0 0; color: var(--muted); font-size: var(--fs-body); }
.chip.role { color: var(--fg); border-color: var(--fg); }
/* A repository reached through a team holds no grant row, so its chip
   says that instead of a role. Muted, because it is the weaker claim
   of the two and should not read as loudly as a granted one. */
.chip.role.t-team { color: var(--muted); border-color: var(--rule); }
.chip.plus { color: var(--ok); }
.chip.minus { color: var(--warn); }
.chip.owns { font-style: italic; }
.roster-add { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2) var(--sp-3); margin-top: var(--sp-2); }
.person-form fieldset { border: 1px solid var(--rule); margin: var(--sp-2) 0; padding: var(--sp-2) var(--sp-3); }
.person-form legend { font-weight: 600; padding: 0 var(--sp-1); }
.person-form label { display: block; padding: var(--sp-0) 0; }
.person-form .roles label small { color: var(--muted); }
.person-form .abilities { display: grid; grid-template-columns: repeat(auto-fill, minmax(18rem, 1fr)); gap: 0 var(--sp-4); }
.person-form .abilities label.inherited { color: var(--muted); }
.person-form .abilities .ab-from { font-size: .8em; color: var(--muted); white-space: nowrap; }
.person-form label.adv { color: var(--muted); }
.person-form .raw { margin: var(--sp-2) 0; }
.status.err { color: var(--danger); }
/* Settings (RFC 0008 refit). One long column of forms beside a rail
   that says where the reader is; a summary card above both. The
   footer flows here rather than floating: nothing on this page is
   read against it, and the last Save button used to sit under it. */
body.flow-footer { padding-bottom: 0; min-height: 100dvh; display: flex; flex-direction: column; }
/* In the flow, but never adrift: a short page (fleet settings is one
   form) pushes the footer to the bottom of the viewport; a long one
   has it after the last control, where the reader arrives. */
/* A flex item with auto side margins is not stretched: it takes its
   fit-content width, and fit-content is at least min-content, which
   for the settings page is the nowrap rail — main grew to 1200px on a
   phone and the page scrolled sideways. A definite width restores the
   block behaviour: the rail and the clone line scroll inside it. */
body.flow-footer > main { flex: 1 0 auto; width: 100%; min-width: 0; box-sizing: border-box; }
/* A flowing footer needs no safe-area inset, and adding one is what
   makes it look detached on a phone. The inset exists for the FIXED
   footer, which lies over the page and can be overlapped by the home
   indicator once Safari hides its toolbar. A flowing footer sits at the
   end of a 100dvh body, and dvh already excludes the browser chrome —
   so the inset is counted twice and leaves an empty band under the
   text. The gap above the text shrinks on a phone too: 2.5rem between
   the rule and the line is generous on a desktop and reads as a
   stranded bar on a short page. */
body.flow-footer footer { position: static; backdrop-filter: none; margin-top: auto;
  padding-top: var(--sp-6); padding-bottom: 0; }
@media (max-width: 40rem) { body.flow-footer footer { padding-top: var(--sp-4); } }
.set-summary { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--sp-1) var(--sp-4);
  padding: var(--sp-3) var(--sp-4); margin: var(--sp-2) 0 var(--sp-4); border: 1px solid var(--rule);
  border-radius: var(--r-2); background: var(--surface); }
.set-name { margin: 0; font-size: var(--fs-lg); }
.set-counts { margin: 0; display: flex; flex-wrap: wrap; gap: var(--sp-1) var(--sp-4);
  color: var(--muted); font-size: var(--fs-body); font-variant-numeric: tabular-nums; }
.set-counts a { color: inherit; text-decoration: none; }
.set-counts a:hover { color: var(--fg); }
/* The clone line is a fact, not a setting; it keeps its <pre> so the
   text can be selected by hand where the copy button cannot appear. */
.set-clone { flex: 1 1 100%; display: flex; align-items: center; gap: var(--sp-2); min-width: 0; }
.set-clone pre { margin: 0; flex: 1 1 auto; min-width: 0; overflow-x: auto; font-size: var(--fs-md); }
.set-copy { background: none; border: 0; color: var(--muted); cursor: pointer;
  padding: var(--sp-0); line-height: 1; display: inline-flex; align-items: center; }
.set-copy .ic { width: 0.95rem; height: 0.95rem; }
.set-copy:hover { color: var(--accent); }
.set-copy.done { color: var(--add); }
.set-grid { display: grid; grid-template-columns: 12rem minmax(0, 1fr); gap: 0 var(--sp-6); align-items: start; }
/* The rail borrows the wiki TOC's shape: a rule down the left, the
   current entry drawn in the accent. Reset the global nav-link box
   first, as .w-toc does, or the current entry gets four sides. */
.set-rail { position: sticky; top: 0.6rem; font-size: var(--fs-md); }
.set-rail ul { list-style: none; margin: 0; padding: 0; border-left: 2px solid var(--rule); }
.set-rail ul ul { border-left: 0; }
.set-rail a { display: block; padding: var(--sp-1) var(--sp-2); margin-left: -2px;
  border: 0; border-left: 2px solid transparent; border-radius: 0; background: none;
  text-decoration: none; color: var(--muted); line-height: 1.35; }
.set-rail ul ul a { padding-left: var(--sp-5); font-size: var(--fs-sm); }
.set-rail a:hover { color: var(--fg); }
.set-rail a:focus-visible { outline: none; color: var(--accent); border-left-color: var(--accent); }
.set-rail a[aria-current] { color: var(--accent); border-left-color: var(--accent); font-weight: 600; }
.set-rail .n { font-variant-numeric: tabular-nums; opacity: 0.8; }
.set-rail .n::before { content: "· "; }
.set-body { min-width: 0; }
.set-body > section { padding: var(--sp-2) 0 var(--sp-5); border-bottom: 1px solid var(--rule); scroll-margin-top: 0.8rem; }
.set-body > section:last-child { border-bottom: 0; }
.set-body h2 { font-size: var(--fs-lg); margin: var(--sp-2) 0 var(--sp-2); }
.set-body h3 { font-size: var(--fs-base); margin: var(--sp-4) 0 var(--sp-1); }
.set-body section:target > h2 { color: var(--accent); }
.set-body details.why > summary { color: var(--muted); font-size: var(--fs-body); padding: var(--sp-1) 0; }
details.capref > summary { cursor: pointer; padding: var(--sp-1) 0; }
table.capref tr.unused td { color: var(--muted); opacity: 0.6; }
/* A control that cannot act says why, beside itself, in the muted
   voice — not as prose three paragraphs away. */
.set-why { color: var(--muted); font-size: var(--fs-md); margin: var(--sp-1) 0 0; }
/* A protected branch reads as a STATE, not a warning: the rule is
   ordinary and most repositories have one. Distinct from the closed
   badge beside it so the two are not read as the same kind of thing. */
.tl-badge.b-prot { border-color: var(--accent); color: var(--accent); }
/* Qualified with .tl-badge on purpose: the base .tl-badge rule is
   LATER in this file and its border shorthand beats a bare
   .b-prot border-color on a specificity tie, so the border was
   silently dropped while the colour took. Same shape as the
   m-failing and imp-run rules above. */
/* A credential shown exactly once: it has to be selectable, legible and
   impossible to mistake for prose, and it must wrap rather than run off
   a phone. */
.push-token { font-family: var(--font-mono); font-size: var(--fs-body);
	/* break-all split the credential mid-GROUP on a phone (…-8VD / T),
	   which is the one place a reader has to transcribe accurately.
	   anywhere prefers the existing break opportunities — the group
	   separators — and only splits a group if one alone overflows.
	   No backtick in this comment: the stylesheet is a Go raw string. */
	user-select: all; word-break: normal; overflow-wrap: anywhere;
	white-space: pre-wrap;
	background: var(--code-bg); border: 1px solid var(--rule);
	border-radius: var(--r-row); padding: var(--sp-2); margin: var(--sp-2) 0; }
/* A rejected submission hands the author their own text back, so the
   block has to be readable and must not run away with the page. */
.f-held-body { max-height: 18rem; overflow: auto; font-size: var(--fs-md);
	white-space: pre-wrap; background: var(--code-bg);
	border: 1px solid var(--rule); border-radius: var(--r-row); padding: var(--sp-2); }
.row-act { display: inline-flex; gap: var(--sp-2); align-items: center; margin: 0; }
.roster-head .row-act { margin-left: auto; }
.row-act button:disabled, .danger-row button:disabled { background: none; color: var(--muted);
  border-style: dashed; cursor: not-allowed; }
.field-err { display: block; color: var(--danger); }
input[aria-invalid="true"] { border-color: var(--danger); }
/* The role picker as one segmented control: radios underneath, so the
   form posts the same field it always did. */
.segc { display: inline-flex; border: 1px solid var(--rule); border-radius: var(--r-row); padding: 0; margin: 0; overflow: hidden; }
.segc label { position: relative; padding: var(--sp-1) var(--sp-3); cursor: pointer; }
.segc label + label { border-left: 1px solid var(--rule); }
.segc input { position: absolute; inset: 0; opacity: 0; margin: 0; }
.segc label:has(input:checked) { background: var(--code-bg); font-weight: 600; }
.segc label:has(input:focus-visible) { outline: 2px solid var(--accent); outline-offset: -2px; }
/* Chip inputs (unit 2): the text field is kept, hidden, and the
   script draws its tokens as chips with an entry beside them. A
   palette shows every option as a toggle — the event nouns — where an
   entry shows only what is chosen. Without the script the field is a
   plain input with its datalist. */
.chips { display: inline-flex; flex-wrap: wrap; align-items: center; gap: var(--sp-1);
  min-width: 12rem; max-width: 100%; padding: var(--sp-1) var(--sp-2); border: 1px solid var(--rule);
  border-radius: var(--r-row); background: var(--bg); vertical-align: middle; }
.chips:focus-within { outline: 2px solid var(--accent); outline-offset: -1px; }
.chips .tok { border: 1px solid var(--rule); border-radius: var(--r-4); background: var(--code-bg);
  color: var(--fg); padding: var(--sp-0) var(--sp-2); font-size: var(--fs-md); line-height: 1.4; cursor: pointer; }
.chips .tok::after { content: " ×"; color: var(--muted); }
.chips-palette .tok { background: none; color: var(--muted); }
.chips-palette .tok::after { content: none; }
.chips-palette .tok.on { background: var(--code-bg); color: var(--fg); border-color: var(--accent); }
.chips .chips-entry { flex: 1 1 6rem; min-width: 6rem; border: 0; background: none;
  padding: var(--sp-0) var(--sp-1); font: inherit; color: inherit; }
.chips .chips-entry:focus { outline: none; }
.set-body .caps-table td .chips { width: 100%; }
button[aria-disabled="true"] { opacity: 0.55; }
.set-filter { margin: var(--sp-2) 0; }
.set-filter input { width: 100%; max-width: 20rem; }
.set-rail a[data-dirty]::after { content: " ●"; color: var(--accent); font-size: 0.6em; vertical-align: middle; }
/* A rule reads as a sentence; its fields live under the disclosure,
   so the list is scannable and the edit is one click away. */
/* A rule is a card row: the sentence, its warnings, then the editor
   folded under it. The row carries the card's inner padding — the
   list-card frame draws only the edges — and the editor's fields line
   up on one label column so the eye reads down, not across. */
.set-rules { list-style: none; margin: var(--sp-2) 0; padding: 0; }
.set-rules > li { padding: var(--sp-3) var(--sp-4); border-bottom: 1px solid var(--rule); }
.set-rules > li:last-child { border-bottom: 0; }
.rule-edit, .set-rules .set-new > details { margin-top: var(--sp-1); }
.rule-edit > summary:focus-visible, .set-rules .set-new > details > summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: var(--r-1); }
.rule-edit[open] > summary, .set-rules .set-new > details[open] > summary { margin-bottom: var(--sp-2); }
.rule-edit .row-act { margin: var(--sp-2) 0 var(--sp-0); }
.rule-said { margin: 0 0 var(--sp-1); }
.rule-warn { margin: var(--sp-0) 0; color: var(--warn); font-size: 0.92em; }
.rule-edit > summary, .set-rules .set-new > details > summary { cursor: pointer; color: var(--muted); font-size: 0.92em; }
.rule-f { margin: var(--sp-2) 0; }
.rule-f label { display: grid; grid-template-columns: 8.5rem minmax(0, 1fr); align-items: center; gap: var(--sp-2) var(--sp-2); color: var(--muted); }
.rule-f label:has(input[type="checkbox"]) { display: flex; align-items: center; }
@media (max-width: 40rem) { .rule-f label { grid-template-columns: 1fr; } }
.rule-f input[type="text"], .rule-f input:not([type]) { max-width: 100%; }
/* A field with its unit shares the one column; the unit is not a third grid item on a row of its own. */
.rule-f .with-unit { display: flex; align-items: center; gap: var(--sp-2); }
.rule-f .with-unit input { width: 7ch; }
.roster .row-act { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2); }
.roster .row-act select { max-width: 12rem; }
/* What you can DO to this line of work. Kept apart from the row above,
   which is where you GO — a live button next to the CLOSED marker made
   a status and a control read as the same kind of thing.

   The link and the buttons are given one style here rather than
   inherited from anywhere: a submit and an anchor start life looking
   nothing alike, and these two sit side by side. */
/* The queue's blind spot, set apart from the queue itself: same row
   shape, its own heading, a rule above so it does not read as more of
   the list. */
.unproposed { margin: var(--sp-5) 0 0; border-top: 1px solid var(--rule); padding-top: var(--sp-4); }
/* The delivery log, folded under each endpoint: a status, what it was,
   when, and how long — the row an owner reads to answer "did it fire". */
.hook-log { margin: var(--sp-1) 0 0; }
.hook-log summary { cursor: pointer; color: var(--muted); font-size: var(--fs-body); }
.hook-att { width: 100%; border-collapse: collapse; margin: var(--sp-2) 0 0; font-size: var(--fs-md); }
.hook-att td { padding: var(--sp-1) var(--sp-2) var(--sp-1) 0; vertical-align: top; }
.d-st { font-weight: 600; font-variant-numeric: tabular-nums; }
.d-st.ok { color: var(--add); }
.d-st.bad { color: var(--del); }
.d-when, .d-ms { color: var(--muted); white-space: nowrap; font-variant-numeric: tabular-nums; }
.d-err { color: var(--del); overflow-wrap: anywhere; }
/* A kind is one word and must not be hyphenated into a column of
   letters, which is what a four-character cell does to it. */
.hook-att td code { white-space: nowrap; }
@media (max-width: 40rem) {
  /* Narrow: an attempt is a line that wraps, not a row of columns that
     cannot. Same move the caps table makes at this width. */
  .hook-att, .hook-att tbody, .hook-att tr, .hook-att td { display: block; width: auto; }
  .hook-att tr { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-0) var(--sp-2);
    padding: var(--sp-2) 0; border-bottom: 1px solid var(--rule); }
  .hook-att tr:last-child { border-bottom: 0; }
  .hook-att td { padding: 0; }
  .hook-att td:empty { display: none; }
  .hook-att .row-act { margin-left: auto; }
  .d-err { flex-basis: 100%; }
}
.trail { list-style: none; margin: var(--sp-2) 0; padding: 0; }
.trail li { padding: var(--sp-1) 0; border-bottom: 1px solid var(--rule); }
.trail li:last-child { border-bottom: 0; }
.trail-what { display: block; }
.unproposed h2 { font-size: var(--fs-base); margin: 0 0 var(--sp-2); color: var(--muted); font-weight: 600; }
.ci-acts { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2); margin: 0 0 var(--sp-3); }
.ci-acts form { display: contents; }
.ci-acts a.btn, .ci-acts button {
  display: inline-flex; align-items: center; gap: var(--sp-2);
  font: inherit; font-weight: 600; color: var(--fg); text-decoration: none;
  background: var(--code-bg); border: 1px solid var(--rule); border-radius: var(--r-row);
  padding: var(--sp-1) var(--sp-3); cursor: pointer; }
.ci-acts a.btn:hover, .ci-acts button:hover { border-color: var(--fg); text-decoration: none; }
.ci-acts .set-why { margin: 0; }
.rule-probe { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2); margin: var(--sp-2) 0 var(--sp-1); }
.rule-probe label { display: flex; align-items: center; gap: var(--sp-2); color: var(--muted); }
.probe-said { margin: var(--sp-1) 0 0; }
.danger-row { padding: var(--sp-1) 0; }
.danger-row + .danger-row { border-top: 1px solid var(--rule); margin-top: var(--sp-2); }
/* One dialog for every destructive button: the sentence of
   consequence and the button's own word, nothing else. */
dialog.confirm { border: 1px solid var(--rule); border-radius: var(--r-2); background: var(--bg);
  color: var(--fg); padding: var(--sp-4) var(--sp-4); max-width: 26rem; box-shadow: var(--shadow-1); }
dialog.confirm::backdrop { background: rgba(0, 0, 0, 0.4); }
.confirm-why { margin: 0 0 var(--sp-4); }
.confirm-acts { display: flex; justify-content: flex-end; gap: var(--sp-2); margin: 0; }
.danger-btn { color: var(--del); border-color: var(--del); }
@media (max-width: 40rem) {
  /* The rail becomes a strip pinned under the repo bar — the one
     sticky thing on the page — and every nested entry joins it. */
  .set-grid { display: block; }
  .set-rail { position: sticky; top: 0; z-index: 4; background: var(--bg);
    margin: 0 calc(-1 * var(--gutter)); padding: var(--sp-2) var(--gutter);
    border-bottom: 1px solid var(--rule);
    overflow-x: auto; white-space: nowrap; -webkit-overflow-scrolling: touch; }
  .set-rail ul { display: flex; gap: var(--sp-1); border-left: 0; }
  .set-rail li, .set-rail ul ul { display: contents; }
  .set-rail a { display: inline-block; margin: 0; padding: var(--sp-1) var(--sp-3);
    border: 1px solid var(--rule); border-radius: var(--r-4); }
  .set-rail ul ul a { padding-left: var(--sp-3); font-size: var(--fs-sm); }
  .set-rail a[aria-current], .set-rail a:focus-visible { border-color: var(--accent); }
  .set-body > section { scroll-margin-top: 3.4rem; }
  /* Tables become cards: one rule per card, each cell labelled by the
     name the desktop kept in aria-label. Nothing scrolls sideways. */
  .set-body .tablewrap { overflow: visible; }
  .set-body .caps-table, .set-body .caps-table tbody, .set-body .caps-table td { display: block; }
  /* Once the rows stack there is no column for a header to sit over,
     and a thead left in table layout renders as one bold heading per
     column above the whole table. The cells carry data-lbl instead. */
  .set-body .caps-table thead { display: none; }
  .set-body .caps-table tr { display: grid; gap: var(--sp-1); padding: var(--sp-2) 0; border-bottom: 1px solid var(--rule); }
  .set-body .caps-table td { padding: 0; }
  .set-body .caps-table td:empty { display: none; }
  .set-body .caps-table td[data-lbl]::before { content: attr(data-lbl); display: block;
    font-size: var(--fs-xs); color: var(--muted); text-transform: uppercase; letter-spacing: 0.05em; }
  .set-body .caps-table input:not([type=checkbox]) { width: 100%; }
  .set-body input:not([type=checkbox]):not([type=radio]):not([type=file]) { max-width: 100%; }
  .set-body code, .set-body .hook-url { overflow-wrap: anywhere; }
  .set-summary { padding: var(--sp-2) var(--sp-3); }
  /* Thumb-sized rows for the radios; the segmented control fills the width. */
  .vis-form label { min-height: 2.75rem; display: flex; align-items: center; gap: var(--sp-2); }
  /* The segmented control becomes pills that wrap. As one strip it
     had a min-content width wider than a phone (five roles, none of
     them able to shrink), so it did not scroll — it was CLIPPED, and
     with it the sentence beside it, which a flex column had stretched
     to the strip's width. Losing a role you cannot see is worse than
     losing the joined-up look. */
  .segc { display: flex; flex-wrap: wrap; gap: var(--sp-1); width: 100%;
    border: 0; overflow: visible; }
  .segc label, .segc label + label { flex: 1 1 auto; min-width: 0;
    min-height: 2.75rem; display: flex; align-items: center;
    justify-content: center; border: 1px solid var(--rule);
    border-radius: var(--r-row); }
  .roster-add { flex-direction: column; align-items: stretch; }
  .roster-add > label { display: flex; align-items: center; gap: var(--sp-2); }
  .roster-add > label input { flex: 1; min-width: 0; }
  .roster-head .row-act { margin-left: 0; width: 100%; }
  .hook-acts { width: 100%; }
  .hook-acts button { flex: 1; }
  /* The Save bar sticks to the bottom only while the form is dirty
     (Datastar sets the class); clean, it is a paragraph like any other. */
  form.dirty .save-bar { position: sticky; bottom: 0; z-index: 3;
    margin: 0 calc(-1 * var(--gutter)); padding: var(--sp-2) var(--gutter);
    background: var(--surface); border-top: 1px solid var(--rule); }
  form.dirty .save-bar button { width: 100%; }
}
.ci-override { margin-top: var(--sp-2); }
/* Inside the decision row the override is a LINE of its own under the
   controls, not a flex item wedged between two buttons: the summary
   reads as a quiet link, and opening it lays the reason and the
   button out on one wrapping line. */
.d-act .ci-override { flex-basis: 100%; margin: 0; order: 9; }
.d-row.d-armed { background: color-mix(in oklab, var(--accent) 8%, transparent); }
.d-act .ci-override summary { display: inline-block; padding: var(--sp-0) 0; font-size: 0.95em; }
.d-act .ci-override p { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2); margin: var(--sp-2) 0 0; }
.d-act .ci-override input { flex: 1 1 16rem; min-width: 0; }
/* The run page's log panes (RFC 0009 §logs). */
/* A red check on the decision card: the row is the summary, the failed
   step's tail is the body, open by default — red is exactly when the
   reader needs the why without leaving the decision. */
/* The CI list: every row answers what happened. The chip and the title
   both open the run; a red row says which step and quotes the line. */
.ci-filter { display: flex; gap: var(--sp-2); align-items: center; flex-wrap: wrap; margin: 0 0 var(--sp-4); }
.ci-health { display: flex; gap: var(--sp-2); align-items: baseline; flex-wrap: wrap; border: 1px solid var(--rule); border-radius: var(--r-2); padding: var(--sp-2) var(--sp-4); margin: 0 0 var(--sp-2); }
/* The tripwire strip: the acknowledge button sits at the row's end,
   the "all" form reads as one line with the history link. */
.ci-gate form { margin-left: auto; }
.ci-gate-foot form.inline { display: inline; }
.ci-pager { display: flex; gap: var(--sp-2); justify-content: space-between; margin: var(--sp-2) 0 0; }
.ci-pager a:only-child:last-child { margin-left: auto; }
/* The People tab: an invite link must survive being copied whole, and
   a shut account reads as shut. */
.invite-link { word-break: break-all; user-select: all; }
/* Sign-in and invite forms: fields a hand's width, not a screen's. */
/* A page that is one form: the COLUMN narrows, so the heading stays
   over the fields it names. Centring the form inside a listing's
   column would have left the h1 at the far left of it. */
main.narrow { max-width: 34rem; }
.auth-form { max-width: 24rem; }
.auth-form input { width: 100%; box-sizing: border-box; font-size: max(var(--fs-base), 1em); }
tr.muted td { color: var(--muted); }
.ci-rows { border: 1px solid var(--rule); border-radius: var(--r-2); }
.ci-row { display: flex; gap: var(--sp-3); align-items: flex-start; padding: var(--sp-2) var(--sp-4); border-bottom: 1px solid var(--rule); }
.ci-row:last-child { border-bottom: none; }
.ci-chip { text-decoration: none; padding-top: var(--sp-0); }
.ci-main { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: var(--sp-0); }
.ci-title { color: var(--ink, inherit); font-weight: 600; text-decoration: none; overflow-wrap: anywhere; }
.ci-title:hover { text-decoration: underline; }
.ci-why { font-size: var(--fs-md); overflow-wrap: anywhere; }
.ci-why code { font-size: 0.85em; }
.ci-meta { color: var(--muted); font-size: var(--fs-sm); }
.ci-side { display: flex; gap: var(--sp-3); align-items: baseline; white-space: nowrap; }
.ci-dur { font-variant-numeric: tabular-nums; }
.ci-rerun { font-size: var(--fs-sm); padding: var(--sp-0) var(--sp-2); }
.ci-pipes { margin-top: var(--sp-3); }
/* On a phone the row's three columns do not fit: duration, age and
   Re-run are nowrap and take about 200px, leaving the title 150 and
   forcing it to break mid-word — "gofmt cmd/quarr / y/main.go". The
   side drops to its own line instead, which gives the title the full
   width and lets the badge stay beside the words it qualifies. */
@media (max-width: 34rem) {
  .ci-row { flex-wrap: wrap; gap: var(--sp-2); }
  .ci-main { flex: 1 1 100%; }
  .ci-side { width: 100%; justify-content: flex-end; }
  /* Break only where a word genuinely cannot fit: overflow-wrap
     anywhere splits ordinary prose mid-syllable, which is what made
     these unreadable at this width. */
  .ci-title, .ci-why { overflow-wrap: break-word; }
}
.ci-editor textarea { width: 100%; font: var(--fs-md)/1.55 var(--font-mono); background: none; color: inherit; border: 1px solid var(--rule); border-radius: var(--r-2); padding: var(--sp-3) var(--sp-4); resize: vertical; }

details.l-check summary { list-style: none; cursor: pointer; display: flex; align-items: baseline; gap: var(--sp-2); flex-wrap: wrap; }
details.l-check summary::-webkit-details-marker { display: none; }
details.l-check .runlog { margin: var(--sp-2) 0 var(--sp-0); max-height: 22rem; overflow-y: auto; }

/* The steps are a LIST of one run's parts, so they read as one object
   with rules between them rather than three cards with gaps. The whole
   list takes the border; each step takes a divider.
   
   The caret is the affordance the native triangle used to be, on the
   far side of the row: the status icon leads, because what a reader
   scans for is which step failed, not which step opens. */
.runsteps { border: 1px solid var(--rule); border-radius: var(--r-2); overflow: hidden; margin: var(--sp-2) 0 var(--sp-4); }
.runstep + .runstep { border-top: 1px solid var(--rule); }
.runstep > summary { align-items: center; }
.runstep > summary:hover { background: var(--surface); }
.runstep[open] > summary { border-bottom: 1px solid var(--rule); }
.runstep .d-line { flex: 1; }
.runstep .d-caret { flex: none; width: 14px; height: 14px; color: var(--muted);
  transition: transform var(--t-fast) ease; }
.runstep[open] .d-caret { transform: rotate(180deg); }
.runstep .d-body { padding: var(--sp-2) var(--sp-4) var(--sp-3); background: var(--bg); }
/* A retried step's attempts, each its own fold inside the step. */
.runattempt + .runattempt { margin-top: var(--sp-2); }
.runattempt > summary { display: flex; align-items: center; gap: var(--sp-1); cursor: pointer; color: var(--muted); }
/* A log line is as long as the command that wrote it, and nothing
   wraps it. The block scrolls inside its own box or the page
   scrolls sideways — which on a phone is the whole page, for one
   line of somebody's stack trace. */
.runlog { line-height: 1.45; overflow-x: auto; overflow-y: auto; max-width: 100%; min-width: 0; }
/* The run page's log controls. A row that wraps to a column on a phone
   rather than scrolling sideways, because the log already owns the
   horizontal axis. */
.run-tools { display: flex; flex-wrap: wrap; gap: var(--sp-2) var(--sp-4); align-items: center;
  justify-content: space-between; margin: var(--sp-4) 0 var(--sp-2); }
.run-filter, .run-acts { display: flex; flex-wrap: wrap; gap: var(--sp-2); align-items: center;
  font-size: var(--fs-md); }
.run-filter input[type="search"] { min-width: 12rem; }
.run-acts form { display: inline; }
.run-clear { font-size: var(--fs-md); }
/* The wrap toggle is a checkbox and a sibling selector: rewrapping text
   that is already on the page must not cost a round trip. */
.run-wrapbox { margin: 0; }
.run-tools:has(.run-wrapbox:checked) ~ * .runlog,
.run-tools:has(.run-wrapbox:checked) ~ .runlog { white-space: pre-wrap; overflow-wrap: anywhere; }
/* A long step's log scrolls past its own heading, and then nothing on
   screen says which step you are reading. */
details.runstep > summary { position: sticky; top: 0; z-index: 1;
  background: var(--bg); padding-top: var(--sp-1); padding-bottom: var(--sp-1); }
/* The row that HOLDS the log has to be shrinkable too: a pre can only
   scroll inside a parent that is itself allowed to be narrower than the
   text. Without this the panel widened and the log ran off the card. */
details.l-check, details.l-check > summary, .l-decide { min-width: 0; }
details.l-check > summary { flex-wrap: wrap; }
.runlog .ll { display: block; }
.runlog .ll-err { color: var(--del); }
.runlog .ll-mark { color: var(--muted); font-style: italic; }
.ll-n { display: inline-block; min-width: 3ch; margin-right: 1ch; color: var(--muted); text-align: right; text-decoration: none; user-select: none; }
.ll:target { background: var(--mark); }
/* RFC 0009: the decision card. Status rows lead — icon, headline,
   muted subline — the verbs close the card; nothing about the decision
   lives anywhere else on the page. */
.l-decide { border: 1px solid var(--rule); border-radius: var(--r-2); margin: var(--sp-3) 0; }
/* One row, wherever it is drawn.
   
   These four rules were scoped to .l-decide, the landing's decision
   card. The run page draws the SAME <details class="d-row"> markup and
   sat outside that scope, so it got nothing but the border .runstep
   adds: padding 0, summary still display:list-item, and the browser's
   own disclosure triangle beside a status icon already saying the same
   thing. Three cramped boxes with a stray arrow, which is what dmestas
   photographed.
   
   What is genuinely the decision card's — the divider between its rows,
   and its non-details rows laying out as flex — stays scoped below. */
.d-row { display: block; padding: var(--sp-3) var(--sp-4); }
.d-row > summary { list-style: none; cursor: pointer; display: flex;
  align-items: baseline; gap: var(--sp-2); }
.d-row > summary::-webkit-details-marker { display: none; }
.l-decide .d-row { border-bottom: 1px solid var(--rule); }
.l-decide div.d-row { display: flex; align-items: baseline; gap: var(--sp-2); }
.d-ic { flex: none; width: 16px; height: 16px; transform: translateY(2px); }
.d-ic.pass { color: var(--add); fill: var(--add); }
.d-ic.fail { color: var(--del); fill: var(--del); }
.d-ic.wait { color: var(--muted); fill: var(--muted); }
/* A lane the engine still reports after the verdict is in. */
.d-ic.stopped { color: var(--muted); fill: var(--muted); }
.d-line { min-width: 0; }
.d-sub { color: var(--muted); font-size: var(--fs-body); }
.d-meta { color: var(--muted); font-size: var(--fs-md); margin-left: auto; white-space: nowrap; }
.d-meta a { color: var(--muted); }
.d-body { padding: var(--sp-2) 0 0 var(--sp-5); }
.d-body .l-check { margin: var(--sp-1) 0; }
/* The waiting row is a flex line of badge, name and note. The details
   row is NOT: a details element made flex puts its body in a slot the
   sheet cannot reach, and a log inside that slot grew to its longest
   line and ran off the card. The details stays a block; only its
   summary lays out as a line. */
p.l-check { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2); }
details.l-check { display: block; }
.d-act { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2); row-gap: var(--sp-2); padding: var(--sp-3) var(--sp-4); }
/* A refused verb must not look pressable: the card's disabled buttons
   go ghost — dashed, muted, no fill — the same honesty the reason
   sentence beside them speaks. */
.d-act button:disabled { background: none; color: var(--muted); border: 1px dashed var(--rule); cursor: not-allowed; }
/* acting: the form has been submitted and the server is working. The
   pressed control says so with a spinner; the rest stop taking
   presses, so a second click cannot start a second one.
   
   Its own class, not part of .d-act, because .d-act also carries the
   landing card's layout, its ghost-disabled rule and the CI override
   block -- four concerns under one name, which is why this behaviour
   lived on exactly one page and 138 other submit buttons across the
   forge had no pending state at all. A form opts in with the acting
   class plus data-signals:busy; a button opts in with data-class:is-busy
   and a .b-busy label. Without script it is a plain form, as before. */
.acting .b-busy, .acting .d-progress { display: none; }
.acting button.is-busy .b-long, .acting button.is-busy .b-abbr, .acting button.is-busy .b-idle { display: none; }
.acting button.is-busy .b-busy { display: inline-flex; align-items: center; gap: var(--sp-2); }
.acting button.is-busy .b-busy::before { content: ""; width: 0.8em; height: 0.8em; border: 2px solid currentColor; border-right-color: transparent; border-radius: 50%; animation: d-spin 0.8s linear infinite; }
.acting.busy button, .acting.busy summary { pointer-events: none; opacity: 0.55; }
.acting.busy button.is-busy { opacity: 1; }
.acting .d-progress.show { display: inline; }
@keyframes d-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .acting button.is-busy .b-busy::before { animation: none; border-right-color: currentColor; opacity: 0.5; } }
.l-decide > :last-child { border-bottom: none; }
.ci-override summary { color: var(--muted); cursor: pointer; }
.vis-form label { display: block; padding: var(--sp-0) 0; }
.f-post-f { display: flex; justify-content: flex-end; padding: 0 var(--sp-3) var(--sp-3); }
.f-replybtn { font-size: var(--fs-md); padding: var(--sp-1) var(--sp-3); }
.f-reply-note { padding: var(--sp-2) var(--sp-3); border-bottom: 1px solid var(--rule);
  background: var(--surface); border-radius: var(--r-2) var(--r-2) 0 0; font-size: var(--fs-md); color: var(--muted); }
.f-post-h { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap;
  padding: var(--sp-2) var(--sp-3); background: var(--surface);
  border-bottom: 1px solid var(--rule); border-radius: var(--r-2) var(--r-2) 0 0;
  font-size: var(--fs-md); color: var(--muted); }
.f-post-h strong { color: var(--fg); }
.f-post-h .f-spacer { margin-left: auto; }
.f-post-b { padding: var(--sp-4) var(--sp-4) var(--sp-0); }
.f-post-b > :first-child { margin-top: 0; }
/* The vote pill: one bordered capsule, arrow · score · arrow. The two
   forms inside are the no-script mechanism; the pill is just clothes. */
.vote-pill { display: inline-flex; align-items: center; border: 1px solid var(--rule);
  border-radius: var(--r-4); background: var(--bg); line-height: 1.2; }
.vote-pill form { margin: 0; display: inline-flex; }
.vote-pill button { border: 0; border-radius: var(--r-4); background: none;
  padding: var(--sp-0) var(--sp-2); font-weight: normal; font-size: var(--fs-md); }
.vote-pill button:hover { background: var(--code-bg); border-color: transparent; }
.vote-pill .vscore { padding: 0 var(--sp-0); font-weight: 600; color: var(--fg); font-variant-numeric: tabular-nums; }
.vote-cast { color: var(--accent); }
/* Moderator actions fold behind a no-script kebab: details/summary. */
.f-kebab { position: relative; display: inline-block; }
.f-kebab > summary { list-style: none; cursor: pointer; padding: 0 var(--sp-2);
  border-radius: var(--r-1); color: var(--muted); font-weight: 600; }
.f-kebab > summary::-webkit-details-marker { display: none; }
.f-kebab > summary:hover { background: var(--code-bg); color: var(--fg); }
.f-kebab-pop { position: absolute; right: 0; top: 1.6rem; z-index: 5;
  display: flex; align-items: center; gap: var(--sp-2); padding: var(--sp-2);
  background: var(--bg); border: 1px solid var(--rule); border-radius: var(--r-row);
  box-shadow: 0 4px 14px rgba(0,0,0,0.25); white-space: nowrap; }
.f-kebab-pop form { margin: 0; display: flex; align-items: center; gap: var(--sp-2); }
.f-kebab-pop input { font-size: var(--fs-md); }
.f-kebab-pop button { font-size: var(--fs-md); }
/* The reply composer: textarea in a card, actions in a footer strip —
   the comment box every forge reader already knows. */
.f-reply { border: 1px solid var(--rule); border-radius: var(--r-2); margin-top: var(--sp-4); overflow: clip; }
.f-reply:focus-within { border-color: var(--accent); }
.f-reply textarea { border: 0; border-radius: var(--r-2) var(--r-2) 0 0; }
.f-reply textarea:focus-visible { outline: none; }
/* The decision row is ONE line. It grew from three verbs to five as
   landings learned to update and to land a whole strata, and the old
   rule let flex shrink the buttons: every label folded to three lines
   and the leftmost still ran off the side of a phone. flex:none and
   nowrap keep each button one line at its own width; where five cannot
   fit, the LABELS shorten rather than the row breaking. */
/* A confirmation is an EVENT, so it has a lifetime. It holds long
   enough to be read, then fades and COLLAPSES — collapsing as well as
   fading, so the page does not keep a paragraph of empty space where a
   sentence used to be.
   
   Done in CSS, not script: this page must behave the same without
   JavaScript, and an animation is the one way to give an element a
   lifetime that costs nothing when the script is absent. role="status"
   announces it on insert, before any of this runs, so a screen reader
   has already heard it.
   
   Reduced motion keeps it: someone who has asked for less movement is
   asking for less MOVEMENT, not for the message to be taken away. */
/* The confirmation lives ON the control that did the saving. Both
   labels sit in the same grid cell, so the button is sized by the wider
   of them and NOTHING moves when they swap — the whole reason to put it
   here rather than in the page's flow.

   The revert is CSS too, on the same four-second beat as the old flash:
   without script the label must still find its way back to "Save", and
   an animation is what gives it a lifetime for free. */
.btn-swap { display: inline-grid; align-items: center; justify-items: center; }
.btn-swap > span { grid-area: 1 / 1; }
/* Green, from the palette's own token — the same --add that marks a
   passing run and an open glyph, so "it worked" reads the same colour
   everywhere rather than this button inventing one. */
.sw-b { opacity: 0; color: var(--add); }
.did-save { border-color: var(--add); }
.did-save .sw-a { opacity: 0; animation: sw-back-a 300ms ease 4s forwards; }
.did-save .sw-b { opacity: 1; animation: sw-back-b 300ms ease 4s forwards; }
@keyframes sw-back-a { to { opacity: 1; } }
@keyframes sw-back-b { to { opacity: 0; } }
/* Reduced motion keeps the word and drops the movement, as the flash
   does: the button simply stays "Saved" until the page next renders. */
@media (prefers-reduced-motion: reduce) {
  .did-save .sw-a, .did-save .sw-b { animation: none; }
}
.flash-once { overflow: hidden; animation: flash-out 400ms ease 4s forwards; }
@keyframes flash-out {
  to { opacity: 0; max-height: 0; margin-top: 0; margin-bottom: 0;
       padding-top: 0; padding-bottom: 0; }
}
@media (prefers-reduced-motion: reduce) { .flash-once { animation: none; } }
/* Wraps: the decision row grows a refusal sentence and an override
   disclosure (RFC 0009), and clipping a button is never the answer. */
.f-reply-foot { display: flex; flex-wrap: wrap; row-gap: var(--sp-2); justify-content: flex-end;
  gap: var(--sp-2);
  padding: var(--sp-2) var(--sp-2); border-top: 1px solid var(--rule);
  background: var(--surface); border-radius: 0 0 var(--r-2) var(--r-2); }
/* One line per button, always: a label that folds makes a control three
   times the height of its neighbours, which reads as a different kind
   of thing rather than as the same button with more words. */
.f-reply-foot > * { flex: none; white-space: nowrap; }
/* Below the width where five verbs fit, the words give way — not the
   line. The short form keeps the same verb, so nothing has to be
   relearned on a phone. Reject keeps its word: there is no X in the
   sprite, and an unlabelled destructive control is worse than a
   cramped one. */
.b-abbr, .b-icon { display: none; }
@media (max-width: 30rem) {
  .f-reply-foot { gap: var(--sp-1); padding-left: var(--sp-1); padding-right: var(--sp-1); }
  .f-reply-foot > * { font-size: var(--fs-sm); padding-left: var(--sp-2); padding-right: var(--sp-2); }
  .b-long { display: none; }
  .b-abbr { display: inline; }
}
/* A second step for the narrowest phones still sold. Measured at 320:
   five abbreviated buttons want 364px against 302 of room, and Comment
   is the widest word left. It has a sprite icon and is the one action
   here that destroys nothing, so it is the one that loses its label. */
@media (max-width: 22rem) {
  .f-reply-foot > * { font-size: var(--fs-xs); padding-left: var(--sp-2); padding-right: var(--sp-2); }
  .b-word { display: none; }
  .b-icon { display: inline-block; }
}
.btn-primary { background: var(--accent); color: var(--bg); border-color: var(--accent); }
.btn-primary:hover { border-color: var(--accent); filter: brightness(1.1); }
/* btn-danger: the one convention for an act that cannot be undone.
   Outlined rather than filled, because a destructive control should
   read as different, not as the thing the page most wants pressed --
   that is btn-primary's job and there is one per view. --del is the
   same red the diff uses for a removed line, in both themes.

   This class used to exist only inside .dt td.dt-act, so it worked
   in one session table and nowhere else; every other irreversible
   control on the forge -- discarding a ticket, removing a webhook and
   its secret, revoking a token, unregistering a repository -- shipped
   with no class at all, visually identical to Save beside it. */
/* btn-quiet: the third weight, for a control that belongs in the row
   but is not what the row is for -- Ping and Rotate secret beside a
   webhook's Save. It keeps the button's shape and hit area and gives
   up the fill and the weight, so a row of five reads as one action,
   some peers, and two utilities instead of five equal offers.

   Deliberately NOT applied widely. The audit read "no secondary or
   tertiary tier" as a forge-wide gap; counting the actual clusters
   found four rows with three or more controls, and three of them are
   genuine peers or already correct -- the ticket footer has Comment as
   its primary and the rest neutral, which is right. One row needed
   this. A weight nobody needs is another thing to keep consistent. */
.btn-quiet { background: none; font-weight: 400; color: var(--muted); border-color: var(--rule); }
.btn-quiet:hover { color: var(--fg); border-color: var(--fg); }
.btn-danger { color: var(--del); border-color: color-mix(in srgb, var(--del) 45%, var(--rule)); }
.btn-danger:hover { border-color: var(--del); background: color-mix(in srgb, var(--del) 10%, transparent); }
/* The markdown composer: GitHub's Write | Preview tabs over a raw
   textarea. The tab bar ships hidden and JS unhides it — without
   script the composer is simply a textarea, which is the whole
   fallback story. Preview renders through the PRODUCTION pipeline. */
.f-composer .f-tabs { display: flex; gap: var(--sp-1); padding: var(--sp-2) var(--sp-2) 0;
  background: var(--surface); border-bottom: 1px solid var(--rule);
  border-radius: var(--r-2) var(--r-2) 0 0; }
.f-tab { border: 1px solid transparent; background: none; font-size: var(--fs-md);
  padding: var(--sp-1) var(--sp-3); border-radius: var(--r-row) var(--r-row) 0 0; color: var(--muted); font-weight: 600; }
.f-tab:hover { color: var(--fg); border-color: transparent; }
.f-tab.here { background: var(--bg); border-color: var(--rule); border-bottom-color: var(--bg);
  color: var(--fg); position: relative; top: 1px; }
.f-composer textarea { border: 0; border-radius: 0; }
.f-preview { padding: var(--sp-4) var(--sp-4); min-height: 9rem; }
.f-composer [hidden] { display: none; }
.f-preview > :first-child { margin-top: 0; }
/* Live split: raw markdown left, the render growing beside it as you
   type — the pane is fed by the production renderer, debounced. JS
   unhides it; without script the composer is a full-width textarea. */
.f-split { display: grid; grid-template-columns: 1fr; }
.f-split:has(.f-live:not([hidden])) { grid-template-columns: 1fr 1fr; }
.f-live { border-left: 1px solid var(--rule); padding: var(--sp-4) var(--sp-4); overflow-y: auto;
  font-size: 0.95em; }
.f-live-pane > :first-child { margin-top: 0; }
.f-live-pane:empty::before { content: "rendered preview appears as you type"; color: var(--muted); font-size: var(--fs-md); }
@media (max-width: 52rem) {
  .f-split:has(.f-live:not([hidden])) { grid-template-columns: 1fr; }
  .f-live { border-left: 0; border-top: 1px solid var(--rule); }
}
/* The poll widget as previewed (ballot-less) and the poll BUILDER —
   structured inputs in the live pane that write the fenced poll block
   back into the raw source. The markdown stays the source of truth. */
.f-pollprev, .f-poll-builder:not(:empty) { border: 1px solid var(--rule); border-radius: var(--r-row);
  padding: var(--sp-2) var(--sp-3); max-width: 30rem; margin: var(--sp-2) 0; }
.f-pollprev-o { margin: var(--sp-1) 0; }
.f-pollbar { height: 6px; background: var(--rule); border-radius: var(--r-1); }
.f-poll-builder label { display: block; font-size: var(--fs-sm); color: var(--muted); margin-top: var(--sp-2); }
.f-poll-builder input { width: 100%; margin-top: var(--sp-0); }
.f-poll-builder .pb-row { display: flex; gap: var(--sp-1); align-items: center; }
.f-poll-builder .pb-row button { padding: var(--sp-1) var(--sp-2); font-size: var(--fs-md); }
.f-poll-builder .pb-add { margin-top: var(--sp-2); font-size: var(--fs-md); }
.f-poll-builder h4 { margin: 0; font-size: var(--fs-body); }
/* The slash menu (composer.js): a popover of block snippets. */
.slash-menu { position: absolute; z-index: 6; background: var(--bg);
  border: 1px solid var(--rule); border-radius: var(--r-row); padding: var(--sp-1);
  box-shadow: 0 4px 14px rgba(0,0,0,0.25); min-width: 11rem; }
.slash-menu button { display: block; width: 100%; text-align: left; border: 0;
  background: none; padding: var(--sp-1) var(--sp-2); border-radius: var(--r-1); font-weight: normal; font-size: var(--fs-body); }
.slash-menu button:hover, .slash-menu button.sel { background: var(--surface); }
.slash-menu small { color: var(--muted); margin-left: var(--sp-2); }
/* A described check-in reference: the comment excerpt beside the hash. */
.cref { color: var(--muted); font-size: 0.9em; }
/* The forum list: rows with anatomy — title, opener excerpt, author
   mark, and on the right the participant stack, count, and the
   activity sparkline. Threads YOU have read mute; anything with
   something new in it carries full ink. */
/* The sort links and "mark all read" share a line: one is where you
   are, the other is a sweep of the whole page's state. */
.ft-bar { display: flex; align-items: baseline; justify-content: space-between;
  gap: var(--sp-4); margin: var(--sp-4) 0; }
/* A button that is honestly a button — it POSTs and changes state — but
   reads at the weight of the quiet text beside it. */
button.linkish { background: none; border: 0; padding: 0; cursor: pointer;
  color: var(--muted); font: inherit; font-size: var(--fs-sm); }
button.linkish:hover { color: var(--fg); text-decoration: underline; }
.ft-list { margin: var(--sp-2) 0 var(--sp-4); }
.ft-row { display: flex; align-items: center; gap: var(--sp-4); padding: var(--sp-2) var(--sp-1);
  border-top: 1px solid var(--rule); }
.ft-row:first-child { border-top: 0; }
/* Read rows fade UNIFORMLY. Muting only the title's link color read as
   two kinds of link — meaning must never ride on link color. The fade
   is gentle because read rows are the MAJORITY on a forum you keep up
   with: at 0.62 a page you had merely kept current with looked
   disabled. */
.ft-row.dim { opacity: 0.78; }
.ft-row.dim:hover { opacity: 1; }
.ft-main { min-width: 0; flex: 1; }
.ft-t { font-weight: 600; }
/* The title is already the obvious target — it is the biggest, boldest
   thing on the row and the whole row leads to it — so at rest it wears
   the page's ink and lets the read/unread fade be the only signal it
   carries. The link color arrives on hover, WHERE HOVER EXISTS: a
   touch device would otherwise be left with the tap-emulated hover
   stuck on the last title tapped. :active pays the affordance back to
   touch, which never gets the hover state at all. */
.ft-t a { color: inherit; text-decoration: none;
  -webkit-tap-highlight-color: transparent; }
@media (hover: hover) {
  .ft-t a:hover { color: var(--accent); text-decoration: underline; }
}
.ft-t a:active { color: var(--accent); text-decoration: underline; }
.ft-score { font-size: var(--fs-sm); color: var(--accent); margin-left: var(--sp-1); }
.ft-x { margin: var(--sp-0) 0 0; color: var(--muted); font-size: var(--fs-body);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* A sentence, not a row. As a flex container every bare text run
   between the elements — " · started ", " · last reply " — became its
   own anonymous flex item, and with the default nowrap a narrow screen
   shrank each item until its text wrapped INSIDE it. The result was the
   words stacked in columns rather than the line wrapping once. Normal
   flow wraps it the way prose wraps. */
.ft-meta { margin: var(--sp-1) 0 0; color: var(--muted); font-size: var(--fs-sm); }
/* The pieces that must not be split mid-way: a name and its mark are
   one thing, and a date is one thing. Wrapping happens BETWEEN them. */
.ft-meta .who, .ft-meta time { white-space: nowrap; }
.ft-meta .idn { width: 14px; height: 14px; }
.ft-side { flex: none; display: flex; align-items: center; gap: var(--sp-3); color: var(--muted); }
.ft-stack { display: inline-flex; }
.ft-stack .idn { width: 18px; height: 18px; border-radius: var(--r-1);
  outline: 2px solid var(--bg); margin-left: -5px; }
.ft-stack .idn:first-child { margin-left: 0; }
.ft-count { font-size: var(--fs-md); font-variant-numeric: tabular-nums; min-width: 1.4rem; text-align: right; }
.ft-spark { color: var(--accent); }
@media (max-width: 40rem) { .ft-side { display: none; } }
/* Bar actions are rounded SQUARES; the account stays a circle — a
   button is a thing you do, an avatar is who you are. Icons render at
   full size in the page's ink, not the avatar's small caption size. */
.bar-btn { flex: none; width: 2rem; height: 2rem; padding: 0;
  border-radius: var(--r-row); background: none; border: 1px solid var(--rule);
  color: var(--muted); cursor: pointer; display: inline-flex;
  align-items: center; justify-content: center; }
/* GitHub's header grammar: rest is quiet (muted ink, hairline border),
   hover is a FILL, and keyboard focus gets a real ring — the hover
   affordance and the focus affordance are different signals. */
.bar-btn:hover { background: var(--code-bg); border-color: var(--rule); color: var(--fg); }
.bar-btn:focus-visible, .avatar:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.bar-btn .ic { width: 1rem; height: 1rem; vertical-align: 0; }
a.inboxbtn { position: relative; }
/* The unread badge: a count, not a mystery dot. It informs and holds
   still — no pulse, no animation. */
#inbox-dot { display: none; }
#inbox-dot.on { display: block; position: absolute; top: -0.35rem; right: -0.35rem;
  min-width: 1rem; height: 1rem; padding: 0 var(--sp-1); box-sizing: border-box;
  border-radius: var(--r-4); background: var(--accent); color: var(--bg);
  font-size: var(--fs-xs); font-weight: 700; line-height: 1rem; text-align: center; }
/* Toasts (RFC 0011 §5). A stack in the corner, above everything, that
   dismisses ITSELF: the animation is the timer, so no script holds a
   handle on a page that may be left open for a day. The keyframes hold
   the toast still for its whole readable life and only then fade, and
   visibility: hidden at the end takes it out of the tab order as well
   as out of sight. Pointer events stay off the container so an empty
   corner never eats a click on the page beneath it. */
#toasts { position: fixed; right: 1rem; bottom: 1rem; z-index: 50;
  display: flex; flex-direction: column; gap: var(--sp-2);
  max-width: min(24rem, calc(100vw - 2rem)); pointer-events: none; }
#toasts .toast { pointer-events: auto; background: var(--bg);
  border: 1px solid var(--rule); border-left: 3px solid var(--accent);
  border-radius: var(--r-row); padding: var(--sp-2) var(--sp-3); box-shadow: 0 2px 10px rgb(0 0 0 / 0.18);
  animation: toast-life 9s forwards; }
#toasts .toast a { font-weight: 600; }
#toasts .toast small { display: block; color: var(--muted); }
#toasts .attn-reason { font-weight: 600; }
/* The banner carrier for a desktop notification when banners are off:
   it exists to hold one expression and must never be seen. */
#toasts .attn-notify { display: none; }
/* The inbox's own list of what was addressed to this account. A plain
   list (Principle 3): the reason, when, and the words underneath. */
.attn-rows { margin: var(--sp-2) 0; }
.attn-row { padding: var(--sp-2) var(--sp-4); }
.attn-row.unread .attn-reason { font-weight: 600; }
.attn-summary { color: var(--muted); }
.attn-head { font-size: var(--fs-body); margin: var(--sp-4) 0 var(--sp-1); }
@keyframes toast-life { 0%, 88% { opacity: 1; } 100% { opacity: 0; visibility: hidden; } }
/* Reduced motion still needs the dismissal — it is the toast's whole
   lifecycle, not decoration — so the fade becomes a cut. */
@media (prefers-reduced-motion: reduce) {
  #toasts .toast { animation: toast-cut 9s forwards; }
  @keyframes toast-cut { 0%, 99% { opacity: 1; } 100% { opacity: 0; visibility: hidden; } }
}
/* A link dressed as a button, matching the button rule above — for
   "new thread"-style actions that are navigations, not submissions. */
a.btn, label.btn { display: inline-block; background: var(--code-bg); color: var(--fg);
  border: 1px solid var(--rule); border-radius: var(--r-row); padding: var(--sp-1) var(--sp-2);
  font-weight: 600; text-decoration: none; }
a.btn:hover, label.btn:hover { border-color: var(--fg); }
/* A file input cannot be styled, so its LABEL is the control and the
   input behind it is what keyboard focus lands on. Without this the
   label read as a sentence beside a button rather than as the thing
   to press. */
label.btn { cursor: pointer; }
/* a.btn is element+class and outranks .btn-primary, so every primary
   ANCHOR button in the forge quietly rendered as a plain one — the New
   ticket call to action included. Matched here rather than by reordering,
   which the next rule appended below would silently undo again. */
a.btn.btn-primary { background: var(--accent); color: var(--bg);
  border-color: var(--accent); }
a.btn.btn-primary:hover { border-color: var(--accent); filter: brightness(1.1); }
.fleet-count { margin: 0 0 var(--sp-2); }
.input-group { display: inline-flex; align-items: center; min-width: 0;
  border: 1px solid var(--rule); border-radius: var(--r-row); background: var(--bg); }
.input-group:focus-within { outline: 2px solid var(--muted); outline-offset: 1px; }
.input-group > input { flex: 1 1 auto; min-width: 0; border: 0; background: none;
  border-radius: var(--r-row); }
.input-group > input:focus-visible { outline: none; }
.clearq { flex: none; color: var(--muted); text-decoration: none; font-size: var(--fs-lg);
  line-height: 1; padding: 0 var(--sp-2); }
.clearq:hover { color: var(--fg); }
/* select keeps its native disclosure arrow — replacing it costs a
   hand-drawn caret that has to be redrawn for both themes. */
.readme-head { margin-top: var(--sp-5); padding-bottom: var(--sp-1);
  border-bottom: 1px solid var(--rule); font-size: var(--fs-base); }
/* Each entry is one unit — a pill with its count inside — rather than a
   word and a loose number. Loose numbers made every repository's nav a
   different shape and, with a wide count, a different number of lines.
   "here" is a filled pill rather than a colour so it survives both
   themes and does not depend on hue perception; "empty" is dimmed but
   still a link. */
/* The nav is the ONLY thing in the bar allowed to shrink, and it
   shrinks by scrolling, never by pushing: the … button sits outside it
   and is always whole. This is the structural guarantee behind every
   width — a surprising repo name or count can at worst make the tabs
   swipeable, exactly GitHub's mobile behavior. */
.repobar nav { display: flex; flex-wrap: nowrap; gap: var(--sp-4); margin-left: auto;
  min-width: 0; flex: 0 1 auto; overflow-x: auto; scrollbar-width: none; }
.repobar nav::-webkit-scrollbar { display: none; }
.navmore-btn { flex: none; }
nav a { display: inline-flex; align-items: baseline; gap: var(--sp-1);
  padding: var(--sp-0) var(--sp-2); border-radius: var(--r-4); text-decoration: none;
  border: 1px solid transparent; }
nav a:hover { border-color: var(--rule); }
nav a.here { font-weight: 600; background: var(--code-bg); color: var(--fg);
  border-color: var(--rule); }
nav a.empty { color: var(--muted); }
/* The repobar and explore navs are underline TABS, not pills: the bar
   is a row of places, and GitHub taught everyone what that looks like.
   The underline element carries a view-transition-name, so on browsers
   with cross-document view transitions (Chrome 126+, Safari 18.2+) the
   highlight glides from the old tab to the new one across the page
   load; everywhere else navigation is simply instant. */
.repobar nav a, nav.tabs a { border-radius: 0; border: 0; padding: var(--sp-2) var(--sp-0);
  border-bottom: 2px solid transparent; background: none; color: var(--muted);
  position: relative; }
.repobar nav a:hover, nav.tabs a:hover { border-color: transparent; color: var(--fg); }
.repobar nav a.here, nav.tabs a.here { background: none; border: 0; font-weight: 600;
  color: var(--fg); border-bottom: 2px solid transparent; }
.ink { position: absolute; left: 0; right: 0; bottom: 0; height: 2px;
  background: var(--accent); border-radius: var(--r-1); }
.repobar nav a.here .ink { view-transition-name: repo-tab; }
nav.tabs a.here .ink { view-transition-name: explore-tab; }
nav.doc-tabs a.here .ink { view-transition-name: doc-tab; }
nav.fleet-tabs a.here .ink { view-transition-name: fleet-tab; }
@view-transition { navigation: auto; }
@media (prefers-reduced-motion: reduce) { @view-transition { navigation: none; } }
/* The bars are ISOLATED from the root crossfade: identical on both
   sides of a navigation, so their groups render static instead of
   flashing with the page — the fixed chrome fading in and out was most
   of what read as "weird". Everything else is kept brisk: at 0.4s a
   crossfade is an effect, at 0.15s it is a page load. */
.topbar { view-transition-name: topbar; }
.repobar { view-transition-name: repobar; }
footer { view-transition-name: sitefooter; }
::view-transition-group(*) { animation-duration: 0.15s; }
/* Only the chrome animates — the orange underline slides, the bars hold
   still. The root snapshot does NOT cross-fade: two translucent copies
   of a dark page over a dark backdrop dip in brightness mid-fade, which
   reads as a flash on every slow navigation. Content just swaps. */
::view-transition-old(root), ::view-transition-new(root) { animation: none; }
/* The collapse, GRADUATED: each tier folds at its own width and the …
   menu reveals exactly the tier the bar just hid — a landscape phone
   keeps six tabs and folds two; a portrait phone keeps three. All-at-
   once hiding read as wrong precisely there. Measured collapse needs
   script; tiers are what owning the layout costs in pure CSS. */
/* Fixed square, self-centered: sized by its own box rather than by the
   bar's baseline alignment, which squashed it once it left the nav. */
.navmore-btn { display: none; align-items: center; justify-content: center;
  width: 1.9rem; height: 1.9rem; padding: 0; align-self: center;
  border: 1px solid var(--rule); border-radius: var(--r-row);
  background: none; color: var(--muted); cursor: pointer;
  anchor-name: --navmore; }
.navmore-btn .ic, .fleetmore-btn .ic { width: 1rem; height: 1rem; }
.fleetmore-btn { display: none; align-items: center; justify-content: center;
  width: 1.9rem; height: 1.9rem; padding: 0; align-self: center; flex: none;
  border: 1px solid var(--rule); border-radius: var(--r-row);
  background: none; color: var(--muted); cursor: pointer;
  anchor-name: --fleetmore; }
.menu.fleetmore .f1, .menu.fleetmore .f2 { display: none; }
/* Mirrors goes first, then Operations. Stores is the way home and
   Exceptions carries the count an operator is here for, so neither
   folds. An active tab never folds: it stays lit where it is. */
@media (max-width: 44rem) {
  .repobar nav.fleet-tabs a.f1:not(.here) { display: none; }
  .fleetmore-btn { display: inline-flex; }
  .menu.fleetmore .f1 { display: block; }
}
@media (max-width: 34rem) {
  .repobar nav.fleet-tabs a.f2:not(.here) { display: none; }
  .menu.fleetmore .f2 { display: block; }
}
.menu.navmore .m1, .menu.navmore .m2, .menu.navmore .m3, .menu.navmore .my { display: none; }
@media (max-width: 62rem) {
  .repobar nav a.aux1:not(.here) { display: none; }
  .navmore-btn { display: inline-flex; }
  .menu.navmore .m1 { display: block; }
  /* An active folded tab stays out of the drawer — selected, visible —
     and wiki (the last tier standing) yields its place for it. */
  .repobar nav:has(a.aux1.here, a.aux2.here) a.aux3:not(.here) { display: none; }
  .repobar nav:has(a.aux3.here) a.aux2:not(.here) { display: none; }
}
@media (max-width: 52rem) {
  .repobar nav a.aux2:not(.here) { display: none; }
  .menu.navmore .m2 { display: block; }
}
@media (max-width: 44rem) {
  .repobar nav a.aux3:not(.here) { display: none; }
  .menu.navmore .m3 { display: block; }
  /* A surfaced drawer tab needs the room a core tab was using: tickets
     yields. The row must NEVER exceed the viewport — an overflowing
     bar drags the whole page sideways — and the clip below is the hard
     guarantee behind these yields. */
  .repobar nav:has(a.aux1.here, a.aux2.here, a.aux3.here) a.yield:not(.here) { display: none; }
  /* ...and whatever yields is then reachable from the drawer, under
     exactly the condition that hid it. The drawer is outside the nav,
     so the test is on the page. */
  body:has(.repobar nav a.aux1.here, .repobar nav a.aux2.here, .repobar nav a.aux3.here) .menu.navmore .my { display: block; }
}
/* The drawer never lists the tab that is currently OUT of it. */
.menu.navmore span:has(a.here) { display: none !important; }
.menu.navmore a { display: flex; border-radius: var(--r-row); padding: var(--sp-1) var(--sp-2); }
.menu.navmore a.here { background: var(--surface); border: 0; font-weight: 600; }
@supports (anchor-name: --a) {
  .menu.navmore { position-anchor: --navmore; inset: auto; top: anchor(bottom);
    right: anchor(right); margin-top: var(--sp-1); }
  /* The fleet drawer wears .navmore for its looks but hangs off its OWN
     button: --navmore is not on the page at all here, and an anchor
     that resolves to nothing drops the menu in the viewport's top-left
     corner, over the search field. */
  .menu.fleetmore { position-anchor: --fleetmore; }
}
nav .n { color: var(--muted); font-size: var(--fs-xs); font-variant-numeric: tabular-nums;
  background: var(--code-bg); border-radius: var(--r-4); padding: 0 var(--sp-1); }
nav a.here .n { background: var(--bg); }
.repo-sub { color: var(--muted); font-size: var(--fs-body); margin: -0.6rem 0 var(--sp-4); }
.newrepo { margin: 0 0 var(--sp-3); }
/* The new-repository form had no styling of its own at all. Two of its
   inputs carry size="52" — about 460px — and a bare <fieldset> takes
   the UA's min-inline-size: min-content, so the Contents box REFUSED
   to shrink below them: measured at a 390px viewport it laid out 491px
   wide, and because nothing scrolls (main clips), the right-hand end
   of every line in it was simply gone. The page nobody reaches until
   they are making their first repository was the one page never given
   a phone pass.

   size="52" stays. It is the right width with no stylesheet at all,
   and the cap below is what makes it a preference rather than a floor. */
.newrepo-form p { margin: var(--sp-3) 0; }
.newrepo-form fieldset { min-inline-size: 0; box-sizing: border-box;
  border: 1px solid var(--rule); border-radius: var(--r-2);
  margin: var(--sp-4) 0; padding: var(--sp-1) var(--sp-4) var(--sp-3); }
.newrepo-form legend { padding: 0 var(--sp-1); color: var(--muted);
  font-size: var(--fs-sm); letter-spacing: 0.05em; text-transform: uppercase; }
/* Every free-text field fills its column: name, description, and the
   upstream URL, which is the longest thing anybody types here. */
.newrepo-form input:not([type]), .newrepo-form input[type=text],
.newrepo-form input[type=url] { width: 100%; max-width: 100%;
  box-sizing: border-box; }
.newrepo-form input[type=number] { width: 8ch; }
/* A row of independent choices, not a sentence: it wraps rather than
   running off the side. */
.newrepo-form .seed { display: flex; flex-wrap: wrap; align-items: center;
  gap: var(--sp-2) var(--sp-4); }
.newrepo-form .seed label { display: inline-flex; align-items: center; gap: var(--sp-1); }
/* The clone row is a URL and an interval, and at phone width they
   stack instead of squeezing each other. */
.newrepo-form .clone-row { display: flex; flex-wrap: wrap;
  align-items: center; gap: var(--sp-2) var(--sp-4); }
.newrepo-form .clone-row > input { flex: 1 1 16rem; min-width: 0; }
.newrepo-form .clone-row > label { display: inline-flex; align-items: center; gap: var(--sp-1); }
.about { margin: -0.3rem 0 var(--sp-3); color: var(--fg); }
/* Red and green are the same two tokens the diff uses. They were
   literals with the dark pair behind an unguarded media query, so an
   explicit light choice on a dark-mode device drew the dark red on the
   light ground — the same defect the highlight had. */
.danger-head { color: var(--del); }
.danger { border: 1px solid var(--del); border-radius: var(--r-1); padding: var(--sp-1) var(--sp-4); }
.danger form + form { border-top: 1px solid var(--rule); margin-top: var(--sp-2); }
/* The listing's numeric and date columns take only what they need, so
   the description and tip get the width instead of "Check-ins" wrapping
   into two lines beside a mostly-empty row. */
table.fleet th, table.fleet td { vertical-align: top; }
/* The name column takes the slack; the numeric ones take only what
   they need and never wrap. Right-aligned tabular figures line up down
   the page by construction — which a per-row grid could not do. */
table.fleet td.r-name { width: 100%; }
/* The repository's own mark, the size a name is — every other named
   thing on the forge carries one, and the listing of repositories was
   the place that did not. Baseline-aligned rather than centred: it
   sits beside a link in running text, not in a cell of its own. */
.r-mark { display: inline-block; width: 1.15em; height: 1.15em;
  vertical-align: -0.22em; margin-right: var(--sp-2); }
.r-mark .idn { width: 100%; height: 100%; display: block; border-radius: var(--r-1); }
/* On a bar the mark sits against a name set larger than a listing's,
   so it takes the name's size rather than the listing's. */
.bar-mark { width: 1.05em; height: 1.05em; vertical-align: -0.16em; margin-right: var(--sp-2); }
/* The image editor: the mark at the size it is being judged at, with
   its controls beside it rather than under it. The file input is
   hidden and reached through its label, because a bare file input
   cannot be styled and reads as a different kind of control from
   every other button on the page — the label IS the control, and
   keyboard focus lands on the input behind it. */
.av-edit { display: flex; flex-wrap: wrap; gap: var(--sp-4); align-items: flex-start; margin: var(--sp-2) 0 var(--sp-4); }
.av-acts { flex: 1 1 18rem; min-width: 0; display: flex; flex-direction: column; gap: var(--sp-2); }
.av-form { display: flex; flex-wrap: wrap; gap: var(--sp-2); align-items: center; }
.av-form input[type=file] { position: absolute; width: 1px; height: 1px; opacity: 0; }
.av-form input[type=file]:focus-visible + button,
.av-form label.btn:hover { border-color: var(--accent); }
.avatar.lg .idn-img { width: 100%; height: 100%; object-fit: cover; }
/* A built-in mark is line art, not a generated block: it takes the
   text colour and a little room, where an identicon fills its box. */
/* The mark's inset is in its viewBox, not in padding. Percentage
   padding resolves against the CONTAINING BLOCK's width, not the
   element's own: the same 12% was about 2px inside a narrow table cell
   and about 43px once that cell became a full-width block at phone
   width, which swamped the 14px box and squeezed the glyph away. A
   viewBox margin is the element's own geometry and cannot be moved by
   whatever it is placed in. */
.idn-mark { color: var(--fg); }
/* A drop target while a file is over it, the same dashed rule
   attach.js's targets use — one vocabulary for "let go here". */
.av-edit[data-over] { outline: 2px dashed var(--accent); outline-offset: 4px; border-radius: var(--r-2); }
/* An unsaved preview says so: the mark is showing something the forge
   is not serving yet, and nothing else on the page would tell you.
   The warning REPLACES the limits in the line that is already there,
   rather than adding one: a line that appears pushes the buttons down
   under the pointer about to press them, and a line reserved for it
   is a permanent blank gap paid for a state that is usually absent. */
.av-edit[data-preview] .avatar.lg { outline: 2px solid var(--accent); outline-offset: 2px; }
.av-unsaved { display: none; color: var(--accent); }
/* The phone's paste target. A long-press menu only offers Paste over
   something editable, so this is the thing to press on — it is not a
   place to type, and anything that is not an image is put straight
   back out. Shipped hidden: without script it would be a box that
   does nothing. */
.av-paste { min-height: 2.75rem; display: flex; align-items: center;
  padding: 0 var(--sp-3); border: 1px dashed var(--rule); border-radius: var(--r-row);
  color: var(--muted); font-size: var(--fs-md); cursor: text; }
.av-paste:empty::before { content: "Paste an image here"; }
.av-paste:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.av-paste img { max-height: 2rem; }
.av-edit[data-preview] .av-limits { display: none; }
.av-edit[data-preview] .av-unsaved { display: inline; }
table.fleet th.n, table.fleet td.n { text-align: right; white-space: nowrap;
  width: 1%; padding-left: var(--sp-4); font-variant-numeric: tabular-nums;
  color: var(--muted); }
table.fleet td.n.last { color: var(--fg); }
table.fleet th.n { padding-bottom: var(--sp-1); }
/* The pair reads as one entry: no rule between a name and its
   description, the rule goes under the description instead. */
table.fleet tr.r-top td { padding-top: var(--sp-2); border-top: 1px solid var(--code-bg); }
table.fleet tr.r-desc td { padding: 0 0 var(--sp-0); color: var(--muted);
  font-size: var(--fs-body); border: 0; }
table.fleet tr.r-desc + tr.r-top td { border-top: 1px solid var(--code-bg); }
@media (max-width: 30rem) {
  /* A phone has room for the name and three numbers, not four. Mirror
     freshness is the most specialised of them and the first to go — the
     repository page still reports it. Tighter gutters, and a long name
     may wrap rather than push the table into a sideways scroll. */
  table.fleet th.n, table.fleet td.n { padding-left: var(--sp-2); }
  table.fleet td.r-name { white-space: normal; overflow-wrap: anywhere; }
  table.fleet th.n:nth-child(4), table.fleet td.n:nth-child(4) { display: none; }
}
.ci-pass { border-color: var(--add); color: var(--add); }
.ci-fail { border-color: var(--del); color: var(--del); }
/* Running is the only state that changes without the reader acting, so
   it is the only badge that moves. Grey and static, it read exactly
   like the required chip beside it and nothing said "wait". */
.ci-run { border-color: var(--accent); color: var(--accent); }
.ci-run::before { content: ""; display: inline-block; width: 0.65em; height: 0.65em;
  margin-right: 0.35em; vertical-align: -0.05em; border: 2px solid currentColor;
  border-right-color: transparent; border-radius: 50%; animation: ci-spin 0.9s linear infinite; }
@keyframes ci-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
  .ci-run::before { animation: none; border-right-color: currentColor; opacity: 0.5; }
}
/* Ticket fields are label/value pairs, so a dl says what they are and
   escapes the global table rules that banded every label with a border.
   The label column sizes to its own widest label instead of a fixed
   10rem: fossil ticket schemas are arbitrary, so the widest label is
   whatever this repository's tracker defines. Values may be 40-char
   hashes, which must wrap rather than push the page sideways. */
dl.tkt-head {
  display: grid; grid-template-columns: minmax(0, max-content) minmax(0, 1fr);
  gap: var(--sp-1) var(--sp-4); margin: var(--sp-3) 0;
}
dl.tkt-head dt { color: var(--muted); font-size: var(--fs-body); }
dl.tkt-head dd { margin: 0; min-width: 0; overflow-wrap: anywhere; }
.tkt-id { overflow-wrap: anywhere; }
.tkt-body { margin: var(--sp-2) 0 var(--sp-4); }
.tkt-comment { border-top: 1px solid var(--rule); padding: var(--sp-2) 0; }
.tkt-comment:first-of-type { border-top: 0; }
/* An import in progress: the same row shape as a store, so the eye
   reads one list, with a state badge that says it is still arriving. */
.imports { margin-bottom: var(--sp-5); }
.imports h2 { font-size: var(--fs-base); margin: 0 0 var(--sp-2); }
.tl-badge.imp-run { border-color: var(--accent); color: var(--accent); }
/* The ticket editor: body and timeline left, metadata right; one
   column on a phone with the metadata after the body. */
/* Editing a ticket in place. The control lives ON the thing it edits
   and TAKES ITS PLACE when pressed — the old fold sat underneath saying
   "edit description", which names a thing rather than offering a way to
   do it. A hidden checkbox drives it, so the field, Cancel and Save
   share one row and cancelling costs no round trip; <details> cannot,
   because the browser slots its content into an anonymous box that will
   not sit in a row beside the summary. */
.t-head { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2); min-width: 0; }
.t-title { margin: 0; min-width: 0; }
/* Quiet until wanted, and never hidden from the keyboard. */
.t-pen { cursor: pointer; color: var(--muted); opacity: 0.5; border-radius: var(--r-1);
  padding: var(--sp-1) var(--sp-1); display: inline-flex; align-items: center; }
.t-head:hover .t-pen, .t-desc:hover .t-pen, .t-toggle:focus-visible ~ .t-title .t-pen,
.t-pen:hover { opacity: 1; color: var(--fg); background: var(--surface); }
.t-toggle:focus-visible ~ .t-title .t-pen { outline: 2px solid var(--accent); outline-offset: 1px; }
/* The title's pencil rides the heading's last line, after its last
   word, rather than taking a line of its own under a long title. */
.t-title .t-pen { vertical-align: middle; margin-left: var(--sp-1); font-size: var(--fs-base); font-weight: 400; }
/* Closed: only the heading and its pencil. */
.t-title-form, .t-save, .t-cancel, .t-desc-form { display: none; }
/* Open: the heading gives way to the field, and the row reads field,
   Cancel, Save — the order a hand uses. */
.t-toggle:checked ~ .t-title { display: none; }
/* A text area, grown to the title, so a long one is read and edited
   whole instead of scrolled through one line; Cancel and Save follow
   on their own line. */
.t-toggle:checked ~ .t-title-form { display: block; flex: 1 1 100%; min-width: 0; }
.t-toggle:checked ~ .t-title-form textarea { width: 100%; font-size: var(--fs-lg); font-weight: 600;
  padding: var(--sp-2); resize: vertical; field-sizing: content; min-height: 2lh; }
.t-toggle:checked ~ .t-cancel, .t-toggle:checked ~ .t-save { display: inline-block; }
.t-cancel { cursor: pointer; border: 1px solid var(--rule); border-radius: var(--r-1);
  padding: var(--sp-2) var(--sp-3); white-space: nowrap; }
.t-cancel:hover { background: var(--surface); }
/* The description's pencil sits in the bottom-right corner, in a strip
   the body reserves for it, so it never covers a line however the text
   falls; the body gives way to the composer. The strip is the button's
   icon plus its padding, and a gap. */
.t-desc { position: relative; }
.t-pen-desc { position: absolute; bottom: 0; right: 0; z-index: 1; }
.tkt-body-pen { padding-bottom: calc(1em + var(--sp-1) * 2 + var(--sp-1)); }
.t-toggle:checked ~ .tkt-body { display: none; }
.t-toggle:checked ~ .t-desc-form { display: block; }
.t-desc-form .f-reply-foot { display: flex; gap: var(--sp-2); justify-content: flex-end;
  align-items: center; }
.t-nodesc { font-style: italic; }
/* "Your recent work" in the dashboard sidebar. The heading needs air
   above it: it follows the repositories' own trailing link, and without
   this the two read as one block. */
/* .dash h2 (0,1,1) outranked this, so the section sat flush under the
   New repository button. */
/* The empty state is a NOTE, not the page's content — at body size it
   shouted louder than the work it was standing in for. */
.dash-mine-none { font-size: var(--fs-md); color: var(--muted); max-width: 26rem; }
.tkt-layout { display: grid; grid-template-columns: minmax(0, 1fr) 17rem; gap: 0 var(--sp-6); align-items: start; }
.tkt-side { font-size: var(--fs-body); }
.tkt-fields { display: grid; gap: var(--sp-3); margin: 0 0 var(--sp-4); }
.tkt-fields label { display: grid; gap: var(--sp-1); }
.tkt-fields select, .tkt-fields input { width: 100%; box-sizing: border-box; font: inherit; }
.tkt-k { color: var(--muted); font-size: var(--fs-sm); text-transform: capitalize; }
.tkt-v { padding: var(--sp-1) 0; }
.tkt-meta { color: var(--muted); font-size: var(--fs-sm); border-top: 1px solid var(--rule); padding-top: var(--sp-2); }
.tkt-event { border-top: 1px solid var(--rule); padding: var(--sp-2) 0; color: var(--muted); font-size: var(--fs-body); }
.tkt-event b { color: var(--fg); font-weight: 600; }
.tkt-remark .f-reply-foot { align-items: center; }
.tkt-edit { margin: 0 0 var(--sp-4); }
.tkt-edit > summary { color: var(--muted); font-size: var(--fs-md); cursor: pointer; width: max-content; }
.tkt-edit[open] > summary { margin-bottom: var(--sp-2); }
.tkt-retitle { margin-top: -0.5rem; }
.tkt-retitle form { display: flex; gap: var(--sp-2); }
.tkt-retitle input { flex: 1; font: inherit; }
.tkt-close { display: inline-flex; align-items: center; gap: var(--sp-1); color: var(--muted); font-size: var(--fs-md); }
.tkt-close select { font: inherit; }
@media (max-width: 52rem) {
  .tkt-layout { grid-template-columns: minmax(0, 1fr); }
  .tkt-side { border-top: 1px solid var(--rule); padding-top: var(--sp-3); margin-top: var(--sp-4); }
}
.tl-badge { border: 1px solid var(--rule); border-radius: var(--r-1); padding: 0 var(--sp-1); font-size: var(--fs-xs); }
.tl-pill { display: inline-block; border: 1px solid var(--rule); border-radius: var(--r-row);
  padding: var(--sp-0) var(--sp-2); font-weight: bold; margin: var(--sp-2) 0 var(--sp-1); }
/* Filter controls wrap as WHOLE units: a label never separates from
   its select, a branch chip never breaks mid-name. The chip is
   GitHub's dismissible filter token — the name truncates before the ×
   detaches. */
/* GRID, not flex: one line is structural. fit-content is GitHub's
   branch-selector rule in one function — sized to its label, capped
   (12rem), and clamped to the space the other two leave, so the row
   cannot overflow whatever the branch is called and a short name gets
   a short control. */
.tl-controls { color: var(--muted); font-size: var(--fs-body); display: grid;
  grid-template-columns: fit-content(12rem) max-content max-content max-content;
  gap: var(--sp-2); align-items: center; margin-bottom: var(--sp-2); max-width: 100%; }
.tl-controls select { min-width: 0; max-width: 100%; }
/* The feed link sits in the filter row because it follows whatever the
   filters say; a visitor without an account reads it as the "follow". */
.tl-feed { display: inline-flex; align-items: center; gap: var(--sp-1); white-space: nowrap; }
/* The fleet bar's first grid cell is a FORM holding field and sort;
   left as a block it stacks them. Flex puts them on the one line the
   grid promised, with the New repository link beside them. */
.fleet-bar { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2) var(--sp-3); }
.fleet-bar > form { display: flex; align-items: center; gap: var(--sp-2); min-width: 0; }
.tl-chip { display: inline-flex; align-items: center; gap: var(--sp-1); min-width: 0;
  max-width: 100%; border: 1px solid var(--rule); border-radius: var(--r-4);
  padding: var(--sp-0) var(--sp-2); background: var(--surface); white-space: nowrap; }
.tl-chip b { overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  max-width: min(14rem, 55vw); }
.tl-chip a { text-decoration: none; }
.repobar nav a.cog { display: inline-flex; align-self: center; padding: var(--sp-2) var(--sp-0); }
/* Group labels are help text, shown where there is room for help. */
.tl-help { color: var(--muted); font-size: var(--fs-sm); }
@media (max-width: 40rem) { .tl-help { display: none; } }
pre { overflow-x: auto; background: var(--code-bg); padding: var(--sp-2); border-radius: var(--r-1); }
/* A plaintext DOCUMENT (a license, an old-school README) is prose that
   happens to be monospace — it reads straight off the page, not out of
   a code box. Fenced code inside markdown keeps the box. */
pre.plaindoc { background: none; padding: 0; border-radius: 0; }
pre.diff .add { color: var(--add); } pre.diff .del { color: var(--del); }
pre.diff .hunk { color: var(--muted); } pre.diff .head { font-weight: bold; }
/* Content can contain tokens no viewport fits — a hash, a URL, a wiki
   page literally named "checkin/2cbbabdf5ef624d809fbb40d2d312a29".
   Unbroken they push the whole page sideways, so a phone scrolls to
   read anything. break-word rather than anywhere: it breaks only when a
   word genuinely does not fit, leaving ordinary prose alone. <pre>
   keeps its own horizontal scroll, which is correct for code. */
main { overflow-wrap: break-word; }
/* A date is one token to a reader: breaking "2020-12-19T13:20:19" across
   two lines makes it unreadable and is what content-driven wrapping does
   to it by default. */
time { white-space: nowrap; }

/* A ticket list is read title-first. A table gave the 10-character id
   its own column and left the title the narrowest one on a phone; this
   gives the title the full width and demotes id/status/date to a meta
   line under it. Same shape at every width — no breakpoint to diverge. */
/* Columns that scroll independently: a board's whole value is seeing
   the states side by side, so the page must not grow to the tallest
   column and make the reader scroll past one to reach another. On a
   phone the columns stack, because four 80px columns are not a board. */
/* The board never widens the page. Open columns share the row with a
   16rem floor and wrap past it — a second row beats a sideways scroll
   that hides the columns that matter. Empty columns fold to strips
   (GitHub's collapsed column), which is what keeps a seven-status
   vocabulary on one row in the common case. */
/* The board is a canvas, not prose: it is the one view that leaves the
   reading column and takes the window. The controls above it keep the
   column so they line up with every other page. A column has ONE width
   (GitHub's 350px, here 22rem) so a card is the same object wherever
   it sits; the board is what varies — columns fold, and past the
   window they wrap. The controls leave the column with it, so the
   board's left edge and its filters line up (GitHub does the same). */
/* Width follows the TOGGLE, not a class on main.
   
   It has to: main sits outside #tkt-view, so a morph — which patches
   only that region — can never change a class on it. Keying the rule
   to the button's own pressed state means the width is correct in both
   worlds from one declaration: a full page load renders the toggle
   pressed and :has widens the page, and a morph swaps the toggle and
   :has re-evaluates. No signal, no second patch, nothing to keep in
   step.

   This used to fire automatically on the board view, so choosing
   "board" silently re-laid-out the whole page under the click. */
main:has(.t-wide a[aria-pressed="true"]) { max-width: none; }
/* A toggle, and it looks like one: pressed carries the selected fill
   and full-strength ink, unpressed is muted. Same geometry either way,
   so pressing it moves nothing but itself. */
.t-wide a { display: inline-flex; align-items: center; padding: var(--sp-2) var(--sp-2); color: var(--muted); }
.t-wide a[aria-pressed="true"] { color: var(--fg); }
.t-wide .ic { width: 1rem; height: 1rem; }
.board { display: flex; flex-wrap: wrap; gap: var(--sp-3); align-items: stretch; padding-bottom: var(--sp-2); }
.board-col { flex: 0 0 22rem; min-width: 0; border: 1px solid var(--rule); border-radius: var(--r-2);
  background: var(--surface); padding: var(--sp-2); }
.board-col.folded { flex: 0 0 auto; padding: var(--sp-2) var(--sp-1); display: flex; align-items: flex-start;
  justify-content: center; }
.board-col.folded h3 { margin: 0; writing-mode: vertical-rl; transform: rotate(180deg);
  display: flex; align-items: center; gap: var(--sp-2); }
.board-col.folded .unfold { text-decoration: none; color: inherit; display: inline-flex;
  align-items: center; gap: var(--sp-2); }
/* Rotated, a pill becomes an oval: the strip names the column in
   plain text, the pill's colour kept, its box dropped. */
.board-col.folded .t-status { border: 0; background: none; padding: 0; border-radius: 0;
  font-weight: 600; letter-spacing: 0.02em; }
.board-col.folded .unfold:hover .t-status { text-decoration: underline; }
/* The chevron is the one control a column has: fold on an open one,
   open on a strip. Same glyph family, same spot, both directions. */
.board-col h3 .fold { margin-left: auto; color: var(--muted); text-decoration: none; padding: 0 var(--sp-1);
  border-radius: var(--r-1); font-size: var(--fs-lg); line-height: 1; }
.board-col h3 .fold:hover { color: var(--fg); background: var(--bg); }
.board-col.folded .chev { color: var(--muted); font-size: var(--fs-lg); }
/* Sticky: in a 38-card column the header is the answer to "which
   column am I in" once the top has scrolled away. */
.board-col h3 { position: sticky; top: 0; z-index: 1; background: var(--surface);
  margin: 0 0 var(--sp-2); padding: var(--sp-0) 0; font-size: var(--fs-body); display: flex; align-items: center;
  gap: var(--sp-2); }
.board-col h3 .n { color: var(--muted); font-size: var(--fs-sm); font-variant-numeric: tabular-nums; }
/* A lane is a horizontal cut across the same columns. The heading is
   sticky because the whole point of a lane is knowing which one you are
   reading while you scroll down a long one. */
.lane-h { position: sticky; top: 0; z-index: 1; margin: var(--sp-4) 0 var(--sp-2);
  font-size: var(--fs-md); text-transform: uppercase; letter-spacing: 0.06em;
  color: var(--muted); background: var(--bg); padding: var(--sp-1) 0;
  display: flex; align-items: center; gap: var(--sp-2); }
.lane-h .lane-name { color: var(--fg); }
/* The filing form (ticketNewPage). Type pills, a large title, the
   composer, a row of chip-selects, then the footer; a side column for
   the keyboard and the API that folds under the form on a narrow
   screen. Chips are native selects dressed as pills: the unset state
   is read off the selected option, so it needs no script. */
.tn-wrap { display: grid; grid-template-columns: minmax(0, 1fr) 17rem; gap: var(--sp-6); align-items: start; }
@media (max-width: 60rem) { .tn-wrap { grid-template-columns: minmax(0, 1fr); gap: var(--sp-5); } }
.tn { display: flex; flex-direction: column; gap: var(--sp-4); min-width: 0; }
.tn-types { border: 0; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: var(--sp-2); }
.tn-type { position: relative; display: inline-flex; align-items: center; min-height: 2.25rem; padding: 0 var(--sp-4);
  border: 1px solid var(--rule); border-radius: var(--r-4); color: var(--muted); font-size: var(--fs-body); cursor: pointer; }
.tn-type input { position: absolute; inset: 0; opacity: 0; margin: 0; cursor: pointer; }
.tn-type:has(input:checked) { background: var(--accent); border-color: var(--accent); color: var(--bg); font-weight: 600; }
.tn-type:has(input:focus-visible) { outline: 2px solid var(--accent); outline-offset: 2px; }
.tn-title { width: 100%; box-sizing: border-box; font-size: var(--fs-xl); font-weight: 600; line-height: 1.25;
  padding: var(--sp-1) var(--sp-0); background: transparent; border: 0; border-bottom: 1px solid var(--rule); border-radius: 0; color: var(--fg); }
.tn-title:focus, .tn-title:focus-visible { outline: none; border-bottom-color: var(--accent); box-shadow: 0 1px 0 var(--accent); }
@media (max-width: 40rem) { .tn-title { font-size: var(--fs-lg); } }
.tn-similar:empty { display: none; }
.tn-similar { border: 1px solid var(--rule); border-radius: var(--r-2); background: var(--surface); padding: var(--sp-3) var(--sp-4); font-size: var(--fs-body); }
.tn-sim-head { margin: 0 0 var(--sp-2); color: var(--muted); font-size: var(--fs-md); }
.tn-similar ul { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--sp-1); }
.tn-similar a { display: flex; align-items: baseline; gap: var(--sp-2); text-decoration: none; color: var(--fg); }
.tn-similar a:hover .tn-sim-t { text-decoration: underline; }
.tn-sim-st { flex: none; font-size: var(--fs-xs); font-weight: 600; padding: var(--sp-0) var(--sp-2); border-radius: var(--r-4); border: 1px solid var(--rule); color: var(--muted); }
.tn-sim-t { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tn-similar code { flex: none; color: var(--muted); font-size: var(--fs-xs); }
.tn-body .f-composer { margin: 0; border: 1px solid var(--rule); border-radius: var(--r-3); overflow: hidden; }
.tn-body .f-composer:focus-within { border-color: var(--accent); }
.tn-body textarea { width: 100%; box-sizing: border-box; min-height: 14rem; padding: var(--sp-4) var(--sp-4); background: transparent; }
.tn-body .attach-composer { padding: var(--sp-2) var(--sp-4); border-top: 1px dashed var(--rule); }
.tn-chips { display: flex; flex-wrap: wrap; gap: var(--sp-2); align-items: flex-start; }
.chip-cell { display: inline-flex; flex-direction: column; gap: var(--sp-1); }
/* The form's chips are CONTROLS, so their rules live under .tn-chips:
   unscoped, the 2.15rem touch height here inflated every static .chip
   on the site (the ticket list's type tags stood 36px tall). */
.tn-chips .chip { position: relative; display: inline-flex; align-items: center; gap: var(--sp-2); min-height: 2.15rem;
  padding: 0 var(--sp-3); border: 1px solid var(--rule); border-radius: var(--r-4); background: var(--surface);
  font-size: var(--fs-md); color: var(--fg); cursor: pointer; }
.chip-k { color: var(--muted); }
.tn-chips .chip select, .tn-chips .chip input { appearance: none; -webkit-appearance: none; background: transparent; border: 0;
  padding: 0; margin: 0; color: inherit; font: inherit; min-width: 0; }
.tn-chips .chip select { cursor: pointer; field-sizing: content; }
.tn-chips .chip select:focus, .tn-chips .chip input:focus { outline: none; }
.tn-chips .chip:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 25%, transparent); }
/* Unset: a dashed ghost that says what it would set. The label is
   inside the empty option ("+ Priority"), so the key is dropped. */
.tn-chips .chip:has(select option[value=""]:checked) { border-style: dashed; background: transparent; color: var(--muted); }
.tn-chips .chip:has(select option[value=""]:checked) .chip-k { display: none; }
.tn-chips .chip[data-field="status"] .chip-k { display: none; }
.chip-text input { width: 9rem; }
.chip-text input::placeholder { color: var(--muted); }
.chip-add { list-style: none; border-style: dashed; background: transparent; color: var(--muted); }
.chip-add::-webkit-details-marker { display: none; }
.chip-cell .field-new { font-size: var(--fs-md); width: 11rem; }
.tn-more { display: contents; }
.tn-more[open] > .fieldrow { flex-basis: 100%; margin: var(--sp-2) 0 0; }
.tn-context { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2) var(--sp-2); font-size: var(--fs-md); color: var(--muted); }
.tn-ctx { padding: var(--sp-0) var(--sp-2); border-radius: var(--r-row); background: var(--code-bg); color: var(--fg); overflow-wrap: anywhere; }
.tn-context:has(input[name=ctx]:not(:checked)) .tn-ctx { text-decoration: line-through; opacity: 0.6; }
.tn-foot { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-3) var(--sp-4); padding-top: var(--sp-4); border-top: 1px solid var(--rule); }
.tn-dest { display: inline-flex; align-items: center; gap: var(--sp-2); font-size: var(--fs-md); color: var(--muted); }
.tn-another { margin-left: auto; display: inline-flex; align-items: center; gap: var(--sp-2); font-size: var(--fs-md); color: var(--muted); }
.tn-submit { min-height: 2.75rem; padding: 0 var(--sp-4); border-radius: var(--r-2); font-weight: 700; font-size: var(--fs-base); }
.tn-submit kbd { margin-left: var(--sp-2); font: inherit; font-size: var(--fs-xs); font-weight: 600; opacity: 0.75; }
.tn-aside { display: flex; flex-direction: column; gap: var(--sp-3); font-size: var(--fs-md); color: var(--muted); padding-top: var(--sp-7); }
@media (max-width: 60rem) { .tn-aside { padding-top: 0; } }
.tn-aside h2 { margin: var(--sp-2) 0 0; font-size: var(--fs-xs); font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase; color: var(--muted); }
.tn-aside p { margin: 0; line-height: 1.55; }
.tn-keys { display: grid; grid-template-columns: 5.2rem minmax(0, 1fr); gap: var(--sp-2) var(--sp-2); margin: 0; }
.tn-keys dt kbd { font: inherit; color: var(--fg); white-space: nowrap; }
.tn-keys dd { margin: 0; }
.tn-curl { margin: 0; padding: var(--sp-2) var(--sp-3); border-radius: var(--r-2); background: var(--code-bg); color: var(--fg);
  font-size: var(--fs-xs); line-height: 1.5; white-space: pre-wrap; overflow-wrap: anywhere; }
.fieldrow { display: grid; gap: var(--sp-2) var(--sp-4); margin: 0 0 var(--sp-4);
  grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr)); }
.fieldrow label { font-size: var(--fs-body); }
.fieldrow input, .fieldrow select { width: 100%; box-sizing: border-box; }
/* Select and its "name a new one" box are ONE cell, stacked, so
   revealing the box grows this field and moves no other. */
.field-pick { display: flex; flex-direction: column; gap: var(--sp-1); min-width: 0; }
.field-pick label { min-width: 0; }
.field-new { font-size: var(--fs-body); }
/* A title is a sentence and often carries a URL. It was the same width
   as a one-word triage field. */
.title-in { width: 100%; box-sizing: border-box; font-size: var(--fs-lg);
  padding: var(--sp-2) var(--sp-2); }
.board-axes { display: flex; gap: var(--sp-4); align-items: center; flex-wrap: wrap;
  margin: 0 0 var(--sp-3); font-size: var(--fs-md); color: var(--muted); }
.board-trunc { color: var(--muted); font-size: var(--fs-sm); }
.board-axes select { font-size: var(--fs-md); padding: var(--sp-0) var(--sp-1); }
/* Drag: the grip and the cursor say a card moves; the card dims while
   it is in the air; the column under the pointer lights up, which is
   the answer to "where will this land". */
.board .card[draggable="true"] { cursor: grab; }
.board .card.held { opacity: 0.5; cursor: grabbing; }
.board .card .grip { float: right; color: var(--muted); font-size: var(--fs-base); line-height: 1; margin-left: var(--sp-2);
  user-select: none; }
.board-col.over { outline: 2px dashed var(--accent); outline-offset: -2px; }
/* The last move, with its undo, sits above the columns. */
.board-moved { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2);
  border: 1px solid var(--rule); border-left: 3px solid var(--accent); border-radius: var(--r-row);
  padding: var(--sp-2) var(--sp-2); margin: 0 0 var(--sp-3); font-size: var(--fs-body); }
.board-moved form { display: contents; }
.board-moved button { font-size: var(--fs-sm); padding: var(--sp-0) var(--sp-2); }
/* The Move menu: one button, a popover of the other columns. */
.board .card .t-meta { display: flex; align-items: center; gap: var(--sp-1); flex-wrap: wrap; }
.board .card .t-id { font-family: var(--font-mono); font-size: var(--fs-sm); color: var(--muted); }
.board .card .mv-btn { margin-left: auto; font-size: var(--fs-xs); padding: var(--sp-0) var(--sp-2); font-weight: 600; }
.board .card .mv-btn .caret { font-size: 0.7em; }
@supports (anchor-name: --a) {
  .menu.mv { top: anchor(bottom); right: anchor(right); margin-top: var(--sp-1); min-width: 9rem; }
}
/* Triage badges. Same weight as the metadata line — they are for
   scanning a column, not for shouting. */
.t-badges { display: flex; flex-wrap: wrap; gap: var(--sp-1); margin-top: var(--sp-1); }
/* The landing page's About block and document tabs. The aside reads
   as a sidebar where there is room and as a header card where there is
   not; the tab strip is the scopes idiom, already legible everywhere. */
/* minmax(0, 1fr), never a bare 1fr. A 1fr track carries min-width:auto,
   so it cannot shrink below its content's min-content width — and the
   feed contains a segmented control that must not wrap. The track grew
   to fit it, the document went wider than the phone, and every heading
   and the pager ran off the right edge. Capping the chips at 100% did
   nothing, because 100% was by then the width of the overgrown track:
   the floor has to be removed at the track, not patched at the child. */
/* The page opens on its two headings with the air a page title would
   have had above it; the first section of the side column starts at the
   top, and each later one sits a section's gap below what precedes it. */
.dash { display: grid; gap: var(--sp-5); grid-template-columns: minmax(0, 1fr);
  margin-top: var(--sp-5); }
.dash-band + .dash { margin-top: 0; }
.dash-side > h2 ~ h2 { margin-top: var(--sp-6); }
.dash-side, .dash-feed { min-width: 0; }
@media (min-width: 64rem) {
  /* Two columns, not three: "your recent work" was the feed filtered to
     one author, and a whole column to express one predicate is a column
     that is empty for most accounts. */
  .dash { grid-template-columns: 20rem minmax(0, 1fr); align-items: start; }
}
/* The first visit's welcome, above its two columns. */
.dash-band { margin-bottom: var(--sp-6); }
.aw-row { display: grid; grid-template-columns: auto minmax(0, 1fr);
  gap: var(--sp-2); align-items: start; }
.aw-row .t-glyph { margin-top: var(--sp-1); }
.dash-more { margin: var(--sp-3) 0 0; font-size: var(--fs-body); }
/* The first visit: the same page, so the same rules, plus these. */
.fv-hello { max-width: 46rem; }
.fv-hello h2 { font-size: var(--fs-lg); margin-bottom: var(--sp-2); }
.fv-lede { margin: 0 0 var(--sp-2); max-width: 60ch; }
.fv-lede .ic { vertical-align: -0.15em; }
.fv-map { display: grid; grid-template-columns: max-content 1fr; gap: var(--sp-1) var(--sp-4); margin: var(--sp-4) 0 0; font-size: var(--fs-body); }
.fv-map dt { color: var(--muted); }
.fv-map .fv-map-h { font-size: var(--fs-xs); text-transform: uppercase; letter-spacing: 0.05em; padding-top: var(--sp-1); }
.fv-map dd { margin: 0; }
.fv-empty { border: 1px dashed var(--rule); border-radius: var(--r-2); padding: var(--sp-3) var(--sp-4); }
.fv-empty p { margin: 0 0 var(--sp-2); }
.fv-empty p:last-child { margin: 0; }
.fv-empty pre { margin: 0 0 var(--sp-2); font-size: var(--fs-sm); overflow-x: auto; }
.search-empty { max-width: 46rem; margin-top: var(--sp-2); }
.search-empty kbd { font: 0.85em var(--font-mono); border: 1px solid var(--rule); border-radius: var(--r-1); padding: 0 0.3em; }
.fv-empty .btn { display: inline-block; margin-top: var(--sp-1); }
.fv-sig { font-size: var(--fs-sm); color: var(--muted); font-variant-numeric: tabular-nums; margin-left: var(--sp-1); }
.fv-sig .ic { vertical-align: -0.15em; }
.fv-second { margin-top: var(--sp-5); }
@media (max-width: 40rem) { .fv-map { grid-template-columns: 1fr; gap: 0; } .fv-map dt { margin-top: var(--sp-2); } }
.dash-act { margin: var(--sp-4) 0 0; }
/* Each thing a signed-in account may do is its own line, not a clause
   in a run-on sentence of checkboxes. */
.part-form > label { display: flex; align-items: baseline; gap: var(--sp-2); margin: var(--sp-1) 0; }
.dash-find { margin: 0 0 var(--sp-2); }
.dash-find input { width: 100%; }
/* The open-work count beside a repository: a link, because the number
   is only useful if it takes you to what it counts. */
a.dash-open { float: right; font-size: var(--fs-sm); color: var(--muted);
  text-decoration: none; border: 1px solid var(--rule);
  border-radius: var(--r-4); padding: 0 var(--sp-2); }
a.dash-open:hover { color: var(--accent); border-color: var(--accent); }
h2.t-view { font-size: var(--fs-base); margin: 0; font-weight: 600; }
/* The wiki's structure panel: two short lists, side by side when there
   is room, under the page list where the reader has finished scanning. */
.w-struct { display: grid; gap: var(--sp-5); margin-top: var(--sp-6);
  padding-top: var(--sp-4); border-top: 1px solid var(--rule); }
@media (min-width: 48rem) { .w-struct { grid-template-columns: 1fr 1fr; } }
.w-struct h2 { font-size: var(--fs-md); margin: 0 0 var(--sp-2); color: var(--muted);
  text-transform: uppercase; letter-spacing: 0.05em; }
ul.w-links { list-style: none; margin: 0; padding: 0; }
/* The count sits BESIDE its label, not flung to the far edge. With
   space-between it ended 496px from the name it belonged to, in a 595px
   row, which reads as two unrelated columns. */
ul.w-links > li { padding: var(--sp-1) 0; display: flex; gap: var(--sp-2);
  align-items: baseline; flex-wrap: wrap; }
ul.w-links a { text-decoration: none; }
ul.w-links a:hover { text-decoration: underline; }
.w-n { font-size: var(--fs-sm); color: var(--muted); }
.w-struct h2 a { color: inherit; text-decoration: none; }
.w-struct h2 a:hover { color: var(--accent); }
.w-struct-links { display: inline-flex; align-items: center; gap: var(--sp-3);
  margin-left: auto; font-size: var(--fs-md); }
.w-struct-links a { text-decoration: none; color: var(--muted); }
.w-struct-links a:hover { color: var(--accent); text-decoration: underline; }
.w-lead { color: var(--muted); font-size: var(--fs-body); max-width: 62ch;
  margin: var(--sp-1) 0 var(--sp-4); }
ul.w-links-full > li { padding: var(--sp-2) 0; border-bottom: 1px solid var(--rule);
  display: block; }
.w-link-t { font-weight: 600; }
/* Who reached for the name. A wanted page without its askers is a to-do
   item with no brief. */
.w-from { display: block; font-size: var(--fs-sm); color: var(--muted);
  margin-top: var(--sp-0); }
/* Saving a report: a name box under the rail, quiet enough that it does
   not compete with the views themselves. */
.t-rail-save { display: flex; gap: var(--sp-1); margin-top: var(--sp-4); }
.t-rail-save input { min-width: 0; flex: 1; font-size: var(--fs-md); }
.t-rail-save button { font-size: var(--fs-md); }
.t-rail-sec li { display: flex; align-items: center; }
.t-rail-sec li > a { flex: 1; min-width: 0; }
.t-rail-x button { background: none; border: 0; color: var(--muted);
  cursor: pointer; padding: 0 var(--sp-1); line-height: 1; }
.t-rail-x button:hover { color: var(--del); }
.dash-depth { margin: var(--sp-2) 0 0; font-size: var(--fs-md); color: var(--muted);
  text-align: center; }
/* A landing: the record, then what it would change, then the talking.
   Branch names are code because they are identifiers people retype. */
.l-meta { margin: -0.4rem 0 var(--sp-4); color: var(--muted); display: flex; gap: var(--sp-2);
  align-items: center; flex-wrap: wrap; }
code.l-branch { background: var(--surface); border: 1px solid var(--rule);
  border-radius: var(--r-1); padding: 0 var(--sp-1); }
.l-desc { margin: var(--sp-2) 0 var(--sp-5); }
.l-desc > :first-child { margin-top: 0; }
.l-cis { margin-bottom: var(--sp-5); }
/* A blocked proposal states the obstacle and whose move it is. Loud
   enough to stop a reader reaching for a control that cannot work. */
.l-block { border: 1px solid var(--del); border-radius: var(--r-2);
  padding: var(--sp-3) var(--sp-4); margin: 0 0 var(--sp-5); }
.l-block strong { color: var(--del); }
/* Not a failure, just a record that has been overtaken: stated in the
   page's own voice rather than in the red of something broken. */
.l-note { border: 1px solid var(--rule); border-radius: var(--r-2);
  padding: var(--sp-3) var(--sp-4); margin: 0 0 var(--sp-5); background: var(--surface); }
.l-note p { margin: var(--sp-1) 0 0; color: var(--muted); }
.l-paths { margin: var(--sp-2) 0; }
ol.l-events { list-style: none; margin: 0 0 var(--sp-5); padding: 0;
  border-left: 2px solid var(--rule); }
.l-ev { padding: var(--sp-1) 0 var(--sp-1) var(--sp-4); color: var(--muted); }
.l-ev strong { color: var(--fg); }
.l-ev-accepted { border-left: 2px solid var(--accent); margin-left: -2px; }
.l-commits h2, .l-diff h2, .l-talk h2 { font-size: var(--fs-base); color: var(--muted);
  text-transform: uppercase; letter-spacing: 0.05em; margin: var(--sp-5) 0 var(--sp-2); }
.l-why { margin: var(--sp-4) 0 var(--sp-2); color: var(--muted); }
/* Collapsed by default: the summary is a control, so it reads as one. */
.l-changes > summary { cursor: pointer; list-style: none; display: flex;
  align-items: baseline; gap: var(--sp-2); padding: var(--sp-2) 0; }
.l-changes > summary::-webkit-details-marker { display: none; }
.l-changes > summary::before { content: "\25b8"; color: var(--muted);
  transition: transform 0.12s; }
.l-changes[open] > summary::before { content: "\25be"; }
.l-sum { font-size: var(--fs-base); color: var(--muted); text-transform: uppercase;
  letter-spacing: 0.05em; }
.l-n { color: var(--muted); font-size: var(--fs-body); }
/* A disabled decision reads as disabled, not as a dimmer accent: the
   accent drains out, the outline goes dashed, and the reason beside it
   is what carries the colour. Half-opacity orange on a dark ground
   looked like a button that was merely tired. */
.f-reply-foot button[disabled] { opacity: 0.6; cursor: not-allowed; background: transparent; color: var(--muted); border: 1px dashed var(--muted); }
/* The filter strip SCROLLS; it does not widen the page. nav.seg-box
   sets flex-wrap: nowrap and beats .dash-chips's wrap on specificity,
   so on a phone the four chips could neither wrap nor shrink and forced
   the document wider than the viewport — which is why the pager looked
   broken and every heading ran off the right edge. Nothing was wrong
   with the pager: it was being measured against a page 40px too wide.
   A segmented control must not wrap (its rounded ends would repeat mid
   row), so the strip is given its own scroll instead. */
.dash-chips { margin-bottom: var(--sp-2); max-width: 100%; overflow-x: auto;
  overscroll-behavior-x: contain; }
/* Tighter than the general segmented control: six chips carrying an
   icon, a word and a count only fit the feed column at this padding,
   and the count only earns its place if the whole strip fits. */
.dash-chips a { display: inline-flex; align-items: center; gap: var(--sp-1);
  padding-left: var(--sp-2); padding-right: var(--sp-2); }
.dash-chips .n { font-size: 0.8em; }
.dash-chips .ic { width: 0.9rem; height: 0.9rem; flex: none; }
.dash-chips .chip-s { display: none; }
/* The strip degrades in one direction, shedding the least-load-bearing
   thing first. Full: icon, full label, count. Then the counts go —
   six-figure numbers are the widest thing here and the least useful at
   a glance. Then the icons, which only ever restated the label. Last
   the label shortens: "ticket changes" becomes "tickets". A word
   always survives, so no chip is ever a glyph you have to guess at,
   and the full wording stays in aria-label and title throughout.

   This sheds only from the CHIPS. The feed rows below keep their icon
   at every width — there the glyph is the only thing marking what kind
   of event a row is. */
/* Keyed to the COLUMN, not the viewport: the strip sits beside the
   repository sidebar, so on a 1200px desktop it still only gets ~820px
   and the full form wants 930. A viewport query sheds at the wrong
   moments in both directions. */
.dash-feed { container-type: inline-size; container-name: feed; }
/* Thresholds are the measured width each form needs, not round
   numbers: counts+icons+words want 929, icons+words 775, words alone
   655, and the short words 370. Each tier is dropped exactly when the
   column can no longer hold it. */
@container feed (max-width: 928px) {
  .dash-chips .n { display: none; }
}
@container feed (max-width: 774px) {
  .dash-chips .ic { display: none; }
}
/* Check-ins, landings and technote edits are one chip until the column
   is narrow, where the three fold into "code" to give the strip back
   its width. The same tier as the short words: that is when the strip
   stops fitting. */
.dash-chips .k-code { display: none; }
@container feed (max-width: 654px) {
  .dash-chips .chip-w { display: none; }
  .dash-chips .chip-s { display: inline; }
  .dash-chips .k-part { display: none; }
  .dash-chips .k-code { display: inline-flex; }
}
.dash-awaiting { margin-bottom: var(--sp-4); }
.dash-awaiting h3 { font-size: var(--fs-base); margin: 0 0 var(--sp-2); }
/* A phone. Six short words and nothing else, at the tightest spacing
   that still gives each one a tap target. */
@container feed (max-width: 420px) {
  .dash-chips a { padding-left: var(--sp-1); padding-right: var(--sp-1);
    font-size: 0.92em; }
}
/* The repository selector is a filter above the list, not part of it:
   it needs air beneath it or the list reads as the select's dropdown. */
.dash-facet { display: flex; align-items: center; gap: var(--sp-2);
  margin: var(--sp-0) 0 var(--sp-4); color: var(--muted); }
.dash-facet label { display: flex; align-items: center; gap: var(--sp-2); }
.dash-chips .n { color: var(--muted); }
.dash-chips .here .n { color: inherit; }
.dash h2 { font-size: var(--fs-base); margin: 0 0 var(--sp-2); color: var(--muted);
  text-transform: uppercase; letter-spacing: 0.05em; }
.dash .tl-badge { margin-right: var(--sp-1); }
/* The dashboard styles its own lists. It used to borrow ul.tickets,
   so when the tracker became a bordered card the dashboard grew three
   boxes it never asked for — a shared class is a shared fate. */
ul.dash-list { margin: var(--sp-1) 0 0; }
ul.dash-list > li { padding: var(--sp-2) var(--sp-4); }
/* Weight is a signal, and a page where every line is bold has spent it.
   A repository NAME is a label and keeps some — it is what you scan the
   sidebar for. A feed entry is a sentence; it gets none, and stays
   findable by being the link on its row. */
a.dash-title { font-weight: 500; color: var(--fg); text-decoration: none; }
.dash-feed a.dash-title { font-weight: 400; }
/* Two lines, then stop. One landing comment ran to fifteen lines on a
   phone and WAS the viewport — a feed entry is a pointer to the thing,
   and the thing is one tap away. Clamping is CSS-only, so with styles
   off the full text is still there rather than cut by the server. */
.dash-feed a.dash-title { display: -webkit-box; -webkit-line-clamp: 2;
  -webkit-box-orient: vertical; overflow: hidden; }
a.dash-title:hover { color: var(--accent); text-decoration: underline; }
.dash-meta { margin-top: var(--sp-0); font-size: var(--fs-md); color: var(--muted); }
.dash-meta a { color: inherit; }
.dash-meta a:hover { color: var(--accent); }
/* GitHub's latest-commit bar: one line, the message eats the slack and
   truncates; the hash/date/count block never wraps. On a phone the
   message yields entirely and the bar keeps author + date, exactly the
   collapse GitHub makes. */
.tip-bar { display: flex; align-items: center; gap: var(--sp-2); min-width: 0;
  border: 1px solid var(--rule); border-radius: var(--r-2); background: var(--surface);
  padding: var(--sp-2) var(--sp-3); margin: 0 0 var(--sp-2); font-size: var(--fs-body); }
.tip-user { font-weight: 600; flex: none; }
.tip-msg { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  color: var(--muted); text-decoration: none; }
.tip-msg:hover { color: var(--accent); text-decoration: underline; }
.tip-right { flex: none; margin-left: auto; color: var(--muted); display: inline-flex;
  align-items: center; gap: var(--sp-1); white-space: nowrap; }
.tip-right a { color: var(--muted); text-decoration: none; display: inline-flex;
  align-items: center; gap: var(--sp-1); }
.tip-right a:hover { color: var(--accent); }
@media (max-width: 44rem) {
  .tip-bar .tip-msg, .tip-bar .tip-hash { display: none; }
}
/* On a phone the stats strip stops being a card and becomes GitHub's
   wrapping row of inline facts, closing the About flow above the
   branch bar. */
@media (max-width: 44rem) {
  .repo-stats { border: 0; background: none; padding: var(--sp-1) 0 var(--sp-2);
    border-bottom: 1px solid var(--rule); border-radius: 0; }
}
/* GitHub's About sidebar, borne on Gitea's literal frame: a real grid,
   not a float — a float squeezes the bars sharing its rows until their
   contents collide. Column 1 holds every bar, table and document;
   column 2 is the sidebar, spanning all rows. One column where there
   is no room, and the aside is FIRST in source order, so the phone
   reads description → facts → branch bar → files, exactly as GitHub
   and Forgejo order it. */
.repo-home { display: grid; grid-template-columns: minmax(0, 1fr); }
.repo-home > * { grid-column: 1; min-width: 0; }
.about-card { margin: var(--sp-2) 0 var(--sp-2); font-size: var(--fs-body); }
@media (min-width: 64rem) {
  .repo-home.has-about { grid-template-columns: minmax(0, 1fr) 17rem; gap: 0 var(--sp-6); }
  .repo-home.has-about > .about-card { grid-column: 2; grid-row: 1 / span 40; margin: var(--sp-2) 0 0; }
}
.about-h { font-size: var(--fs-base); margin: 0 0 var(--sp-2); }
.about-h a { color: var(--fg); text-decoration: none; }
.about-h a:hover { text-decoration: underline; }
.about-desc { margin: 0 0 var(--sp-2); color: var(--fg); }
.topics { display: flex; flex-wrap: wrap; gap: var(--sp-1); margin: var(--sp-3) 0 var(--sp-3); }
a.b-pill { color: inherit; text-decoration: none; }
a.b-pill:hover { border-color: var(--fg); color: var(--fg); }
.about-row { display: flex; align-items: center; gap: var(--sp-2); margin: 0;
  padding: var(--sp-1) 0; color: var(--muted); text-decoration: none; min-width: 0; }
.about-row .ic { flex: none; }
/* An about row WRAPS, so its icon belongs on the first line rather than
   centred over all of them. Baseline alignment gets close and misses by
   1.70px, because an SVG has no baseline and the synthesised one stands
   the icon on the text's.
   Giving the icon a box exactly one line tall settles it without a
   magic number: the width still says how big the glyph is, and the
   SVG's own preserveAspectRatio centres it inside the taller box. On a
   genuinely two-line row that measures +0.29px, against -1.70px for
   baseline and -3.30px for a plain start.
   Scoped here rather than shared, for the reason the ticket rows teach:
   the SAME declaration measured +2.39px on li.t-row, where the row's
   own baseline alignment is already exact at -0.01px. There is no
   alignment that is right for every row.
   Behind @supports because without the lh unit the height falls back to
   1em and a plain start is WORSE than what we have -- an unsupported
   browser should keep today's 1.70px, not inherit 3.30px. */
@supports (height: 1lh) {
  .about-row > .ic { align-self: start; height: 1lh; }
}
/* A wrapping title starts level with its icon rather than centred
   against the whole block, and its later lines stay in the text
   column. Notes and Announcement carry real titles; the icon rows
   above them are all one line, where this is indistinguishable. */
.about-row:has(strong) { align-items: baseline; }
.about-row strong { min-width: 0; }
.about-row span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
a.about-row:hover { color: var(--accent); }
a.about-row:hover span { text-decoration: underline; }
.about-link { color: var(--accent); font-weight: 600; }
.about-div { border-top: 1px solid var(--rule); margin: var(--sp-4) 0; }
.about-tag a { color: var(--fg); text-decoration: none; }
.about-tag a:hover { text-decoration: underline; }
.latest-pill { border: 1px solid var(--add); color: var(--add); border-radius: var(--r-4);
  padding: 0 var(--sp-2); font-size: var(--fs-xs); line-height: 1.5; }
.about-sub { margin: 0 0 var(--sp-1) var(--sp-5); font-size: var(--fs-md); color: var(--muted); }
.about-sub a { color: var(--accent); text-decoration: none; }
.about-sub a:hover { text-decoration: underline; }
.doc-tabs { display: flex; flex-wrap: wrap; gap: var(--sp-1) var(--sp-4); margin: var(--sp-5) 0 0;
  border-bottom: 1px solid var(--rule); font-size: var(--fs-body); }
nav.doc-tabs a { padding: var(--sp-1) var(--sp-0); border: 0; border-bottom: 2px solid transparent;
  border-radius: 0; background: none; color: var(--muted); text-decoration: none; }
nav.doc-tabs a.here { color: var(--fg); background: none; border: 0;
  border-bottom: 2px solid transparent; font-weight: 600; }
nav.doc-tabs a { position: relative; }
.doc-panel { margin-top: var(--sp-4); }
/* The structured diff: gutters own the numbers and the +/- marker so
   selection copies clean code; the mark class is the intra-line "what
   actually changed" emphasis; syntax classes come from the chroma
   stylesheet already on the page (.dtab carries .chroma). */
.dtab { border-collapse: collapse; width: 100%; font: var(--fs-md)/1.45 var(--font-mono); }
.dtab td { padding: 0 var(--sp-2) 0 0; vertical-align: top; }
.dnum { user-select: none; text-align: right; color: var(--muted); min-width: 3ch;
  font-variant-numeric: tabular-nums; padding-left: var(--sp-2); }
.dmark { user-select: none; width: 1ch; color: var(--muted); }
tr.d-addition .dmark::before { content: "+"; }
tr.d-deletion .dmark::before { content: "-"; }
tr.d-addition, .dcell.d-addition { background: var(--add-row); }
tr.d-deletion, .dcell.d-deletion { background: var(--del-row); }
.dcode { white-space: pre-wrap; overflow-wrap: anywhere; width: 100%; }
/* A phone gets one number column, the line's own (the new number, or
   the old one for a removed line), and code that scrolls sideways
   instead of wrapping: two gutters and a wrap left a 30-character line
   in four fragments. */
.dnum-one { display: none; }
/* Line notes (linenotes.go). A line number that opens one is quiet
   until pointed at; a note sits under its line as a card in the page's
   own face, and on a phone it stays at the left edge while wide code
   scrolls under it. */
.dn-link { color: inherit; text-decoration: none; }
.dn-link:hover, .dn-link:focus-visible { color: var(--accent); text-decoration: underline; }
tr.d-notes > td { padding: var(--sp-2) var(--sp-3); background: var(--bg);
  border-top: 1px solid var(--rule); border-bottom: 1px solid var(--rule);
  white-space: normal; font-family: var(--font-sans); }
tr.d-notes > td > * { position: sticky; left: var(--sp-3);
  max-width: min(46rem, calc(100vw - 4 * var(--sp-3))); }
.d-note { border: 1px solid var(--rule); border-radius: var(--r-2);
  padding: var(--sp-2) var(--sp-3); background: var(--surface); }
.d-note + .d-note { margin-top: var(--sp-2); }
.d-note-h { display: block; font-size: var(--fs-sm); color: var(--muted); margin-bottom: var(--sp-1); }
.d-note-b { font-size: var(--fs-body); }
.d-note-b p { margin: var(--sp-1) 0; }
.d-note-form textarea { width: 100%; box-sizing: border-box; font-size: var(--fs-body);
  padding: var(--sp-2); border: 1px solid var(--rule); border-radius: var(--r-row);
  background: var(--bg); color: var(--fg); }
.d-note-form textarea:focus { outline: none; border-color: var(--accent); }
.d-note-foot { display: flex; justify-content: flex-end; align-items: center;
  gap: var(--sp-3); margin-top: var(--sp-2); font-size: var(--fs-sm); }
.f-line code { font-size: var(--fs-sm); }
@media (max-width: 40rem) {
  .dtab:not(.split) .dnum-old, .dtab:not(.split) .dnum-new { display: none; }
  .dtab:not(.split) .dnum-one { display: table-cell; }
  .filediff .tw { overflow-x: auto; }
  .dtab:not(.split) .dcode { white-space: pre; overflow-wrap: normal; }
}
tr.d-addition .dm, .dcell.d-addition .dm { background: var(--add-mark); border-radius: var(--r-1); }
tr.d-deletion .dm, .dcell.d-deletion .dm { background: var(--del-mark); border-radius: var(--r-1); }
/* Side-by-side: two half-width code cells; a side with no counterpart
   is a hatched void, fossil's idiom for "nothing here". */
.dtab.split .dcell { white-space: pre-wrap; overflow-wrap: anywhere; width: 50%;
  padding-right: var(--sp-2); border-left: 1px solid var(--rule); padding-left: var(--sp-2); }
.dcell.d-void { background: repeating-linear-gradient(45deg, transparent, transparent 4px,
  var(--surface) 4px, var(--surface) 8px); }
.hunkhead { color: var(--muted); font: var(--fs-sm)/1.4 var(--font-mono);
  padding: var(--sp-1) var(--sp-2); border-top: 1px solid var(--rule); background: var(--surface); }
.filediff { margin: var(--sp-4) 0; border: 1px solid var(--rule); border-radius: var(--r-2); overflow: hidden; }
/* Side-by-side halves the room each line gets, so the split view earns
   what GitHub gives it: the diff breaks out of the page column and
   takes the viewport, 2rem margins aside. 50% - 50vw walks from the
   centred column's edge to the viewport's. Only once the viewport is
   actually wider than the column — below that the breakout is a no-op
   that still moves pixels. */
@media (min-width: 80rem) {
  .filediff.wide { margin-left: calc(50% - 50vw + 2rem);
    margin-right: calc(50% - 50vw + 2rem); }
}
.filediff-head { margin: 0; padding: var(--sp-2) var(--sp-3); font-size: var(--fs-body);
  background: var(--surface); }
.diffcount { font-weight: 600; margin: var(--sp-4) 0 var(--sp-1); }
.diffmore { color: var(--muted); font-size: var(--fs-body); margin: var(--sp-4) 0; text-align: center; min-height: 1.5rem; }
/* Segmented toggle (GitHub's "Unified | Split"): one bordered group,
   buttons joined, the current one filled. Overrides nav a's pill. */
nav.toggle { display: inline-flex; margin: var(--sp-2) 0 var(--sp-3); border: 1px solid var(--rule);
  border-radius: var(--r-row); overflow: hidden; font-size: var(--fs-body); }
nav.toggle a { border: 0; border-radius: 0; padding: var(--sp-1) var(--sp-3); color: var(--fg);
  line-height: 1.25; }
nav.toggle a + a { border-left: 1px solid var(--rule); }
nav.toggle a:hover { background: var(--surface); }
nav.toggle a.here { background: var(--code-bg); color: var(--fg); font-weight: 600; }
/* The pager, shared by the people list and windowed diffs. GitHub's
   pagination shape: bordered buttons either side of a position
   readout; a disabled end keeps its place, muted, so nothing shifts
   between pages. nav a's pill rule is overridden per button. */
nav.pager { display: flex; align-items: center; justify-content: center; gap: var(--sp-2);
  margin: var(--sp-3) 0 var(--sp-4); font-size: var(--fs-body); flex-wrap: wrap; }
nav.pager a.pager-btn, nav.pager span.pager-btn { display: inline-block; padding: var(--sp-1) var(--sp-3); border-radius: var(--r-row);
  border: 1px solid var(--rule); text-decoration: none; color: var(--fg); line-height: 1.25;
  white-space: nowrap; }
nav.pager a.pager-btn:hover { background: var(--surface); border-color: var(--muted); }
nav.pager .pager-btn[aria-disabled="true"] { color: var(--muted); border-color: transparent;
  pointer-events: none; }
nav.pager .pager-where { color: var(--muted); padding: 0 var(--sp-2); font-variant-numeric: tabular-nums; }
nav.pager .pager-where b { color: var(--fg); font-weight: 600; }
/* The message leads the page but is not a headline (no forge makes it
   one — its length is untrusted). wrap-anywhere: a 2000-character
   unbroken line is real data, not a layout veto. */
/* The header names the page (GitHub's "Commit abc123"); the message
   sits below it as content in a quiet box, subject bolded — unless the
   first line is too long to be a subject, in which case it is a
   paragraph and renders like one. wrap-anywhere throughout: a
   2000-character unbroken line is real data, not a layout veto. */
.ci-h { font-size: var(--fs-xl); font-weight: 600; margin: 0 0 var(--sp-1);
  display: flex; align-items: center; flex-wrap: wrap; gap: var(--sp-2); }
/* The full hash: its own line, wrapping anywhere, because 64 unbroken
   characters have no break opportunity of their own and would push a
   narrow page sideways. */
.ci-fullhash { margin: 0 0 var(--sp-3); font-size: var(--fs-sm); color: var(--muted);
  overflow-wrap: anywhere; }
/* Both of these sit inside 1.4rem display type, so their icons are
   sized in REM rather than em — at 1em the glyph would inherit the
   heading and render half again too large. */
.ci-copy { background: none; border: 0; color: var(--muted); cursor: pointer;
  padding: var(--sp-0); line-height: 1; display: inline-flex; align-items: center; }
.ci-copy .ic { width: 0.95rem; height: 0.95rem; vertical-align: 0; }
.ci-copy:hover { color: var(--accent); }
/* A 28x22 button is a fine SIGHT and a poor TARGET: a thumb wants
   44px. The hit area is grown with a pseudo-element rather than with
   padding, so the button keeps its size next to the hash and only the
   reachable part changes — and only where the pointer is coarse. */
@media (pointer: coarse) {
  .ci-copy { position: relative; }
  .ci-copy::after { content: ""; position: absolute; top: 50%; left: 50%;
    width: 44px; height: 44px; transform: translate(-50%, -50%); }
}
.ci-copy.done { color: var(--add); }
.ci-tags { display: inline-flex; gap: var(--sp-1); flex-wrap: wrap; }
.ci-tag { display: inline-flex; align-items: center; gap: var(--sp-1);
  font-size: var(--fs-xs); font-weight: 500; line-height: 1.3;
  border: 1px solid var(--rule); border-radius: var(--r-4); padding: var(--sp-0) var(--sp-2);
  color: var(--muted); background: var(--surface); white-space: nowrap; }
.ci-tag .ic { width: 0.85rem; height: 0.85rem; vertical-align: 0; }
a.ci-tag { text-decoration: none; }
a.ci-tag:hover { color: var(--accent); border-color: var(--accent); }
/* Tag history. A log read forwards, so the STANDING rows must read as
   current and the overruled ones as past without a legend doing all the
   work: struck text and reduced contrast say "superseded" on sight. */
.ct-back { margin: 0 0 var(--sp-4); font-size: var(--fs-body); }
.ct-h { font-size: var(--fs-lg); margin: var(--sp-5) 0 var(--sp-2); }
.ct-flag { font-size: var(--fs-xs); text-transform: uppercase; letter-spacing: 0.06em;
  border: 1px solid var(--del); color: var(--del); border-radius: var(--r-4);
  padding: var(--sp-0) var(--sp-2); font-weight: 600; }
.ct-was { margin: var(--sp-2) 0 0; font-size: var(--fs-md); color: var(--muted); }
.ct-waslabel { text-transform: uppercase; letter-spacing: 0.07em; font-size: var(--fs-xs);
  font-weight: 600; margin-right: var(--sp-1); }
table.ct { width: 100%; border-collapse: collapse; font-size: var(--fs-md); }
table.ct th { text-align: left; font-weight: 600; color: var(--muted);
  font-size: var(--fs-xs); text-transform: uppercase; letter-spacing: 0.06em;
  padding: var(--sp-1) var(--sp-3) var(--sp-2) 0; border-bottom: 1px solid var(--rule); white-space: nowrap; }
table.ct td { padding: var(--sp-2) var(--sp-3) var(--sp-2) 0; border-bottom: 1px solid var(--rule);
  vertical-align: top; }
.ct-when { white-space: nowrap; color: var(--muted); font-variant-numeric: tabular-nums;
  font-size: var(--fs-sm); }
.ct-src, .ct-sign { white-space: nowrap; }
.ct-val { overflow-wrap: anywhere; }
.ct-none { color: var(--muted); font-style: italic; }
.ct-gone { color: var(--muted); }
.ct-gone .ct-val, .ct-gone .ct-name code { text-decoration: line-through; }
.sg { display: inline-block; min-width: 1.1rem; text-align: center; font-weight: 700;
  font-family: var(--font-mono); }
.sg-add { color: var(--add); }
.sg-del { color: var(--del); }
.sg-prop { color: var(--accent); }
table.mf { width: 100%; border-collapse: collapse; font-size: var(--fs-md); }
table.mf td { padding: var(--sp-2) var(--sp-3) var(--sp-2) 0; border-bottom: 1px solid var(--rule);
  vertical-align: baseline; }
.mf-l { width: 1.6rem; }
.mf-l code { font-weight: 700; color: var(--accent); }
.mf-t { font-family: var(--font-mono); font-size: var(--fs-sm);
  overflow-wrap: anywhere; }
.mf-n { color: var(--muted); font-size: var(--fs-sm); white-space: normal; min-width: 12rem; }
@media (max-width: 40rem) {
  table.mf, table.mf tbody, table.mf tr, table.mf td { display: block; width: auto; }
  table.mf tr { border-bottom: 1px solid var(--rule); padding: var(--sp-2) 0; }
  table.mf td { border: 0; padding: var(--sp-0) 0; }
  .mf-l { display: inline-block; margin-right: var(--sp-2); }
  .mf-t { display: inline; }
}
.mf-files { margin: var(--sp-4) 0 0; display: flex; flex-wrap: wrap; gap: var(--sp-2);
  align-items: baseline; }
.mf-why { color: var(--muted); font-size: var(--fs-md); }
.mf-note { color: var(--muted); font-size: var(--fs-sm); margin: var(--sp-4) 0 0; max-width: 62ch; }
.dv-mode { margin: 0 0 var(--sp-2); font-size: var(--fs-body); }
.ct-legend { font-size: var(--fs-sm); color: var(--muted); margin: var(--sp-3) 0 0; max-width: 62ch; }
/* A person, as one target: the mark and the name move together, and
   the whole thing takes the page's link colour on hover — the same
   affordance the hash and the branch beside it already have. */
/* A byline inside a row that is ITSELF a link stays plain — the docs
   list and the wiki home card are whole-card anchors, and an anchor
   inside an anchor is invalid: the parser closes the outer one at the
   inner, which tore those rows in half. The row already goes
   somewhere; a second target inside it was never the right idea.

   INLINE, not inline-flex. A flex box is atomic: it cannot flow with
   the text around it, so as a byline it stopped wrapping mid-phrase and
   jumped to a line of its own — which is what it did to the docs list,
   where every row grew a second line. The mark is aligned by the
   baseline rule .idn already carries. */
a.who { color: inherit; text-decoration: none; border: 0; padding: 0;
  white-space: nowrap; }
a.who:hover { color: var(--accent); }
a.who:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.tl-filter { margin: 0 0 var(--sp-2); font-size: var(--fs-body); color: var(--muted);
  display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; }
/* Patch is an ACTION, not a third view, so it stands outside the
   segmented toggle and borrows the pager button's shape. */
.ci-patch { display: inline-flex; align-items: center; gap: var(--sp-1);
  margin: var(--sp-2) 0 var(--sp-3) var(--sp-2); padding: var(--sp-1) var(--sp-3); font-size: var(--fs-body);
  line-height: 1.25; border: 1px solid var(--rule); border-radius: var(--r-row);
  color: var(--fg); text-decoration: none; vertical-align: top; }
.ci-patch .ic { width: 0.95rem; height: 0.95rem; vertical-align: 0; }
.ci-patch:hover { background: var(--surface); border-color: var(--muted); }
/* Counts read as a pair, so they hold their column: tabular numerals
   stop a list of files jittering line to line. */
.ci-stat { font-size: var(--fs-sm); font-weight: 400; font-variant-numeric: tabular-nums;
  white-space: nowrap; }
.ci-add { color: var(--add); }
.ci-del { color: var(--del); }
.ci-box { margin: 0 0 var(--sp-4); }
.ci-sub { font-weight: 600; margin: 0; overflow-wrap: anywhere; }
.ci-sub.long { font-weight: 400; }
.ci-msg { white-space: pre-wrap; overflow-wrap: anywhere;
  margin: var(--sp-2) 0 0; color: var(--muted); }
/* The changed-files list wears the files page's clothes: icon + name
   as one label, no underline until hover, the change kind sitting
   where the listing puts its sizes. */
/* The changed-file TREE. One row per directory, its count on the row,
   the A/M/D tally beside it — so a 600-file check-in says where it
   changed before a reader scrolls anything. */
.ftree { margin: 0 0 var(--sp-3); }
/* Wrapping and an ellipsis are DEFENSIVE, not measured. The tally sits
   at the row's right edge, so a compressed path long enough to fill a
   narrow row would push it out; wrapping only engages when the content
   genuinely does not fit, and a directory name is an identifier that
   must not break mid-segment. Headless Chrome cannot render a real
   phone viewport (it clamps the window and the layout viewport stays
   wide), so this pair is reasoned from the box model rather than seen
   — the device is the test. */
.ftree-d > summary { cursor: pointer; padding: var(--sp-1) var(--sp-0); display: flex;
  align-items: center; gap: var(--sp-2); border-radius: var(--r-row); flex-wrap: wrap; }
/* A directory name is an identifier and must not break mid-segment; it
   scrolls its own row instead of reflowing the tally under it. */
.ftree-d > summary code { overflow: hidden; text-overflow: ellipsis;
  white-space: nowrap; max-width: 100%; }
.ftree-d > summary:hover { background: var(--surface); }
.ftree-d { border-left: 1px solid var(--rule); margin-left: var(--sp-1);
  padding-left: var(--sp-2); }
.ftree-n { color: var(--muted); font-size: var(--fs-sm); }
.ftree-t { margin-left: auto; display: inline-flex; gap: var(--sp-2);
  font-size: var(--fs-sm); font-variant-numeric: tabular-nums; }
.ftree-t .d-add { color: var(--add); }
.ftree-t .d-del { color: var(--del); }
.ftree-t .d-mod { color: var(--muted); }
.ftree-files { list-style: none; margin: var(--sp-0) 0 var(--sp-1); padding: 0; }
/* Columns only where a group is long enough to need them; 18rem is
   about the width a path reads at without wrapping mid-name. */
.ftree-files.cols { columns: 18rem; column-gap: var(--sp-5); }
.ftree-files li { display: flex; align-items: center; gap: var(--sp-2);
  padding: var(--sp-0) var(--sp-0); break-inside: avoid; }
.ftree-files li:hover { background: var(--surface); }
.ftree-files a { text-decoration: none; display: inline-flex;
  align-items: center; gap: var(--sp-1); min-width: 0; }
.ftree-files a:hover { text-decoration: underline; }
.ftree-files .tl-badge { margin-left: auto; }
/* In columns the badge follows the NAME instead of the column edge:
   pushed to the edge it left a hand's width of nothing between a
   filename and its own status, in every row of every column. */
.ftree-files.cols li { justify-content: flex-start; }
.ftree-files.cols .tl-badge { margin-left: var(--sp-2); }
/* The summary strip IS the filter: each chip says how many of that
   kind, and narrows to them when pressed. */
.ftree-f { margin: 0 0 var(--sp-2); display: flex; gap: var(--sp-2); align-items: center;
  flex-wrap: wrap; font-size: var(--fs-md); }
.ftree-k { text-decoration: none; color: var(--muted); border: 1px solid var(--rule);
  border-radius: var(--r-row); padding: var(--sp-0) var(--sp-2); font-variant-numeric: tabular-nums; }
.ftree-k:hover { border-color: var(--muted); color: var(--fg); }
.ftree-k strong { color: var(--fg); }
.ftree-k.here { border-color: var(--fg); color: var(--fg); background: var(--surface); }
.ftree-k.k-A strong { color: var(--add); }
.ftree-k.k-D strong { color: var(--del); }
.ftree-only { color: var(--muted); font-size: var(--fs-sm); }
.ftree-clear { text-decoration: none; color: var(--muted); font-size: var(--fs-sm); }
.ftree-clear:hover { text-decoration: underline; }
.difffiles { margin: 0 0 var(--sp-3); }
.difffiles li { display: flex; align-items: center; gap: var(--sp-2);
  padding: var(--sp-1) var(--sp-4); line-height: 1.45; }
.difffiles li:hover { background: var(--surface); }
.difffiles a { text-decoration: none; display: inline-flex; align-items: center;
  gap: var(--sp-2); min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.difffiles a:hover { text-decoration: underline; }
.difffiles .tl-badge { margin-left: auto; }
.b-pill { font-size: var(--fs-xs); padding: var(--sp-0) var(--sp-1); border-radius: var(--r-4);
  border: 1px solid var(--rule); color: var(--muted); white-space: nowrap; }
.b-pill.b-pri { border-color: var(--accent); color: var(--accent); }
.t-badges span { font-size: var(--fs-xs); padding: var(--sp-0) var(--sp-1); border-radius: var(--r-4);
  border: 1px solid var(--rule); color: var(--muted); white-space: nowrap; }
.t-badges .b-pri { border-color: var(--accent); color: var(--accent); }
/* A card looks the same on either board. .board-rows is the team's
   (RFC 0018), which lays cards out in one column rather than in lanes
   because its grouping axis is people and there is no bound on how
   many — but a card is a card, and the two surfaces being recognisably
   one idea is the whole reason the team board borrows this look rather
   than inventing another. */
.board .card, .board-rows .card { background: var(--bg);
  border: 1px solid var(--rule); border-radius: var(--r-row);
  padding: var(--sp-2); margin-bottom: var(--sp-2); }
.board .card .t-title, .board-rows .card .t-title { display: block; font-weight: 600; overflow-wrap: anywhere; }
.board .card .t-meta, .board-rows .card .t-meta { margin-top: var(--sp-1); }
/* The move control is deliberately always present, never revealed by
   script: it is how a touch or keyboard reader moves a card, and any
   drag layer is an addition on top of it. */
/* On a phone the open columns stack full-width and the folded ones
   become a row of chips above them. The earlier design kept a swiped
   row here; a page that scrolls sideways was judged worse than a board
   that reads as stacked lists, which it is on any phone-sized screen. */
@media (max-width: 40rem) {
  .board { gap: var(--sp-2); }
  .board-col { flex: 1 1 100%; }
  .board-col.folded { flex: 0 0 auto; order: -1; padding: var(--sp-1) var(--sp-2); }
  .board-col.folded h3 { writing-mode: horizontal-tb; transform: none; }
}
/* ---- wiki ---------------------------------------------------------
   Rails beside the page on wide screens, page alone on narrow ones.
   Dropped rather than stacked on a phone: a dozen sibling links and a
   contents above the text push the text itself below the fold, and
   both rails lead only where the page already leads. */
/* Two widths, not one. Running text caps at the measure because that
   is what reading wants; a code block, a table or a diagram gets the
   whole column, because wrapping those to prose width is what makes
   people scroll sideways. The column then sizes to the CONTENT rather
   than eating the viewport — at 1800px the article ended 391px short of
   the contents rail, which is not breathing room, it is a hole. */
.w-doc { --measure: 62ch; --column: 78ch;
  display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--sp-5); }
/* The track list follows the rails that are actually THERE. A doc page
   has no sibling rail and a short page has no contents, and a grid that
   reserves a column for a rail nobody rendered puts the document itself
   in the 11.5rem gutter. */
@media (min-width: 64rem) {
  /* Left-aligned, not centred. Centring indented the block away from
     the page title, which sits outside the grid — on a doc page the
     heading started at 392px and its own first paragraph at 495px,
     which reads as two columns that missed each other. The leftover
     goes to the page edge, where every other page in the forge puts it.

     The contents track is reserved WHETHER OR NOT a page has contents.
     Sizing it to what each page happens to hold made the prose jump
     sideways every time a reader clicked from a page with headings to
     one without — the text should sit where it sat, and an empty track
     costs nothing but the space it was always going to occupy. The
     left rail still follows :has, because a doc page has no siblings
     to list and reserving a gutter for nothing is a different mistake. */
  .w-doc { grid-template-columns: minmax(0, var(--column)) 12.5rem; }
  .w-doc:has(.w-rail) {
    grid-template-columns: 11.5rem minmax(0, var(--column)) 12.5rem; }
  /* A page with NEITHER rail centres instead. The rule above is left-
     aligned for one specific reason — the layout's h1 sits outside the
     grid, so centring the block left the heading and its own first
     paragraph starting a hundred pixels apart. A notes page draws its
     heading INSIDE the column (OwnH1), so nothing is left behind to
     miss, and the reserved-but-empty contents track that keeps prose
     from jumping sideways between doc pages has nothing to hold still
     for. Without this the column strands itself against the left edge
     under a wide empty margin, which is not how the rest of the forge
     lays a page out. */
  .w-doc.w-solo { grid-template-columns: minmax(0, var(--column));
    justify-content: center; }
}
.w-rail, .w-toc { display: none; }
@media (min-width: 64rem) { .w-rail, .w-toc { display: block; } }
.w-railhead { font-size: var(--fs-xs); text-transform: uppercase;
  letter-spacing: 0.07em; color: var(--muted); font-weight: 600;
  margin: 0 0 var(--sp-2); }
.w-rail ul, .w-toc ul { list-style: none; margin: 0; padding: 0; }
.w-rail a { display: block; padding: var(--sp-1) var(--sp-2); border-radius: var(--r-row);
  text-decoration: none; color: var(--fg); font-size: var(--fs-md);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.w-rail a:hover { background: var(--surface); }
.w-rail a.here { background: var(--surface); color: var(--fg); font-weight: 600; }
/* No ring on these two. A box drawn around a wrapped three-line entry
   reads as a defect, and the rails are dense enough that colour alone
   carries the state. Focus still SHOWS — it borrows the accent the
   current entry uses — because a keyboard reader who cannot see where
   they are is worse off than one looking at an ugly outline. */
.w-rail a:focus-visible, .w-toc a:focus-visible { outline: none; }
.w-rail a:focus-visible { background: var(--surface); color: var(--accent); }
.w-toc a:focus-visible { color: var(--accent); border-left-color: var(--accent); }
.w-railall { color: var(--muted) !important; }
.w-crumb { font-size: var(--fs-sm); color: var(--muted); margin: 0 0 var(--sp-2); }
.w-crumb a { text-decoration: none; }
.w-by { font-size: var(--fs-sm); color: var(--muted); margin: 0 0 var(--sp-4);
  padding-bottom: var(--sp-2); border-bottom: 1px solid var(--rule); }
/* THE measure. Running text at the full content width came out around
   152 characters a line, roughly twice the comfortable maximum, which
   is the single thing most wrong with reading a long page here. */
.w-main article > * { max-width: var(--measure); }
/* The exceptions: content whose lines are authored, not flowed. */
.w-main article > pre,
.w-main article > table,
.w-main article > .tw,
.w-main article > figure,
.w-main article > svg,
.w-main article > .pikchr { max-width: none; }
.w-main article h1:first-child { margin-top: 0; }
.w-toc { position: sticky; top: 0.6rem; align-self: start; }
.w-toc ul { border-left: 2px solid var(--rule); }
/* These rails are nav elements, and the global nav-link rule gives
   every link a 1px box with a radius that the .here rule then COLOURS
   IN. Setting only border-left left the other three sides to show up on
   the current entry as a box drawn around a wrapped label. Reset the
   box, then draw the one edge this component actually wants. */
.w-toc a { display: block; padding: var(--sp-0) var(--sp-2); margin-left: -2px;
  border: 0; border-left: 2px solid transparent; border-radius: 0;
  background: none; text-decoration: none;
  color: var(--muted); font-size: var(--fs-sm); line-height: 1.35; }
.w-toc a:hover { color: var(--fg); }
.w-toc a.sub { padding-left: var(--sp-5); font-size: var(--fs-sm); }
.w-toc a.here { color: var(--accent); border-left-color: var(--accent);
  background: none; font-weight: 600; }
/* The index. */
.w-home { display: flex; flex-wrap: wrap; align-items: baseline;
  gap: var(--sp-1) var(--sp-3); border: 1px solid var(--rule); border-radius: var(--r-2);
  padding: var(--sp-3) var(--sp-4); margin: var(--sp-1) 0 var(--sp-4); background: var(--surface);
  text-decoration: none; color: var(--fg); }
.w-home:hover { border-color: var(--accent); }
.w-home-t { font-weight: 700; font-size: var(--fs-lg); }
.w-home-m { color: var(--muted); font-size: var(--fs-sm); margin-left: auto; }
.w-controls { display: flex; flex-wrap: wrap; align-items: center;
  gap: var(--sp-2); margin: 0 0 var(--sp-4); }
.w-find { display: flex; align-items: center; gap: var(--sp-2);
  border: 1px solid var(--rule); border-radius: var(--r-2); background: var(--surface);
  padding: var(--sp-0) var(--sp-0) var(--sp-0) var(--sp-2); flex: 1 1 16rem; max-width: 22rem; }
.w-find label { color: var(--muted); font-size: var(--fs-md); }
.w-find input { flex: 1 1 auto; min-width: 0; border: 0; background: none;
  color: var(--fg); font: inherit; font-size: var(--fs-body); padding: var(--sp-1) 0; }
.w-find input::placeholder { color: var(--muted); }
.w-find:focus-within { border-color: var(--accent); }
.w-pages { list-style: none; margin: 0; padding: 0; }
@media (min-width: 52rem) { .w-pages { columns: 2; column-gap: var(--sp-6); } }
.w-pages li { break-inside: avoid; border-bottom: 1px solid var(--rule); }
.w-pages a { display: grid; grid-template-columns: 1fr auto auto;
  align-items: baseline; gap: var(--sp-3); padding: var(--sp-2) var(--sp-1);
  text-decoration: none; color: var(--fg); font-size: var(--fs-body); }
.w-pages a:hover { background: var(--surface); }
.w-pages a:hover .w-t { color: var(--accent); }
.w-t { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.w-when { color: var(--muted); font-size: var(--fs-sm); white-space: nowrap; }
.w-revs { color: var(--muted); font-size: var(--fs-xs); background: var(--surface);
  border-radius: var(--r-4); padding: 0 var(--sp-2); min-width: 2.1rem;
  text-align: center; font-variant-numeric: tabular-nums; }
.w-count { margin: var(--sp-4) 0 0; font-size: var(--fs-sm); color: var(--muted); }
/* The source marker is deliberately the quietest thing in the row: a
   reader needs it only when they want to EDIT, and until then it is
   noise between them and the title. */
.w-kind { font-size: var(--fs-xs); text-transform: uppercase; letter-spacing: 0.06em;
  color: var(--muted); border: 1px solid var(--rule); border-radius: var(--r-4);
  padding: 0 var(--sp-1); white-space: nowrap; opacity: 0.75; }
.w-assoc { margin: var(--sp-4) 0 0; font-size: var(--fs-sm); color: var(--muted); }
/* The About sits on the surface tint rather than in a bordered card:
   it is context for what follows, not a peer of it. */
/* The context panel. A rail of hashes down the left with the message
   beside it, and the check-in you are on lifted out of the run — the
   one row that must be findable without reading. */
.ci-ctx { margin: var(--sp-4) 0; }
.ci-ctx h2 { font-size: var(--fs-xs); text-transform: uppercase;
  letter-spacing: 0.07em; color: var(--muted); font-weight: 600;
  margin: 0 0 var(--sp-2); }
.ci-ctx ol { list-style: none; margin: 0; padding: 0; }
.ci-ctx li { display: grid; grid-template-columns: 14px auto auto minmax(0, 1fr);
  align-items: center; gap: var(--sp-2); padding: 0; }
.ci-ctx li.here { background: var(--surface); }
.ci-node { width: 14px; height: 34px; display: block; overflow: visible; }
.ci-ctx-h { text-decoration: none; }
.ci-ctx li.here .ci-ctx-h { font-weight: 600; }
.ci-ctx-m { color: var(--muted); font-size: var(--fs-sm); white-space: nowrap; }
.ci-ctx-c { min-width: 0; overflow: hidden; text-overflow: ellipsis;
  white-space: nowrap; font-size: var(--fs-md); }
/* On a phone the row stacks: hash, then the message, then who and when.
   The drawn rail goes with it — an SVG node cannot stretch to a stacked
   row without distorting its circle, so the rule becomes a border on
   the list, which is the same line by other means. */
@media (max-width: 44rem) {
  .ci-ctx ol { border-left: 2px solid var(--rule); }
  .ci-node { display: none; }
  .ci-ctx li { grid-template-columns: minmax(0, 1fr); gap: var(--sp-0);
    padding: var(--sp-2) var(--sp-3); border-left: 2px solid transparent;
    margin-left: -2px; }
  .ci-ctx li.here { border-left-color: var(--accent); }
  .ci-ctx-c { white-space: normal; }
}
.a-note { background: var(--surface); border-radius: var(--r-2);
  padding: var(--sp-3) var(--sp-4) var(--sp-2); margin: var(--sp-4) 0; }
/* Closed, it is a one-line control rather than a banner with nothing
   in it: the padding that frames prose looks like a mistake around a
   summary on its own. */
.a-note:has(.a-note-d:not([open])) { padding: var(--sp-2) var(--sp-4); }
.a-note-b > :first-child { margin-top: 0; }
.a-note-b > :last-child { margin-bottom: 0; }
/* Prose keeps a readable measure; BLOCK content takes the panel. The
   note is as wide as the page, so capping the whole body at 68ch left a
   terminal transcript clipped mid-word inside a box with room to spare
   — the width was there and nothing was allowed to use it. This is the
   same split the doc pages already make between an article and the
   figures inside it. */
.a-note-b > * { max-width: 68ch; }
.a-note-b > pre, .a-note-b > table, .a-note-b > .tw, .a-note-b > figure,
.a-note-b > svg, .a-note-b > .pikchr, .a-note-b > blockquote { max-width: none; }
/* A note carries arbitrary prose, including a command line longer than
   a phone. Its own scroll container, so the PAGE never moves sideways —
   the same treatment rendered tables already get. */
.a-note-b pre { overflow-x: auto; }
.a-note-b code { overflow-wrap: anywhere; }
.a-note-d > summary { cursor: pointer; font-size: var(--fs-sm); color: var(--muted);
  text-transform: uppercase; letter-spacing: 0.06em; font-weight: 600;
  list-style: none; }
.a-note-d > summary::-webkit-details-marker { display: none; }
.a-note-d > summary::before { content: "\25b8"; display: inline-block;
  margin-right: var(--sp-2); transition: transform 0.12s; }
.a-note-d[open] > summary::before { transform: rotate(90deg); }
.a-note-d > summary:hover { color: var(--fg); }
.a-note-d[open] > summary { margin-bottom: var(--sp-2); }
@media (prefers-reduced-motion: reduce) {
  .a-note-d > summary::before { transition: none; }
}
.a-note-f { margin: var(--sp-2) 0 0; font-size: var(--fs-sm); color: var(--muted); }
.a-note-f a { color: var(--muted); text-decoration: none; }
.a-note-f a:hover { color: var(--accent); text-decoration: underline; }

/* The tracker header is ONE line: which lives are listed on the left,
   how they are drawn and the one action on the right. Stacked, the four
   controls read as four unrelated decisions; in a row they read as the
   axes of a single view, which is what they are. */
.t-head { display: flex; flex-wrap: wrap; align-items: center;
  gap: var(--sp-2) var(--sp-4); margin: var(--sp-1) 0 var(--sp-3); }
.t-tools { display: flex; flex-wrap: wrap; align-items: center;
  gap: var(--sp-2); margin-left: auto; }
/* The filter bar is ONE line. The segmented control is the page's
   navigation and cannot shrink; the action beside it can, so below the
   width where both fit it keeps its icon and drops its words. Letting
   the row wrap instead pushed the action onto a line of its own, which
   reads as a second toolbar. */
.act-icon { display: inline-flex; align-items: center; gap: var(--sp-1); }
.act-icon .ic { flex: none; }
@media (max-width: 30rem) {
  /* The row wraps, but the state filter is always FIRST and whole:
     it is how a reader says what they are looking at. Where a page
     carries more tool groups than fit beside it (the tracker has three
     and a button), those move to the next line instead. A page whose
     tools do fit — the landings list, one icon — still keeps one line. */
  .t-head { flex-wrap: wrap; gap: var(--sp-2) var(--sp-2); }
  .t-head > nav.seg-box:first-child { flex: none; }
  .act-icon .act-label { display: none; }
  .act-icon { padding-left: var(--sp-2); padding-right: var(--sp-2); }
  /* nowrap alone would push the action off the side rather than fit
     it: the control is a hair too wide at 390px once the icon is
     beside it. The choices give up the padding, not the words —
     shortening them would cost the reader more than the whitespace. */
  .t-head nav.seg-box a { padding-left: var(--sp-2); padding-right: var(--sp-2); }
}
/* A segmented control, not a row of loose pills: the choices along one
   axis share a box and butt against each other, so the group reads as
   "pick one of these three" before a single word is read. Loose pills
   read as three unrelated links, which is what they were. */
/* The dividers are GAPS, not borders. A border-right per item is the
   single-row idiom and it breaks the moment the strip wraps: the item
   ending a row keeps a divider that lands against the container edge,
   the last item overall has none, and nothing rules between the rows.
   Measured at 390px this strip is three rows, so wrapping is the normal
   case, not the exception.
   
   Letting the container's colour show through a 1px gap draws every
   divider the layout actually needs — vertical between neighbours,
   horizontal between rows — with no rule knowing where a row ends. */
/* A segmented control is ONE control, so it never breaks mid-way: with
   wrap, a squeezed bar dropped its last choice onto a line of its own
   ("99 All" below "30 Open | 69 Closed"), which reads as a stray button
   rather than as part of the set. It keeps its line; the tools BESIDE
   it are what give way. */
nav.seg-box { display: inline-flex; flex-wrap: nowrap; align-items: stretch;
  gap: var(--sp-0); background: var(--rule);
  border: 1px solid var(--rule); border-radius: var(--r-2); overflow: hidden; }
nav.seg-box a { border: 0; border-radius: 0; margin: 0;
  padding: var(--sp-1) var(--sp-3); background: var(--bg); }
/* These tabs CENTRE their contents. The generic nav-link rule above
   aligns on the baseline, which is right for a link whose label can
   wrap and wrong here, where a tab is one line by construction.
   It matters because an SVG has no baseline: the browser synthesises
   one from the bottom margin edge, so a baseline row stands the icon ON
   the text baseline and it rides high. Measured at exactly -2.00px on
   the ticket, landing and sqlite state tabs -- icon centre against the
   adjacent text's own glyph box -- and 0.00 with this line.
   Stated on the tab rather than on .ic on purpose. The icon cannot know
   whether its row is one line or four: align-self on .ic fixed these
   tabs and then centred the ticket-list status glyph over a four-line
   row, 27px below the title it belongs to. Only the host knows which
   it is, so the host is what says it. */
nav.seg-box a { align-items: center; }
/* Selected reads as the ACCENT, not a grey wash — the same language the
   repobar, explore, doc and fleet tabs already speak with their ink
   underline. A filled --surface pill was wrong twice: the token is one
   value for both themes on purpose (it tints whatever ground it lands
   on), which makes it heavy on the light paper, and hover used the very
   same value, so a hovered tab and the selected one were identical.
   Now --surface means hover and the accent means selected. The rule is
   an inset shadow rather than a border so nothing shifts by 2px when a
   tab becomes current. */
nav.seg-box a { color: var(--muted); white-space: nowrap; flex: none; transition: background-color 120ms ease, color 120ms ease; }
/* Hover is a whisper — a five-percent wash of the text colour — so the
   pointer's tab never competes with the selected one for the eye. */
nav.seg-box a:hover { background: color-mix(in oklab, var(--fg) 5%, transparent); color: var(--fg); }
nav.seg-box a.here { background: var(--code-bg); color: var(--fg); font-weight: 600; }
nav.seg-box a.here:hover { background: var(--code-bg); }
/* The report row: six native selects that read as one control strip.
   A set axis is outlined in accent so "why am I only seeing four
   tickets" is answerable by looking rather than by reading the URL. */
.t-filters { display: flex; flex-wrap: wrap; align-items: center;
  gap: var(--sp-2); margin: 0 0 var(--sp-4); }
/* The query box, sized so a real report fits without scrolling inside
   it: "is:closed subsystem:web crash" is the shape people type. */
/* The query box and the six selects are one strip: one height, one
   radius, padding on the spacing scale. They were 32 and 24 px tall. */
.t-search { display: flex; align-items: center; gap: var(--sp-1);
  box-sizing: border-box; min-height: 2.25rem;
  border: 1px solid var(--rule); border-radius: var(--r-2);
  background: var(--surface); padding: 0 var(--sp-2) 0 var(--sp-3);
  margin: 0 0 var(--sp-2); max-width: 34rem; }
.t-search-label { color: var(--muted); font-size: var(--fs-md); }
.t-search input { flex: 1 1 auto; min-width: 0; border: 0; background: none;
  color: var(--fg); font: inherit; font-size: var(--fs-body); padding: var(--sp-1) 0; }
.t-search input::placeholder { color: var(--muted); }
.t-search:focus-within { border-color: var(--accent); }
.t-filter { display: inline-flex; align-items: center; gap: var(--sp-1);
  box-sizing: border-box; min-height: 2.25rem;
  border: 1px solid var(--rule); border-radius: var(--r-2);
  padding: 0 var(--sp-3); background: var(--surface);
  font-size: var(--fs-md); color: var(--muted); }
.t-filter > span { text-transform: lowercase; }
.t-filter select { border: 0; background: none; color: var(--fg);
  font: inherit; font-size: var(--fs-md); padding: 0; max-width: 11rem; }
.t-filter.on { border-color: var(--accent); color: var(--accent); }
.t-filter.on select { color: var(--accent); font-weight: 600; }
.t-clear { font-size: var(--fs-md); }
/* The dense scan. Rows are for COMPARING here, not reading, so the
   type scale drops and the columns stay narrow enough that an eye can
   run down one without tracking sideways. */
table.t-table { width: 100%; border-collapse: collapse; font-size: var(--fs-body); }
table.t-table th { text-align: left; font-weight: 600; white-space: nowrap;
  border-bottom: 1px solid var(--rule); }
table.t-table th a { text-decoration: none; color: var(--muted);
  display: inline-block; padding: var(--sp-2) var(--sp-2); }
table.t-table th a:hover { color: var(--fg); }
table.t-table td { padding: var(--sp-2) var(--sp-2); border-bottom: 1px solid var(--code-bg);
  white-space: nowrap; }
table.t-table tr:hover td { background: var(--surface); }
table.t-table .ord { color: var(--accent); }
table.t-table td.c-title { white-space: normal; min-width: 18rem; }
table.t-table td.c-title a { color: var(--fg); text-decoration: none; font-weight: 600; }
table.t-table td.c-title a:hover { color: var(--accent); text-decoration: underline; }
/* The id is a link because fossil readers reach for it, but the title
   beside it already goes to the same place: underlining both draws two
   lines per row and neither means anything the other does not. */
table.t-table td.c-id a { text-decoration: none; color: var(--muted); }
table.t-table td.c-id a:hover { color: var(--accent); text-decoration: underline; }
table.t-table td.c-id { white-space: nowrap; }
/* Rail beside listing on wide screens, listing alone on narrow ones.
   The rail is dropped rather than stacked on a phone: stacked, a dozen
   browse links push the tickets themselves below the fold, and the
   dropdowns above already reach every value it offers. */
.t-body { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--sp-4); }
@media (min-width: 64rem) {
  .t-body { grid-template-columns: 12rem minmax(0, 1fr); }
}
.t-rail { display: none; }
@media (min-width: 64rem) { .t-rail { display: block; } }
.t-rail-sec + .t-rail-sec { margin-top: var(--sp-4); }
.t-rail h2 { font-size: var(--fs-xs); text-transform: uppercase;
  letter-spacing: 0.06em; color: var(--muted); margin: 0 0 var(--sp-1);
  font-weight: 600; }
.t-rail ul { list-style: none; margin: 0; padding: 0; }
.t-rail a { display: flex; align-items: baseline; gap: var(--sp-2);
  padding: var(--sp-1) var(--sp-2); border: 0; border-radius: var(--r-row);
  text-decoration: none; color: var(--fg); font-size: var(--fs-md); }
.t-rail a:hover { background: var(--surface); border-color: transparent; }
.t-rail a.here { background: var(--surface); border-color: transparent;
  color: var(--fg); font-weight: 600; }
.t-rail .t-rail-name { flex: 1 1 auto; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.t-rail .n { color: var(--muted); font-size: var(--fs-sm); }
/* The numbered pager. Page numbers sit close together as one run so the
   eye reads them as a scale; the two step controls stand apart from it,
   and the position line sits apart again because it is a statement
   rather than a control. */
.t-pager { display: flex; align-items: center; justify-content: center;
  flex-wrap: wrap; gap: var(--sp-1); margin: var(--sp-4) 0 var(--sp-1); font-size: var(--fs-md); }
.t-pager a { text-decoration: none; padding: var(--sp-1) var(--sp-2);
  border: 1px solid transparent; border-radius: var(--r-row); color: var(--fg); }
.t-pager a:hover { background: var(--surface); border-color: var(--rule); }
.t-pager > span { color: var(--muted); padding: var(--sp-1) var(--sp-2); }
.t-page-step { margin: 0 var(--sp-2); }
a.t-page-step { border-color: var(--rule); }
/* One shape for a page number, selected or not, and one way of
   centring it.

   The pager is a nav, and a global "nav a { display: inline-flex }"
   therefore applies to every page link. text-align does nothing to a
   flex container's items, so the digit sat at the flex start — hard
   left — while the CURRENT page, a span, stayed display:block and
   centred properly. Measured live: the anchors had 10.6px to the left
   of the glyph and 33.9px to the right.
   
   The boxes differed too — 0.5rem of padding and no border on the span
   against 0.6rem plus a 1px border on the anchors, 48px versus 53.2px
   for the same digit — so the highlight also changed size as the
   pointer moved. Both are settled here, for both elements. */
.t-page-n { min-width: 2rem; text-align: center;
  font-variant-numeric: tabular-nums; }
.t-pager .t-page-n { display: flex; align-items: center; justify-content: center;
  padding: var(--sp-1) var(--sp-2); border: 1px solid transparent; }
/* Where you are is a statement, not a link — it goes nowhere, so it is
   not shaped like something that would. */
.t-page-n.here { background: var(--accent); color: var(--bg);
  border-radius: var(--r-row); font-weight: 600; }
.t-page-gap { padding: var(--sp-1) var(--sp-1); }
.t-pager .t-pager-at { font-variant-numeric: tabular-nums;
  margin-left: var(--sp-3); }
/* Narrow: the count drops to its own line and the controls keep theirs.
   The row already wraps, but justify-content centres each wrapped line
   independently, so whatever spilled — usually "older" — landed centred
   under the numbers beside the count, reading as if it belonged to them.
   The count is a statement ABOUT the list, not a control in it, so it is
   the part that leaves. */
@media (max-width: 34rem) {
  .t-pager .t-pager-at { flex-basis: 100%; margin-left: 0; text-align: center; }
  /* And the controls are trimmed to fit one row rather than spilling a
     lone "older" onto its own. Measured at 390px: the row wanted 398px
     of a 358px container, and most of that was the number cells — 2rem
     of min-width plus padding on each. */
  .t-page-step { margin: 0 var(--sp-0); }
  .t-pager a { padding: var(--sp-1) var(--sp-1); }
  /* Same selector as the rule above, or it loses on specificity and the
     numbers keep their wide padding while everything else shrinks. */
  .t-pager .t-page-n { padding: var(--sp-1) var(--sp-1); min-width: 1.5rem; }
}
/* The local tier's marks: the chip that leads a queue row, and the
   icon on a local ticket's title. */
.chip.c-local { border-color: var(--accent); color: var(--accent); }
.t-local { color: var(--accent); border-color: var(--accent); }
.t-local .ic { width: 0.8em; height: 0.8em; vertical-align: 0.05em; }
/* The state under a ticket's title: a filled pill, so open and closed
   differ in shape and word as well as hue, from the tokens both themes
   already contrast-check (the listing's own state colours). */
.t-state { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2); margin: var(--sp-2) 0 var(--sp-4); }
.t-pill { display: inline-flex; align-items: center; gap: var(--sp-1); padding: var(--sp-1) var(--sp-3);
  border-radius: var(--r-4); font-size: var(--fs-sm); font-weight: 600; line-height: 1.2; color: var(--bg); }
.t-pill .ic { width: 1em; height: 1em; }
.t-pill-open { background: var(--add); }
.t-pill-review { background: var(--accent); }
.t-pill-closed { background: var(--muted); }
/* A ticket named in prose (markup.TicketDescriber): its number, then
   its state as a small pill in the same colours as the ticket's own
   page, so a list of references says which are done at a glance. */
a.tref { font-variant-numeric: tabular-nums; white-space: nowrap; }
.tref-state, .tref-local { display: inline-block; padding: 0 var(--sp-2); border-radius: var(--r-4);
  font-size: var(--fs-xs); font-weight: 600; line-height: 1.5; vertical-align: 0.08em; white-space: nowrap; }
.tref-open { background: var(--add); color: var(--bg); }
.tref-review { background: var(--accent); color: var(--bg); }
.tref-closed { background: var(--muted); color: var(--bg); }
.tref-local { border: 1px solid var(--accent); color: var(--accent); }
/* Trunk's CI failed: a dot on the Timeline tab, in the colour the
   failure badges already use. */
.tab-dot { display: inline-block; width: 0.5em; height: 0.5em; margin-left: var(--sp-1);
  border-radius: var(--r-4); background: var(--del); vertical-align: 0.15em; }
.tip-ci { text-decoration: none; }
/* Promote and Discard: each a small summary that opens one line saying
   what it will do and the button that does it — a confirmation that
   needs no script and works the same on a phone. */
.t-tier-acts { display: flex; flex-wrap: wrap; gap: var(--sp-2); margin-top: var(--sp-2); }
.t-confirm > summary { list-style: none; display: inline-flex; align-items: center;
  padding: var(--sp-0) var(--sp-2); border: 1px solid var(--rule); border-radius: var(--r-row);
  font-size: var(--fs-sm); color: var(--fg); cursor: pointer; }
.t-confirm > summary::-webkit-details-marker { display: none; }
.t-confirm[open] > summary { border-color: var(--accent); color: var(--accent); }
.t-confirm[open] { flex-basis: 100%; }
.t-confirm p { margin: var(--sp-2) 0; font-size: var(--fs-sm); color: var(--muted); }
/* The honeypot is for robots only: off-canvas, unfocusable. */
.hp { position: absolute; left: -9999px; width: 1px; height: 1px;
  overflow: hidden; }

/* A row is a GRID — glyph gutter, the ticket, conversation count pinned
   right — not a paragraph. Inline spans with no box are just more
   words, which is how the type and subsystem chips ran into the title
   and into each other ("Code_Defectci"). */
/* THE LIST COMPONENT. One CARD with hairlines inside it, the shape
   every forge converged on: a bordered block says "this is the list"
   and its last row is visibly the last, where a stack of underlined
   paragraphs trails off into the footer.

   It was the ticket listing's shape; every row list in the app now
   wears it, so a list is recognisable as a list wherever you meet one.
   The card owns the border, the radius and the hairlines. Rows own
   their own layout and vertical rhythm — they differ, and should — but
   take their horizontal padding from here so no row's text touches the
   card edge.

   Not for lists that are not rows: a stack of separate cards
   (.ops-reqs) and the inline link lists (.w-links, .l-paths,
   .tkt-refs) are other shapes on purpose. */
.list-card { list-style: none; padding: 0;
  border: 1px solid var(--rule); border-radius: var(--r-2); overflow: hidden; }
.list-card > li { border-bottom: 1px solid var(--rule); }
.list-card > li:last-child { border-bottom: 0; }
ul.tickets { margin: var(--sp-1) 0 0; }
li.t-row { display: grid; grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: baseline; column-gap: var(--sp-2); padding: var(--sp-3) var(--sp-4); }
li.t-row:hover { background: var(--surface); }
/* An SVG's baseline is its BOTTOM edge, so a round glyph baseline-aligned
   against text floats above the title's optical centre (measured: 2.4px
   at the 16/24 body scale). Nudged, not re-aligned, so a title that wraps
   still keeps its glyph on the first line. */
.t-glyph { width: 1.05em; height: 1.05em; position: relative; top: 0.15em; }
/* State by hue, spending only tokens both themes already contrast-check:
   a fifth colour for one glyph would owe light and dark a new pair. */
.g-open { color: var(--add); }
.g-review { color: var(--accent); }
.g-done { color: var(--muted); }
.t-main { min-width: 0; display: flex; flex-wrap: wrap;
  align-items: baseline; gap: var(--sp-1) var(--sp-2); }
a.t-title { font-weight: 600; color: var(--fg); text-decoration: none; }
a.t-title:hover { color: var(--accent); text-decoration: underline; }
.t-sub { flex-basis: 100%; font-size: var(--fs-md); color: var(--muted); }
/* A chip earns its box only when it DISTINGUISHES the row (see
   ticketRows): type and subsystem read as labels, severity and priority
   speak only when they are shouting. */
.chip { font-size: var(--fs-xs); line-height: 1.6; padding: 0 var(--sp-2);
  border-radius: var(--r-4); border: 1px solid var(--rule);
  background: var(--surface); color: var(--muted); white-space: nowrap;
  /* Centred on the flex line rather than sat on the title's baseline: a
     chip's box is taller than its text, so baseline alignment hangs it
     low. align-self needs no magic number to say "middle of the line". */
  align-self: center; }
.c-type { color: var(--fg); }
/* The tracker a row came from, on the cross-repository listing only.
   Quieter than a chip and a link in its own right: it is provenance
   first, and a way to narrow to that tracker second. */
a.t-repo { font-size: var(--fs-xs); color: var(--muted); text-decoration: none;
  align-self: center; white-space: nowrap; }
a.t-repo:hover { color: var(--accent); text-decoration: underline; }
.c-hot { color: var(--del); border-color: var(--del); font-weight: 600; }
/* The row's trailing cell: the conversation count and the state mark
   read as one group at the end of the row, and stay ONE grid child so
   neither can displace the other onto a line of its own. */
.t-tail { display: flex; align-items: baseline; gap: var(--sp-2); }
/* Referenced-by rows read like the tracker's own list: a state glyph
   in the gutter, the name as the click target, the branch pair after. */
.tkt-refs { list-style: none; padding: 0; margin: 0 0 var(--sp-4); }
.tkt-refs li { display: flex; align-items: baseline; gap: var(--sp-2);
  padding: var(--sp-1) 0; border-bottom: 1px solid var(--rule); }
.tkt-refs li:last-child { border-bottom: 0; }
.tkt-refs .ic { flex: none; }
/* A verdict a reader can follow. The badge kept its own colours; the
   link only makes it clickable, because "fail" with nowhere to go is
   the state this page was in. */
a.ci-link { text-decoration: none; border: 0; }
a.ci-link:hover .tl-badge { border-color: var(--fg); }
/* The exceptions surface. Rows are collapsed to their signature line
   and open onto the trace, because the list is for scanning and the
   trace is for one row at a time. */
/* An exception row on a phone: the summary line WRAPS, so the
   signature, surface badge and message share the width that exists
   instead of forcing the page sideways. The message may be a whole
   zit stderr line — anywhere-wrap or it decides the layout. */
/* An issue row, Sentry's shape: a severity mark, then what broke in
   the page's own voice, then where and how often underneath, then the
   count on the right. The card and its hairlines come from
   .list-card, so this only draws what is particular to an issue. */
/* Loud on purpose. This is the one thing on the page that means the
   forge is not doing its job. */
.dep-down { border: 1px solid var(--danger); border-left-width: 3px;
  border-radius: var(--r-2); padding: var(--sp-2) var(--sp-4); margin: 0 0 var(--sp-4);
  background: color-mix(in srgb, var(--danger) 8%, transparent); }
.dep-down p { margin: var(--sp-0) 0; }
.dep-since { color: var(--muted); font-size: 0.9em; }
/* A running step must not look like a queued one. The glyph turns, so
   "something is happening" is legible without reading a word — and it
   stops turning for anyone who asked the system for less motion. */
.runstep .d-ic.run { color: var(--accent); animation: ci-spin 1.4s linear infinite; }
@keyframes ci-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .runstep .d-ic.run { animation: none; } }
.runstep .d-ic.wait { opacity: 0.55; }
/* Stalled is not running and not failed: nothing is wrong with the
   code, nothing is happening either. */
.tl-badge.ci-stalled { color: var(--warn); border-color: var(--warn); }
.exc-list { margin: var(--sp-1) 0 0; }
.exc-item { padding: 0; }
details.exc { padding: 0; }
.exc summary { cursor: pointer; display: grid; list-style: none;
  grid-template-columns: auto minmax(0, 1fr) auto; align-items: start;
  gap: var(--sp-2); padding: var(--sp-3) var(--sp-4); }
.exc summary::-webkit-details-marker { display: none; }
.exc summary:hover { background: var(--surface); }
.exc-lvl { width: 0.5rem; height: 0.5rem; border-radius: 50%;
  margin-top: var(--sp-2); flex: none; }
/* Two levels, because there are two: something threw and nobody caught
   it, or something returned an error a page had to give up on. */
.exc-fatal { background: var(--danger); }
.exc-error { background: var(--accent); }
.exc-head { min-width: 0; }
.exc-title { display: block; font-weight: 600; overflow-wrap: anywhere; }
.exc-meta { display: block; margin-top: var(--sp-0); color: var(--muted);
  font-size: var(--fs-md); overflow-wrap: anywhere; }
.exc-count { color: var(--muted); font-variant-numeric: tabular-nums;
  background: var(--code-bg); border-radius: var(--r-4); padding: 0 var(--sp-2);
  font-size: var(--fs-sm); }
.exc .d-line { display: flex; flex-wrap: wrap; align-items: baseline;
  gap: var(--sp-1) var(--sp-2); min-width: 0; }
.exc .d-line strong { overflow-wrap: anywhere; }
.exc .d-sub { display: block; margin-top: var(--sp-0); }
.exc summary { cursor: pointer; }
.exc-sig { color: var(--muted); font-size: 0.85em; }
.exc-repo { font-size: 0.85em; }
.exc-body { padding: var(--sp-2) var(--sp-3); border-top: 1px solid var(--rule); }
/* What it costs, on the summary line: a reader who does not know what
   a lane is still learns what is not happening. */
.exc-why { display: block; margin-top: var(--sp-0); color: var(--fg); opacity: 0.85; }
.exc-cost, .exc-fix { margin: var(--sp-1) 0; }
/* The specifics as data, so an operator or an agent reads them without
   parsing a sentence. Two columns on anything but a phone. */
.exc-fields { display: grid; grid-template-columns: max-content 1fr; gap: var(--sp-0) var(--sp-3);
  margin: var(--sp-2) 0; padding: var(--sp-2) var(--sp-2); background: var(--code-bg); border-radius: var(--r-row); }
.exc-fields dt { color: var(--muted); }
.exc-fields dd { margin: 0; overflow-wrap: anywhere; }
/* The shape beside the number: how often lately, against how often
   ever. A burst that ended and a fire that is still burning are the
   same count and different pictures. */
.exc-build { margin-left: var(--sp-2); }
.exc-num { display: flex; align-items: center; gap: var(--sp-2); }
.exc-spark { width: 4.5rem; height: 1.1rem; flex: none; fill: var(--muted); }
.exc-rate { color: var(--fg); }
.exc-msg { margin: 0 0 var(--sp-2); overflow-wrap: anywhere; }
.exc-trace { max-height: 26rem; overflow: auto; font-size: var(--fs-sm);
  background: var(--code-bg); padding: var(--sp-2) var(--sp-3); border-radius: var(--r-row);
  white-space: pre; }
/* A stack trace on a phone. The frames stay unwrapped — a wrapped Go
   trace is unreadable — so the panel scrolls in both axes instead, and
   gives back the side padding the lines need. Salvaged from the
   abandoned fleet-tabs-1, whose navigation went the other way but whose
   reading of this page was right. */
@media (max-width: 40rem) {
  .exc-body { padding: var(--sp-2) var(--sp-1); }
  .exc-trace { font-size: var(--fs-xs); max-height: 18rem; }
}
.t-conv { display: inline-flex; align-items: center; gap: var(--sp-1);
  font-size: var(--fs-md); color: var(--muted); text-decoration: none; }
.t-conv:hover { color: var(--fg); }
.t-none { color: var(--muted); }
/* Open vs resolved by weight, not hue: the palette is four variables and
   a fifth for one badge would owe both themes a contrast check. */
.t-status { font-size: var(--fs-xs); padding: var(--sp-0) var(--sp-2); border-radius: var(--r-4);
  border: 1px solid var(--rule); }
.st-open { color: var(--fg); border-color: var(--fg); font-weight: 600; }
.st-done { color: var(--muted); }
main pre { overflow-wrap: normal; }
/* A quotation had NO rule at all, so it rendered as prose the browser
   happened to indent — indistinguishable from a paragraph someone had
   tabbed. Fossil marks a quote with a rule down its side and a quieter
   voice; the bar carries the author's hue where there is one, so quoting
   a person in a thread looks like that person. */
main blockquote { margin: var(--sp-4) 0; padding: var(--sp-1) 0 var(--sp-1) var(--sp-4);
  border-left: 3px solid oklch(var(--idn-l) var(--uc, var(--idn-c0)) var(--uh, 0) / 0.55);
  color: var(--muted); }
main blockquote > :first-child { margin-top: 0; }
main blockquote > :last-child { margin-bottom: 0; }
main blockquote blockquote { margin-left: var(--sp-1); }
/* RENDERED MARKDOWN. Every surface that prints markup.Render output —
   a doc or wiki page, a ticket body and its comments, a forum post, a
   landing event, the repository README — and nothing else, so the app's
   own tables, headings and code (.dt, .ops-t, .pref-matrix, table.tl,
   chrome headings) keep their own rules. Each rule below repeats the
   same :is() list. */
/* The GFM alignment survives the sanitizer as an align attribute; the
   global th, td left rule would otherwise beat it. */
:is(.w-main article, .tkt-body, .tkt-comment, .f-post-b, .l-ev-body) table :is(th, td)[align="left"] { text-align: left; }
:is(.w-main article, .tkt-body, .tkt-comment, .f-post-b, .l-ev-body) table :is(th, td)[align="center"] { text-align: center; }
:is(.w-main article, .tkt-body, .tkt-comment, .f-post-b, .l-ev-body) table :is(th, td)[align="right"] { text-align: right; }/* An @mention carries the same mark as the person's byline, so someone
   named in prose is recognisably the same person named anywhere else. */
a.uref .idn { width: 1em; height: 1em; vertical-align: -0.15em;
  margin-right: 0.15em; }
/* Templates wrap their tables in .tw, which scrolls. Tables that arrive
   from RENDERED CONTENT — the API doc, a wiki page, a ticket body —
   have no wrapper to put them in, and a wide one pushed the whole page
   sideways (the /docs rules column overflowed a phone by 208px). Making
   such a table its own scroll container needs no wrapper and covers
   every source of them, including markup quarry did not generate. */
main :not(.tw) > table { display: block; overflow-x: auto; max-width: 100%; }
.tw > table { display: table; }
main .dt-wrap > table { display: table; }
/* Except when it is stacking on a phone — see .dt-stack above, which
   has to outrank this to become a block. */
/* The highlight is a palette token like everything else. It was a pair
   of literals with the dark one behind an UNGUARDED media query, so a
   reader who chose light on a dark-mode device got the dark highlight
   under light text: a dark block with dark text on it. Tokens follow
   the three-state rule for free, which is the point of having them. */
mark { background: var(--mark); color: var(--fg); }
/* Attachments (RFC 0016 §Reading). One section shape on four pages, and
   a smaller copy of the same cards inside a forum post. Every colour is
   a palette token, so both themes hold without a second set of rules. */
.attach { margin: var(--sp-5) 0 0; }
.attach-h { display: flex; align-items: baseline; gap: var(--sp-2); }
.attach-h h3 { margin: 0 0 var(--sp-2); }
.attach-h .tools { margin-left: auto; font-size: var(--fs-sm); }
.attach-h .n { color: var(--muted); font-weight: 400; }
.attach-grid { display: grid; gap: var(--sp-3);
  grid-template-columns: repeat(auto-fill, minmax(210px, 1fr)); }
.attach-compact .attach-grid { gap: var(--sp-2);
  grid-template-columns: repeat(auto-fill, minmax(170px, 1fr)); }
.attach-card { border: 1px solid var(--rule); border-radius: var(--r-row);
  overflow: hidden; background: var(--surface); }
.attach-card:hover, .attach-card:focus-within { border-color: var(--muted); }
/* One aspect for every kind, so a grid of mixed files is a grid and not
   a ragged pile. The preview is the anchor: the whole tile is the link. */
/* The whole tile is a link, so it has to be told not to LOOK like one:
   without this the code preview arrives underlined and in the accent
   colour, which reads as eight links rather than as a sample of a file. */
.attach-prev { position: relative; display: block; aspect-ratio: 4 / 3;
  max-width: 100%; overflow: hidden; background: var(--code-bg);
  border-bottom: 1px solid var(--rule);
  color: var(--fg); text-decoration: none; }
.attach-prev :where(span, pre, code, div) { text-decoration: none; }
.attach-prev img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* A picture the markup placed in a body (an attach: reference, a
   phone screenshot) never runs past its column. */
.f-post-b img, .wiki-body img, .tk-body img, .md img { max-width: 100%; height: auto; }
/* A diagram (markup.Figure) shrinks to its column, but never below 60%
   of its drawn width: past that its small labels stop being readable,
   so it scrolls inside its frame instead of the page. The fades are
   the scroll shadows: a page-colored cover scrolls with the diagram
   and hides the shadow at an end it has reached, so an edge only
   shadows when there is more past it. Tapping opens it full size. */
.pikchr-fig { margin: var(--sp-4) 0; max-width: 100%; container-type: inline-size; }
.pikchr-view { max-width: 100%; overflow-x: auto; overscroll-behavior-x: contain;
  background:
    linear-gradient(to right, var(--bg) 40%, transparent) left / 32px 100% no-repeat local,
    linear-gradient(to left, var(--bg) 40%, transparent) right / 32px 100% no-repeat local,
    linear-gradient(to right, color-mix(in srgb, var(--fg) 22%, transparent), transparent) left / 14px 100% no-repeat scroll,
    linear-gradient(to left, color-mix(in srgb, var(--fg) 22%, transparent), transparent) right / 14px 100% no-repeat scroll; }
.pikchr-fig .pikchr-open { display: block; width: max-content; max-width: 100%; color: inherit; cursor: zoom-in; }
.pikchr-view svg { display: block; width: var(--pk-w, auto); max-width: 100%;
  min-width: calc(var(--pk-w, 0px) * 0.6); height: auto; }
.pikchr-close { display: none; }
.pikchr-fig:target { position: fixed; inset: 0; z-index: 60; margin: 0; overflow: auto;
  background: var(--bg); padding: var(--sp-6) var(--sp-4); }
.pikchr-fig:target .pikchr-view { overflow: visible; background: none; }
.pikchr-fig:target .pikchr-open { max-width: none; cursor: auto; margin: auto; }
.pikchr-fig:target .pikchr-view svg { max-width: none; width: max(var(--pk-w, 100%), 100%); min-width: 0; }
.pikchr-fig:target .pikchr-close { display: block; position: fixed; top: var(--sp-3); right: var(--sp-3);
  padding: var(--sp-1) var(--sp-3); border: 1px solid var(--rule); border-radius: var(--r-2);
  background: var(--bg); color: var(--fg); font-size: var(--fs-xl); line-height: 1; text-decoration: none; }
/* The diagram's source, a click away. Two radio buttons choose what
   the frame shows, so it switches with scripts off; they are hidden
   from sight but not from the keyboard, and their labels in the bar
   are the control. Copy needs a script and is hidden without one. */
.pikchr-v { position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none; }
.pikchr-src { display: none; margin: 0; max-width: 100%; overflow-x: auto; padding: var(--sp-3);
  background: var(--code-block); border: 1px solid var(--rule); border-radius: var(--r-2); font-size: var(--fs-sm); }
.pikchr-vs:checked ~ .pikchr-view { display: none; }
.pikchr-vs:checked ~ .pikchr-src { display: block; }
.pikchr-bar { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: nowrap; min-width: 0;
  margin-top: var(--sp-2); padding-top: var(--sp-2); border-top: 1px solid var(--rule);
  font-size: var(--fs-sm); color: var(--muted); }
.pikchr-kind { font-family: var(--font-mono); }
.pikchr-seg { display: inline-flex; flex: none; border: 1px solid var(--rule); border-radius: var(--r-2); overflow: hidden; }
.pikchr-seg label { padding: var(--sp-0) var(--sp-2); cursor: pointer; }
.pikchr-vd:checked ~ .pikchr-bar label[for$="-d"],
.pikchr-vs:checked ~ .pikchr-bar label[for$="-s"] { background: var(--surface); color: var(--fg); font-weight: 500; }
.pikchr-vd:focus-visible ~ .pikchr-bar label[for$="-d"],
.pikchr-vs:focus-visible ~ .pikchr-bar label[for$="-s"] { outline: 2px solid var(--accent); outline-offset: -2px; }
.pikchr-gap { flex: 1; }
.pikchr-act { display: inline-flex; align-items: center; gap: var(--sp-1); flex: none; white-space: nowrap;
  padding: var(--sp-0) var(--sp-2); border: 1px solid transparent; border-radius: var(--r-2);
  background: none; color: var(--fg); font: inherit; cursor: pointer; text-decoration: none; }
.pikchr-act:hover { border-color: var(--rule); background: var(--surface); }
.pikchr-act .ic { width: 0.9rem; height: 0.9rem; }
.pikchr-studio { color: var(--accent); }
.pikchr-copy.done { color: var(--add); }
.pikchr-ic-done, .pikchr-copy-done, .pikchr-copy.done .pikchr-ic-copy, .pikchr-copy.done .pikchr-copy-idle { display: none; }
.pikchr-copy.done .pikchr-ic-done, .pikchr-copy.done .pikchr-copy-done { display: inline; }
@media (scripting: none) { .pikchr-copy { display: none; } }
/* The bar answers to the figure's own width, wherever the figure sits:
   one line at every width. The label drops first, then Copy and the
   studio link keep only their icons (and their accessible names). */
@container (max-width: 36rem) { .pikchr-kind, .pikchr-long { display: none; } }
@container (max-width: 24rem) {
  .pikchr-copy .pikchr-lbl { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }
  .pikchr-bar { gap: var(--sp-1); }
}
@container (max-width: 17rem) {
  .pikchr-studio .pikchr-lbl { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }
}
.attach-card.attach-inline { grid-column: 1 / -1; }
.attach-loading .attach-prev { display: grid; place-items: center; }
.attach-spin { width: 1.4rem; height: 1.4rem; border-radius: 50%; border: 2px solid var(--rule); border-top-color: var(--accent); animation: attach-spin 0.9s linear infinite; }
@keyframes attach-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .attach-spin { animation: none; border-top-color: var(--rule); } }
.attach-look { padding: 0; border: 0; background: none; cursor: zoom-in; line-height: 0; }
.attach-card.attach-inline .attach-info { padding: var(--sp-1) var(--sp-2); }
/* The badge names what the bytes ARE — the sniffed format, or a line
   count. Top-left, out of the way of the picture's subject. */
.attach-kind { position: absolute; top: 0.3rem; left: 0.3rem;
  font: var(--fs-xs)/1.4 var(--font-mono);
  padding: var(--sp-0) var(--sp-1); border-radius: var(--r-1);
  background: var(--bg); color: var(--muted); border: 1px solid var(--rule); }
/* A text preview fades out rather than cutting a line in half: the
   eight lines are a sample, and a hard edge reads as a broken render. */
/* The top padding clears the badge: the badge sits over the preview,
   and without room for it the first line of the file is behind it. */
.attach-code { font-size: var(--fs-xs); line-height: 1.35; padding: var(--sp-5) var(--sp-2) 0;
  overflow: hidden; height: 100%;
  -webkit-mask-image: linear-gradient(to bottom, #000 55%, transparent 100%);
  mask-image: linear-gradient(to bottom, #000 55%, transparent 100%); }
.attach-code pre { margin: 0; background: none; padding: 0; overflow: hidden; }
.attach-ext { position: absolute; inset: 0; display: grid; place-items: center;
  font: 600 var(--fs-lg)/1 var(--font-mono);
  color: var(--muted); letter-spacing: 0.05em; }
/* A media tile is a dark frame with a play mark — no poster frame
   exists without decoding the file, which the forge does not do. */
.attach-card[data-kind="media"] .attach-prev { background: #1d2021; }
.attach-play { position: absolute; inset: 0; display: grid; place-items: center; }
.attach-play::before { content: ""; width: 0; height: 0;
  border-left: 18px solid #f0ece4; border-top: 12px solid transparent;
  border-bottom: 12px solid transparent; opacity: 0.85; }
/* Name and download share one row; the sub line spans both columns
   underneath, in the mono face so sizes line up down the grid. */
.attach-info { display: grid; grid-template-columns: minmax(0, 1fr) auto;
  gap: 0 var(--sp-2); padding: var(--sp-1) var(--sp-2) var(--sp-2); align-items: center; }
.attach-name { min-width: 0; overflow: hidden; text-overflow: ellipsis;
  white-space: nowrap; font-size: var(--fs-sm); }
.attach-missing { opacity: .6; cursor: default; }
.attach-dl { color: var(--muted); display: inline-flex; }
.attach-dl:hover { color: var(--accent); }
.attach-sub { grid-column: 1 / -1; color: var(--muted);
  font: var(--fs-xs)/1.4 var(--font-mono);
  font-variant-numeric: tabular-nums;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.attach-sub .idn { width: 0.9em; height: 0.9em; vertical-align: -0.1em; }
/* Phase 3: a held upload is a link and never a picture, so its tile is
   the placeholder with the reason over it; the buttons a holder of
   the letters sees sit under the sub line, small and quiet. */
.attach-held { cursor: pointer; }
.attach-card[data-held="true"] .attach-prev { background: repeating-linear-gradient(
  -45deg, var(--code-bg), var(--code-bg) 6px, var(--surface) 6px, var(--surface) 12px); }
.attach-act { grid-column: 1 / -1; display: flex; gap: var(--sp-2); margin-top: var(--sp-1); }
.attach-inline-form { display: inline; }
.attach-act button { font-size: var(--fs-xs); padding: var(--sp-0) var(--sp-2); }
.attach-form { display: flex; flex-wrap: wrap; align-items: center; gap: var(--sp-2);
  margin: var(--sp-3) 0 0; font-size: var(--fs-md); }
.attach-compact .attach-form { margin-top: var(--sp-2); font-size: var(--fs-sm); }
.attach-pick input[type="file"] { max-width: 16rem; }
.attach-progress { width: 10rem; }
.attach-error { color: var(--danger); }
.attach-limit { color: var(--muted); }
.attach-unbound { margin: var(--sp-3) 0 0; color: var(--muted); font-size: var(--fs-md); }
/* A drop target while a file is over it (attach.js): the section, or
   the composer a paste lands in. A dashed rule in the sheet's own
   colour, offset so it frames rather than crowds — quiet, and gone the
   moment the file leaves. */
[data-drop="over"] { outline: 2px dashed var(--muted); outline-offset: 4px; border-radius: var(--r-2); }
/* The composer's pending list: what was pasted while writing — a
   thumbnail for a picture, the name and size for anything — each with
   a way to take it back before the post. A row the bucket refused at
   submit is marked so the error line's name has a face. */
.attach-composer { padding: var(--sp-2) var(--sp-2); border-top: 1px solid var(--rule); font-size: var(--fs-sm); color: var(--muted); }
/* :not([hidden]) — a bare display rule outranks the attribute, and the
   empty bar showed under every composer before any upload began. */
.attach-composer .attach-progress:not([hidden]) { display: block; margin: var(--sp-1) 0; }
.attach-choose { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; margin: var(--sp-1) 0 0; }
.attach-choose .attach-pick { display: inline-flex; align-items: center; gap: var(--sp-1); cursor: pointer;
  border: 1px solid var(--rule); border-radius: var(--r-row); padding: var(--sp-1) var(--sp-2);
  font-size: var(--fs-sm); color: var(--fg); }
.attach-choose .attach-pick:hover { border-color: var(--accent); color: var(--accent); }
.attach-choose .attach-pick input { position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none; }
.attach-choose .attach-pick .ic { width: 0.85rem; height: 0.85rem; }
.attach-choose .attach-hint { color: var(--muted); }
.attach-pending { list-style: none; margin: 0; padding: 0; }
.attach-pending:empty { display: none; }
.attach-pending li { display: flex; align-items: center; gap: var(--sp-2); margin: var(--sp-0) 0; color: var(--fg); }
.attach-pending .attach-size { color: var(--muted); }
.attach-pending button { font-size: var(--fs-xs); padding: var(--sp-0) var(--sp-2); }
.attach-pending .attach-thumb { width: 4.5rem; height: 4.5rem; object-fit: cover; border-radius: var(--r-row); border: 1px solid var(--rule); }
.attach-pending li[data-state="failed"] .attach-name { color: var(--danger); }
.attach-held-list li { margin: var(--sp-1) 0; }
/* The lightbox. Presentation only: with no script the same images are
   plain links to the inline view, which is what this dialog shows. */
.attach-lightbox { border: none; padding: 0; max-width: 94vw; max-height: 94vh;
  min-width: min(360px, 92vw);
  background: var(--bg); color: var(--fg); border-radius: var(--r-2); }
.attach-lightbox::backdrop { background: #000000c4; }
/* A tiny image must not collapse the dialog to its own bar: the frame
   keeps a floor and centres whatever lands in it. */
.attach-lightbox figure { margin: 0; display: grid; place-items: center;
  min-height: 140px; background: var(--code-bg); }
.attach-lightbox img { display: block; max-width: 94vw; max-height: 80vh;
  object-fit: contain; }
.attach-lb-bar { display: flex; align-items: center; gap: var(--sp-2);
  padding: var(--sp-2) var(--sp-2); border-top: 1px solid var(--rule); }
.attach-lb-meta { flex: 1; min-width: 0; overflow: hidden;
  text-overflow: ellipsis; white-space: nowrap; font-size: var(--fs-sm); }
.attach-lb-sub { color: var(--muted);
  font: var(--fs-xs)/1.4 var(--font-mono); }
.attach-lb-bar button { background: none; border: 1px solid transparent;
  color: var(--fg); cursor: pointer; font-size: var(--fs-lg); line-height: 1;
  padding: var(--sp-0) var(--sp-2); border-radius: var(--r-1); }
.attach-lb-bar button:hover { border-color: var(--rule); }
.attach-lightbox img { transition: opacity 120ms ease; }
@media (prefers-reduced-motion: reduce) {
  .attach-lightbox img { transition: none; }
}
/* The text viewer: a whole attachment, numbered and highlighted. The
   gutter classes are chroma's own; only the frame is ours. */
.atx-h { font-size: var(--fs-lg); word-break: break-all; }
.atx { border: 1px solid var(--rule); border-radius: var(--r-row); overflow-x: auto; }
.atx .chroma { margin: 0; }
.atx pre { margin: 0; }
.atx .lnt { color: var(--muted); user-select: none; }`