/* Generated by scripts/repo-pages.py. Edit the CSS constant there, not this file. */
:root{--bg:#0a0d10;--float:#14151a;--float2:#181a20;--border:#2e2f38;--border2:#24252d;
 --ink:#f9f9fb;--dim:#9b9bb1;--dimmer:#8a8aa0;--pink:#ff1aff;--pink2:#ff55ff;--blue2:#8585ff;
 --textbody:#d6dae3;--sigglow:#ff2bff;--green:#4fa877;--red:#b96b8e;
 --dip:#000;--pink-soft:rgba(255,26,255,.14);--textstrong:#fff;
 --btnhi:#bc17bc;--btnlo:#980498;--btntext:#ffffff;--btnbd:rgba(255,26,255,.45);
 --grid:#1e1f27;--chartink:#9b9bb1;
 --mono:"JetBrains Mono",ui-monospace,SFMono-Regular,Menlo,monospace;
 --sans:"Inter",-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
 --s1:#bc0bbc;--s2:#583cff;--s3:#ae920d;--s4:#a3530b;--s5:#de639a;--s6:#8f7bff;
 /* Native scrollbars and form controls follow the same three-way theme cascade as the tokens
    above, no-JS fallback included, or the chrome disagrees with the page. */
 color-scheme:dark}
:root[data-theme=light]{color-scheme:light;--bg:#f5f6f9;--float:#fff;--float2:#eef0f5;--border:#dcdde7;--border2:#e8e9f0;
 --ink:#15161c;--dim:#56566a;--dimmer:#6b6b80;--pink:#cc00cc;--pink2:#b81fb8;--blue2:#4a47d6;
 --textbody:#33343c;--sigglow:#ff5cf2;--green:#297a4e;--red:#a8456a;
 --dip:#ffffff;--pink-soft:rgba(204,0,204,.10);--textstrong:#0a0a0e;
 --btnhi:#bc14bc;--btnlo:#9d029d;--btntext:#ffffff;--btnbd:rgba(204,0,204,.4);
 --grid:#e9eaf0;--chartink:#5c5c6e;
 --s1:#ba05ba;--s2:#5739ff;--s3:#ac9008;--s4:#a65611;--s5:#d75c94;--s6:#8d7afe}
@media(prefers-color-scheme:light){:root:not([data-theme=dark]){color-scheme:light;--bg:#f5f6f9;--float:#fff;
 --float2:#eef0f5;--border:#dcdde7;--border2:#e8e9f0;--ink:#15161c;--dim:#56566a;--dimmer:#6b6b80;
 --pink:#cc00cc;--pink2:#b81fb8;--blue2:#4a47d6;--textbody:#33343c;--sigglow:#ff5cf2;
 --green:#297a4e;--red:#a8456a;
 --dip:#ffffff;--pink-soft:rgba(204,0,204,.10);--textstrong:#0a0a0e;
 --btnhi:#bc14bc;--btnlo:#9d029d;--btntext:#ffffff;--btnbd:rgba(204,0,204,.4);
 --grid:#e9eaf0;--chartink:#5c5c6e;
 --s1:#ba05ba;--s2:#5739ff;--s3:#ac9008;--s4:#a65611;--s5:#d75c94;--s6:#8d7afe}}
@font-face{font-family:'Inter';font-style:normal;font-weight:100 900;font-display:optional;
 src:url(/fonts/inter.woff2) format('woff2')}
@font-face{font-family:'JetBrains Mono';font-style:normal;font-weight:100 900;font-display:optional;
 src:url(/fonts/jbmono.woff2) format('woff2')}
*{box-sizing:border-box}
/* [hidden] is a plain display:none, which ANY display declaration outranks: .cmpbar's
   display:flex drew an empty compare bar on every /repos load. Stated once here, because the
   next element given a display gets the same bug silently. */
[hidden]{display:none!important}
:root{accent-color:var(--pink)}
/* The front page's rule, character for character (build-html.py): a tinted selection rather
   than a solid --pink block. If one of the two moves, both move. */
::selection{background:color-mix(in srgb,var(--pink) 30%,transparent);color:var(--textstrong)}
::-moz-selection{background:color-mix(in srgb,var(--pink) 30%,transparent);color:var(--textstrong)}
body{margin:0;padding:0;background:var(--bg);color:var(--textbody);
 font:15px/1.55 var(--sans);-webkit-font-smoothing:antialiased}
/* Every scrolling box, by selector; the reason is in build-html.py's comment on the same rule.
   */
body *{scrollbar-width:thin;scrollbar-color:color-mix(in srgb,var(--dim) 55%,transparent) transparent}
body *::-webkit-scrollbar{width:10px;height:10px}
body *::-webkit-scrollbar-track{background:transparent}
body *::-webkit-scrollbar-thumb{background:color-mix(in srgb,var(--dim) 55%,transparent);border-radius:10px;border:3px solid transparent;background-clip:padding-box}
body *::-webkit-scrollbar-thumb:hover{background:var(--dim);background-clip:padding-box;border:3px solid transparent}
main{max-width:1080px;margin:0 auto;padding:30px 22px 90px}
/* The lockup is sized against the h1 UNDER it, not to a number shared with the front page:
   both are 700-weight lines 20px apart, so at equal size they read as one block and the page
   title stops leading. */
/* Cross-document view transitions. navigation:auto engages only when BOTH documents carry it,
   hence the repeat in all three generators; a browser without it navigates as before, so
   nothing here is load-bearing. The masthead is NAMED, which takes it out of the crossfade and
   moves it between its two positions instead, so a navigation reads as a change of content.
   Names must be unique per document, and .brand appears once. */
@view-transition{navigation:auto}
.brand{view-transition-name:masthead}
/* Sequential, not concurrent: overlapping the two snapshots put both near half opacity at the
   midpoint, showing two whole page bodies superimposed (fixed 2026-08-26). 80ms out, then
   150ms in on an 8px rise, 230ms end to end. */
::view-transition-group(root){animation-duration:230ms}
::view-transition-old(root){animation:vtout 80ms cubic-bezier(.4,0,1,1) both}
::view-transition-new(root){animation:vtin 150ms cubic-bezier(0,0,.2,1) 80ms both}
/* The masthead keeps the plain crossfade: its two snapshots differ only in which nav item
   carries the mark, and that change is worth seeing. */
::view-transition-group(masthead){animation-duration:230ms}
@keyframes vtout{to{opacity:0}}
@keyframes vtin{from{opacity:0;transform:translateY(8px)}}
/* The theme flip holds both snapshots still, paints the NEW palette underneath and fades the
   old one out; crossfading a full-page inversion looks like a fault. Driven by data-vt, which
   assets/recent-theme.js sets for exactly the length of the flip. */
:root[data-vt=theme]::view-transition-old(root),
:root[data-vt=theme]::view-transition-new(root){mix-blend-mode:normal}
:root[data-vt=theme]::view-transition-new(root){animation:none}
:root[data-vt=theme]::view-transition-old(root){z-index:1;animation:vtfade 260ms linear both}
/* The named masthead is its own group, so without this it holds the OLD palette through the
   flip. */
:root[data-vt=theme]::view-transition-group(masthead){animation:none}
:root[data-vt=theme]::view-transition-old(masthead){animation:vtfade 260ms linear both}
:root[data-vt=theme]::view-transition-new(masthead){animation:none}
@keyframes vtfade{to{opacity:0}}
/* A reader who asked for less motion gets the destination and none of the above. */
@media (prefers-reduced-motion:reduce){
 ::view-transition-old(root),::view-transition-new(root),
 ::view-transition-group(root),::view-transition-group(masthead){animation:none}
}
.brand{display:flex;align-items:center;gap:9px;font-size:clamp(17px,3.6vw,20px);font-weight:700;
 letter-spacing:-.4px;color:var(--ink);margin-bottom:26px;flex-wrap:wrap}
/* The whole mark is ONE link home, not a logo and a wordmark with two destinations, and its
   accessible name is the visible text rather than an aria-label repeating it. The front page
   keeps its two links: "back home" means nothing when you are already there. */
/* margin-right:auto, NOT flex:1: on an <a>, flex:1 made the whole header row a click target
   and lit the mark from the far right. The auto margin still pushes the theme button to the
   edge. */
.lockup{display:inline-flex;align-items:center;gap:9px;min-width:0}
/* The site nav lives in the masthead because a reader who arrived on a shared link to one
   repository has no history stack and nothing else names the other destinations. Narrow: the
   nav wraps to its own full-width row (order:4) and the auto margin moves onto the lockup. Its
   own font-size and letter-spacing because .brand sets 20px/700/-.4px for the wordmark. */
/* margin-left: the gap to the lockup must beat the 16px gap between nav items, or the first
   item reads as part of the wordmark. The total must MATCH build-html.py's, which reaches it
   from a 10px grid gap where this is a 9px flex row, hence the +1px; .brand carries
   view-transition-name:masthead, so any width where the totals disagree slides the nav
   sideways mid-transition. Measured equal at 640, 762, 900 and 1280. */
.nv{display:flex;gap:16px;font-size:13px;font-weight:600;letter-spacing:0;min-width:0;
 flex-wrap:wrap;margin-right:auto;margin-left:clamp(13px,calc(2vw + 1px),19px)}
.nv a{position:relative;color:var(--dim);text-decoration:none;padding:5px 0;
 border-radius:6px;outline-offset:3px;transition:color .14s}
.nv a:hover{color:var(--ink)}
.nv a:focus-visible{outline:2px solid var(--pink)}
/* The current place is marked twice, in colour and with a rule under it, so it does not rely
   on colour alone. */
.nv a[aria-current]{color:var(--ink)}
.nv a[aria-current]::after{content:"";position:absolute;left:0;right:0;bottom:0;height:2px;
 background:var(--pink)}
@media (max-width:639px){
/* margin-top:2px renders as ~19px of gap, because the lockup's line box already leaves ~10px
   under the wordmark. build-html.py reaches the same 19px from row-gap:12px. The two must
   AGREE, not share a number: .brand is one view-transition group, so a different rendered gap
   slides the nav vertically during every page-to-page transition. Measure before changing
   either. */
 /* Five destinations need 377px on one line and a 375px phone gives the nav 347, so one of
    them wraps, and at gap:14px the wrapped row sat 24px under the others and read as a stray
    heading rather than a nav item. The alternatives all cost more than the row does: a
    nowrap scroll strip cuts "New repos" mid-word 30px short, which reads as a bug and not as
    something to scroll; shortening the label to "New" is ambiguous next to "Repositories";
    and shrinking the type to fit leaves under 3px of slack, which the next platform's font
    metrics take back. 4px puts the rendered gap between the two rows at 14, the same as the
    gap between items, because .nv a carries 5px of padding top and bottom. */
 .nv{order:4;flex-basis:100%;margin:2px 0 0;column-gap:14px;row-gap:4px}
 .lockup{margin-right:auto}
}
.themebtn{display:inline-flex;align-items:center;justify-content:center;width:34px;height:34px;
 border-radius:9px;background:var(--float2);border:1px solid var(--border);color:var(--dim);
 cursor:pointer;transition:.14s;flex:none}
.themebtn:hover{color:var(--ink);border-color:var(--blue2)}
.themebtn svg{width:17px;height:17px;display:none}
:root:not([data-themepref=dark]):not([data-themepref=light]) .themebtn .auto{display:block}
:root[data-themepref=dark] .themebtn .moon{display:block}
:root[data-themepref=light] .themebtn .sun{display:block}
/* no JS means no data-themepref and no working button, so the control is not shown at all */
:root:not([data-themepref]) .themebtn{display:none}
.logo{position:relative;display:inline-block;width:20px;height:20px;border-radius:6px;
 background:url(/logo-stats.png) center/cover no-repeat}
/* Same glow, timing and token as the front page and the feed page. */
.logo::before{content:"";position:absolute;inset:-55% -55%;z-index:-1;border-radius:50%;
 background:radial-gradient(ellipse at center,color-mix(in srgb,var(--sigglow) 70%,transparent),transparent 70%);
 filter:blur(7px);opacity:0;transition:opacity .55s ease;pointer-events:none;will-change:opacity,transform}
.lockup:hover .logo::before{opacity:1;animation:sigsway 4.5s ease-in-out infinite}
@keyframes sigsway{0%,100%{transform:translate(-7%,0) scale(1)}50%{transform:translate(7%,-5%) scale(1.1)}}
@media (prefers-reduced-motion:reduce){.lockup:hover .logo::before{animation:none}}
.lockup{text-decoration:none;color:inherit;border-radius:8px;outline-offset:3px;
 transition:opacity .16s}
/* The hover mark is the glow behind the logo and nothing else; the pointer cursor is the
   affordance. */
.lockup:hover{opacity:.94}
.lockup:focus-visible{outline:2px solid var(--pink)}
.brand .at{color:var(--pink)}
.brand .sd{color:var(--dim);font-size:.67em;font-weight:500}
.brand a:hover{color:inherit}
/* Prose links are underlined so they do not rely on colour alone (WCAG 1.4.1), as on the front
   page. UI links opt back out below, one by one. */
a{color:var(--blue2);text-decoration:underline;text-underline-offset:2px}
a:hover{color:var(--pink2)}
.brand a,.btn,.mv,.dumplink,.chip a,.side a,td a,th a{text-decoration:none}
td a:hover,th a:hover,.side a:hover{text-decoration:underline}
/* The page title, on the front page's scale. At a flat 26px it sat 5px above the h2s, so
   nothing said which heading named the page. */
h1{font-size:clamp(26px,4.6vw,33px);font-weight:700;letter-spacing:-.5px;line-height:1.15;
 color:var(--ink);margin:0 0 5px;overflow-wrap:anywhere}
/* An identity page with no alias has only the full did:key: as a name. Sized well below the h1
   clamp, because 56 monospace characters there is three lines on a phone; h1's
   overflow-wrap:anywhere breaks mid-key rather than pushing the layout wide. */
h1 code.idkey{font-size:clamp(15px,2.2vw,19px);font-weight:600;letter-spacing:0;color:var(--ink)}
/* Section heading. Rank comes from the space above it and from weight, NOT from a rule: a
   full-width hairline announced a hard break between sections that are one continuous read.
   With the rule gone the space does the job alone, so 74px above and 8px below, the same ~9:1
   grouping the front page has. */
h2{font-size:19px;font-weight:700;letter-spacing:-.3px;color:var(--ink);margin:46px 0 8px;
 scroll-margin-top:14px}
main>h2{padding-top:28px}
/* the card title, matching the front page's .chartbox .t */
p.sub{color:var(--dim);font-size:13.5px;margin:0 0 16px;overflow-wrap:anywhere}
/* Title, lead and the way OUT of this page on one row. "What IS this repository" is answered
   on app.radicle.at, so that link sits where the reader forms the question rather than last in
   the facts list. Deliberately NOT tinted by classification: the "Classification" row of the
   fact card carries that, in a sentence rather than a colour. The lead's bottom
   margin comes off inside the row, which owns the gap below. */
.hrow{display:flex;flex-wrap:wrap;align-items:flex-start;gap:12px 20px;margin:0 0 16px}
.hmain{flex:1 1 320px;min-width:0}
.hmain p.sub{margin-bottom:0}
/* Shrink 0, or the label wraps before the title gives up a pixel; the title is longer and is
   the one with a word-break rule. */
.hrow>.code{flex:0 0 auto;margin-top:5px}
/* Full width on a phone: the row is a stack there, and a button hugging its label on an
   otherwise empty line reads as an afterthought. */
@media(max-width:559px){.hrow>.code{width:100%;justify-content:center;margin-top:2px}}
p.sub .asof{color:var(--dimmer)}
/* The separator is the literal character and this constant is a RAW string: written as a hex
   CSS escape in a normal Python string, Python read the leading zeros as an octal escape and
   published a NUL byte, which also made grep treat the stylesheet as binary.
   scripts/test-page-assets.py fails the gate if a control character reaches these templates.
   */
p.sub .asof::before{content:"·";margin:0 7px;color:var(--border)}
/* A trailing clause explained by its own title; the dotted underline is the only cue there is
   one to ask for. */
.aside{color:var(--dimmer);text-decoration:underline dotted;text-underline-offset:3px;
 cursor:help}
.aside::before{content:"·";margin:0 7px;color:var(--border);text-decoration:none;
 display:inline-block}
.card{position:relative;background:var(--float);border:1px solid var(--border);border-radius:8px;
 padding:16px 18px;margin-bottom:14px}
/* A card's padding IS its gap. Padding stops margins collapsing through it, so a first child's
   margin-top is added to it and the box comes out lopsided. Said once here rather than per
   child, because every card gains a child eventually. */
.card>:first-child{margin-top:0}
.card>:last-child{margin-bottom:0}
.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(190px,1fr));gap:14px}
/* Keeps the tile count monotonic in the viewport: without this band a 557px screen laid out 3
   columns and a 580px one fell back to 2. 145px is the only floor giving 3 at both edges,
   because the band's NARROWEST grid is 478px at 560, where the desktop padding takes back what
   the phone block left. Both edges were swept on a real viewport, not derived. 560 and not 561
   because Chrome at 2x DPR stops matching (max-width:560px) at exactly 560, leaving one pixel
   served by neither block; where both match, the phone block is later in this file and wins.
   */
@media(min-width:560px) and (max-width:700px){
.grid{grid-template-columns:repeat(auto-fit,minmax(145px,1fr))}
}
/* Five tiles, explicit and EQUAL tracks: auto-fit lands on four, because the card's inner
   width is capped at 998px and five 190px tracks plus gaps want 1,006, which strands the
   fifth. Unequal tracks were tried and the differing gap is visible, so the sentence was
   shortened instead. 960 because "Merged from outside" holds one line from 940 up and breaks
   at 900, and once a label takes two lines the five figures stop sharing a baseline. Below
   that the tiles are 3 up, and 2 up on a phone. */
/* The three-up band, 701 to 959, is the only place the container grows without the tile count
   changing: at 959 three 1fr tracks are 283px for at most 166px of ink, spreading the ragged
   right edges 82px. Capping the track and giving the LEFTOVER to the gap holds the spread at
   20. 200px clears the widest label (166) and the 180px the five-up row gives, so the cap can
   never be what wraps a line. justify-content only bites in this band; elsewhere the leftover
   is zero. */
.grid.five{grid-template-columns:repeat(3,minmax(0,200px));justify-content:space-between}
.grid.idcard{row-gap:26px}
@media(min-width:960px){.grid.five{grid-template-columns:repeat(5,1fr);column-gap:24px}}
@media(max-width:700px){
 /* Capped for the same reason the three-up band is, and it is the same 200px. Below about 480
    the cap is inert: two tracks and a gap no longer fit 200 apiece. */
 .grid.five{grid-template-columns:repeat(2,minmax(0,200px));justify-content:space-between}
}
/* Two lines of label whether or not the label uses them, so that a tile whose label wraps does
   not sit its figure 17px below its neighbour's. Reserved only this far down because only this
   far down does a label wrap: the longest, "Typical seed count", measures 134px, and the two-up
   track stays wider than that until the viewport falls under 338. 360 is the nearest device
   width below that with room to spare; above it the reservation was 17px of empty line under
   every label on a phone, which is what it looked like. 34px is the `lh` fallback for Chrome
   under 109 and Safari under 16.4: .k computes 11px at the body's 1.55 line-height, so two
   lines measure 34.09px. */
@media(max-width:360px){.idcard .k{min-height:34px;min-height:2lh}}
/* A tile is a column with its note pushed to the bottom, so a row of tiles shares three
   horizontals: the top of the labels, the baseline of the figures, the bottom of the notes.
   margin-top:auto rather than a reserved height, so the slack in a short tile falls between
   its figure and its note, where nobody reads a gap. The gap is 24 at five up rather than the
   grid's 14, which is affordable only because the notes are short: the widest renders at 175px
   in a 180px track. */
.idcard .stat{display:flex;flex-direction:column;min-width:0}
.stat .k{font-size:11px;text-transform:uppercase;letter-spacing:.06em;color:var(--dimmer);
 font-weight:600}
/* Mono, like every other figure on this site: the front page's .card .n, the site-wide .num,
   and this page's own .dnum and .mv .d. */
/* No tabular-nums, deliberately, and the same for .dnum and .mv .d below: tabular figures earn
   their loose spacing when numbers stack in a column, and these three sit alone in a tile. */
/* --vbox is the height this font-size actually renders to (25 x the inherited 1.56), declared
   here so it sits beside the size it is derived from. The identity card reserves it, so a
   short "Not ranked" no longer leaves a void under itself when the notes bottom-align. */
.stat .v{font-family:var(--mono);font-size:25px;--vbox:39px;font-weight:600;letter-spacing:-.5px;
 color:var(--textstrong);margin-top:2px;overflow-wrap:anywhere}
.stat .v.unknown{font-size:15px;font-weight:500;color:var(--dim);font-style:italic}
.stat .n{font-size:12px;color:var(--dim);margin-top:3px}
/* Bottom-aligned only where the notes are captions. Scoped, because .stat is shared with
   ~12,500 repository pages, where the note is a sentence continuing out of the figure above it
   and margin-top:auto opened a 38px void between the two. Scoped to the identity CARD and not
   to its five-tile variant: .grid.five is set only when the accepted-patch tile exists, and
   most identity pages have four tiles, so keyed that way the alignment reached the minority
   and a four-tile "Not ranked" sat its caption 16px above the three beside it. */
.idcard .stat .v{min-height:var(--vbox)}
.idcard .stat .n{margin-top:auto;padding-top:3px}
/* The rank as a position rather than as a pair of numbers to divide. --p is set per page; the
   rest is drawn once for all ~16,000 pages. The ticks are listed FIRST so they paint on top of
   the fill, and they mark the quartiles, which is what makes the bar a scale rather than a
   progress meter. They are --chartink, not --border, because they must read against both the
   fill and the empty track in both themes. The fill is the accent at 40% and the marker at
   full strength: at the extremes the marker is the only one of the two with anything left to
   draw. */
.pct{--p:0%;position:relative;height:6px;margin:7px 0 6px;border-radius:3px;
 background:
  repeating-linear-gradient(90deg,color-mix(in srgb,var(--chartink) 45%,transparent) 0 1px,
   transparent 1px 25%),
  linear-gradient(90deg,color-mix(in srgb,var(--pink2) 40%,transparent) 0 var(--p),
   var(--float2) var(--p) 100%)}
.pct::after{content:"";position:absolute;top:-2px;bottom:-2px;left:var(--p);width:2px;
 margin-left:-1px;border-radius:1px;background:var(--pink2)}
/* The top of the scale, drawn as a state. The fill goes to full strength, because a 40% tint
   with no empty track left to contrast against reads as unfilled. The marker goes, because a
   "you are here" line promises a there beyond it. The ticks are cut UNDILUTED in the track's
   own colour: 4.69 in light and 6.62 in dark, against 1.22 and 1.10 for --chartink over the
   full-strength accent. Do not put a color-mix back on them; at the 70% this shipped with for
   one commit it fell to 2.93 in light, under the 3:1 WCAG 1.4.11 asks of a non-text graphic.
   The ticks stay because every other rank bar on the site has them. */
.pct.top{background:
  repeating-linear-gradient(90deg,var(--float2) 0 1px,transparent 1px 25%),
  var(--pink2)}
.pct.top::after{content:none}
/* The compact disclosure under a stat. A <details> and not a title tooltip, because a tooltip
   on a phone never opens; this one opens on a tap, a click and Enter, and its text is in the
   HTML whether or not anyone opens it. Inline and one word: as a block reading "What that
   means" it spent three signals of importance on optional background reading, directly under
   the biggest number on the page. */
.stat .more{display:inline}
.stat .more>summary{display:inline;cursor:pointer;color:var(--dimmer);list-style:none;
 text-decoration:underline dotted;text-underline-offset:2px}
.stat .more>summary::-webkit-details-marker{display:none}
.stat .more>summary:hover{color:var(--pink2)}
.stat .more>summary:focus-visible{outline:2px solid var(--pink);outline-offset:2px;
 border-radius:3px}
.stat .more>p{margin:5px 0 0}
/* The stat row's explanations. The trigger is a mark in the card's top-right corner: it costs
   the figures no row of their own and sits outside their reading order, which a full-width line
   under them did not. What it opens lands in the same card, full width, under a hairline.
   display:contents on the <details> so a closed disclosure leaves no grid row behind, because
   an empty row still pays the 26px row-gap and that showed as a void under the last tile. */
.legend{display:contents}
/* The mark occupies the card's top-right, which is also where a tile label ends, so the room
   for it is bought differently at each width and both purchases are here rather than filed
   next to the breakpoint that triggers them.
   Above 440 the tiles have horizontal slack, so the card widens its right padding until the
   mark falls wholly outside the tracks: the mark occupies 12 to 34 from the card's right edge
   and the padding is 40 or 44, so the last track ends 6 or 10 clear of it, and the tracks each
   give up a few pixels nobody can see. By
   construction rather than by measurement, because the alternative was a clearance that held
   only while the longest label kept rendering at the width it does here. Two blocks, because
   the card's horizontal padding is 14 under 561 and 18 above it, and the reservation adds to
   whichever is in force.
   Narrow, that trade is not available: two up at 375 the longest label measures 134 in a 152
   track, so widening the padding shortens the track and pushes the label onto a second line,
   which puts the figure beside it 17px out of line. There the card reserves a band above the
   tiles and the mark sits in it, 36 clearing the mark's 10 + 22. The band stops at 440 because
   the reservation takes over there; carried further it reads as a forehead on a card that has
   no title to put in it.
   The two used to leave 441 to 959 to nothing, on the reasoning that three up the longest
   label ends 62px short. That is true of the FIVE-tile card. The four-tile one reaches three
   columns at 503, where its third label ran 3px under the mark. Swept every width from 320 to
   1600 on both: the tightest gap from a label's last glyph to the mark is 11px, no label wraps
   above 360, no horizontal overflow. The bands overlap by a pixel at each edge on purpose: met
   exactly, a fractional viewport matched neither and the mark had nothing bought for it.
   .card.idcard, not .idcard: the phone block further down sets .card{padding:13px 14px}, which
   is one class and wins on order, and the band silently did not render at all. */
@media(min-width:441px) and (max-width:561px){
 .card.idcard,.card.aboutcard{--corner:26px;padding-right:calc(14px + var(--corner))}}
@media(min-width:561px){.card.idcard,.card.aboutcard{--corner:26px;padding-right:calc(18px + var(--corner))}}
/* The fact card takes the top band too. Under 561 its list is one column, so the identity's
   own value runs the full width on the line under its label, and that value is a tap-to-copy
   control: the mark's 44px hit area would sit on top of it and swallow the tap. */
@media(max-width:441px){.card.idcard,.card.aboutcard{padding-top:36px}}
.legend>summary{position:absolute;top:10px;right:12px;box-sizing:border-box;width:22px;height:22px;
 display:grid;place-items:center;border:1px solid currentColor;border-radius:50%;
 color:var(--dimmer);font:600 12px/1 var(--sans);cursor:pointer;list-style:none}
.legend>summary::-webkit-details-marker{display:none}
/* The mark is 22px and a thumb wants 44px, so the hit area is grown by a transparent inset
   rather than by the circle, which at 44px would be a button competing with the figures. The
   inset reaches over a corner of the neighbouring tile's label, which costs nothing: the label
   is not a control, and the tooltip it carries is the same sentence this mark reveals. */
.legend>summary::before{content:"";position:absolute;inset:-11px}
.legend>summary:hover,.legend[open]>summary{color:var(--pink2)}
/* Where a pointer exists the mark does not have to announce itself: it is a footnote to the
   figures, and a 22px ring at full strength competes with them. The ring goes transparent
   rather than away, so the box and everything laid out around it are unchanged, and it comes
   back on hover, on focus and while open. The GLYPH is left alone: it is 12px, and dimming it
   as well took it under 4.5:1, which buys very little quiet for the one part that has to stay
   readable. Deliberately not applied under (hover:none): a touch reader can only find this by
   seeing it, and has no cheap way to probe the corner. */
@media(hover:hover){.legend:not([open])>summary{border-color:transparent}
 .legend:not([open])>summary:hover,.legend:not([open])>summary:focus-visible{
  border-color:currentColor}}
.legend>summary:focus-visible{outline:2px solid var(--pink);outline-offset:2px}
/* Whichever of these two is the grid item has to span the card, or the list lands in the one
   free cell beside the last tile. Chrome slots the open content into ::details-content, so the
   <dl> is a block inside that; where the pseudo does not exist the <dl> is the item itself. */
.legend::details-content{grid-column:1/-1}
/* Closed, the slot is still a grid item of zero height, and an empty row costs the full
   26px row-gap: it showed as a void between the last tile and the card's floor. The second
   rule is for engines older than the pseudo (Chrome 131, Safari 18.4, Firefox 139): there the
   <dl> is the grid item, and display:contents has already dropped whatever closed-state hiding
   the UA hangs off the <details> box, so without it the list stands permanently open. */
.legend:not([open])::details-content{display:none}
.legend:not([open])>dl{display:none}
/* The corner is reserved against the tiles, and the list opens below them, where nothing is
   in its way. Without giving it back the list sat 45px from the card's right edge against 19
   on its left, which on a definition list of wrapped sentences reads as a crooked margin. */
.legend dl{grid-column:1/-1;display:grid;grid-template-columns:max-content 1fr;gap:6px 16px;
 margin:0 calc(0px - var(--corner,0px)) 0 0;padding-top:12px;border-top:1px solid var(--border2);
 font-size:12.5px}
.legend dt{color:var(--ink);font-weight:600}
.legend dd{margin:0;color:var(--dim)}
@media(max-width:639px){
 .legend dl{grid-template-columns:1fr;gap:1px}
 .legend dd{margin-bottom:8px}
 .legend dd:last-child{margin-bottom:0}
}
dl.facts{display:grid;grid-template-columns:max-content 1fr;gap:7px 18px;margin:0;font-size:14px}
dl.facts dt{color:var(--dimmer);font-size:12.5px;padding-top:2px}
dl.facts dd{margin:0;color:var(--textbody);overflow-wrap:anywhere}
dl.facts dd.unknown,dl.facts dd.fact.unknown{color:var(--dim);font-style:italic}
/* One delegate: the alias its owner chose, then the key it really is. The alias is display
   text any stranger can set, so it never appears without the key beside it. A wrapping flex
   row rather than two stacked blocks, which cost a line of height per delegate at the widths
   most readers are at. */
.del{display:flex;flex-wrap:wrap;align-items:baseline;gap:1px 9px;margin-bottom:5px}
.del:last-child{margin-bottom:0}
.del b{font-weight:600;color:var(--ink);letter-spacing:-.1px}
/* The alias is a link wherever the node has a page, with the same quiet treatment as .list td
   a: a dim hairline underline that firms up on hover, rather than eight pink delegate names.
   Drawn unconditionally, because an affordance nobody sees until they are already on it is not
   one. Not a DOTTED underline: .aside and .stat .more already spend that on "this carries its
   own explanation in a title". */
a.delto{color:inherit;text-decoration:underline;text-decoration-color:var(--dimmer);
 text-decoration-thickness:1px;text-underline-offset:3px}
a.delto:hover,a.delto:focus-visible{text-decoration-color:var(--pink2)}
a.delto:hover b,a.delto:focus-visible b{color:var(--pink2)}
.del code{font-size:12px;color:var(--dimmer);overflow-wrap:anywhere;min-width:0}
code,pre{font-family:var(--mono);font-size:12.5px}
pre.clone{background:var(--float2);border:1px solid var(--border2);border-radius:6px;
 padding:10px 12px;margin:0;overflow-x:auto}
/* The strings a reader came here to take away: the repository ID, the seed command and every
   delegate's node key. The TEXT ITSELF is the control, because selecting one by hand across a
   wrap fails quietly; there is no button, which would put a bordered control beside every key
   on a page whose subject is those keys. .copyable is ADDED BY assets/repo-copy.js and never
   written into the page: without a clipboard the control cannot work and must not advertise
   itself. The hover fill is a box-shadow spread rather than padding, which would shift the
   line inside pre.clone, and --pink-soft rather than a surface token because the targets sit
   on both --float and --float2. */
.copyable{cursor:copy;border-radius:4px;
 transition:background .12s ease,box-shadow .12s ease,color .12s ease}
.copyable:hover{background:var(--pink-soft);box-shadow:0 0 0 3px var(--pink-soft);
 color:var(--ink)}
.copyable:focus-visible{outline:2px solid var(--pink);outline-offset:3px;border-radius:4px}
/* The confirmation. Same element, same rules and same timing as the front page's, which is
   what copying a chart image already shows; scripts/test-style-parity.py holds the two in
   step. */
#toast{position:fixed;left:50%;bottom:30px;transform:translateX(-50%) translateY(20px);
 background:var(--float2);border:1px solid var(--pink);color:var(--pink2);padding:9px 16px;
 border-radius:10px;font-size:13px;opacity:0;transition:.2s;pointer-events:none;z-index:50}
#toast.show{opacity:1;transform:translateX(-50%) translateY(0)}
#toast.bad{border-color:var(--red);color:var(--red)}
/* The line, and beside it the two figures it runs between; without them the plot said only "it
   went up". line-height is pinned to the font size because the labels are centred on the
   plot's own high and low levels: see PAD in sparkline(). */
.sparkwrap{display:flex;align-items:stretch;gap:11px;margin:0 0 14px}
.spark{display:block;flex:1 1 auto;min-width:0;height:66px;overflow:visible}
.sparkax{flex:none;display:flex;flex-direction:column;justify-content:space-between;
 font-family:var(--mono);font-size:11px;line-height:11px;color:var(--dimmer);
 font-variant-numeric:tabular-nums}
.sparkax.one{justify-content:center}
/* Solid, deliberately: a dash reads as a projection or a threshold, and two dashed rules on a
   66px plot read as a dotted box drawn round it. */
.spark .sparkgrid{stroke:var(--border2);stroke-width:1;vector-effect:non-scaling-stroke}
/* A 2px line is not a hit target. One band per recorded level makes the plot hoverable and
   carries the <title> the browser turns into a tooltip; `transparent` is a paint, so it takes
   pointer events where `none` would not. */
.spark rect{fill:transparent}
.spark rect:hover{fill:color-mix(in srgb,var(--pink) 9%,transparent)}
/* The two deltas are the page's reason to exist. The number leads, the sign carries the
   direction on its own (colour only reinforces it), and the sentence says what it is measured
   against. */
/* min() on the track floor, not a bare 272px: a grid child cannot be narrower than its own
   minimum, so on a 320px phone the single track overflowed its card and scrolled the WHOLE
   document sideways. The front page hit exactly this and documents it. */
.deltas{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(272px,100%),1fr));gap:10px;
 margin-top:15px}
.dchip{display:flex;align-items:center;gap:13px;min-width:0;padding:11px 14px;
 border:1px solid var(--border2);border-radius:11px;background:var(--float2)}
.dnum{font-family:var(--mono);font-size:22px;font-weight:600;line-height:1;white-space:nowrap;
 color:var(--dim)}
.dchip.up .dnum{color:var(--green)}
.dchip.down .dnum{color:var(--red)}
.dbody{min-width:0}
.dper{display:block;font-size:10.5px;font-weight:600;letter-spacing:.06em;text-transform:uppercase;
 color:var(--dimmer)}
/* The two levels the figure is the difference of, in mono, so "14 --> 5" reads as one movement
   rather than two numbers in prose. It replaced a twenty-word sentence beside a chip whose
   point is being readable without reading. */
.dlev{margin:5px 0 0;font-family:var(--mono);font-size:12.5px;line-height:1.35;
 color:var(--textbody);overflow-wrap:anywhere}
/* Quieter and smaller: the date is the last thing anyone needs off this card. */
.dwhen{margin:2px 0 0;font-size:11px;line-height:1.4;color:var(--dim)}
/* Only the unknown arm still sets a sentence, and the italic marks a card with no measurement
   on it rather than a measurement of zero. */
.dsay{margin:3px 0 0;font-size:13px;line-height:1.45;color:var(--textbody)}
.dchip.unknown .dsay{color:var(--dim);font-style:italic}
/* The maintainer's other repositories, in the same table vocabulary as the hub and the compare
   view; .list is what gives the names their resting underline. */
.sibs table{margin:2px 0 0}
/* The site-wide 14px gutter on the outer edges as well as between the columns: .card.list
   drops the card's padding so rows can be full-bleed, but here the card is 360px wide with a
   visible border and a name starting on it reads as a typo. */
.sibs th,.sibs td{padding:7px 14px}
/* `th` is sticky site-wide, which is wrong on a card halfway down a repository page: the
   header would detach and follow the reader down the document. */
.sibs th{position:static;background:none}
.sibs tr:last-child td{border-bottom:none}
/* .card.list drops the card's padding so rows run edge to edge, which is wrong for the prose
   under them; the same 14px the columns take, so it starts under the repository names. */
.sibs .note{margin-top:9px;padding:0 14px 13px}
/* The prose under a HUB's table, which .card.list's edge-to-edge rows leave with no gutter of
   its own. :not(.sibs) because `.card.list>.note` outranks `.sibs .note`, so an unscoped rule
   took over the note on every repository and identity page as well. */
.card.list:not(.sibs)>.note{margin:0;padding:11px 14px 0}
.card.list:not(.sibs)>.note:last-child{padding-bottom:13px}
/* A rule between the last row and the prose about it, on whichever paragraph is first ON
   SCREEN: #empty and #cap are two answers to one question and at most one is ever shown (see
   report() in identities-index.js and repos-index.js), and while a query is live neither is,
   which leaves the alias note first. `:first-of-type` matched NOTHING on either hub and did
   match the single note on the two .sibs cards, drawing the one border those cards
   deliberately remove. */
.card.list>#empty,.card.list>#cap,
.card.list>#empty[hidden]+#cap[hidden]+.note{border-top:1px solid var(--border2)}
.card.list>#cap,
.card.list>#empty[hidden]+#cap[hidden]+.note{padding-top:12px}
.spark polyline{fill:none;stroke:var(--pink2);stroke-width:2;stroke-linejoin:round;
 stroke-linecap:round;vector-effect:non-scaling-stroke}
.note{color:var(--dim);font-size:12.5px;margin:8px 0 0}
.thin{border-color:var(--border);background:var(--float2)}
footer{color:var(--dim);font-size:12.5px;margin-top:26px;border-top:1px solid var(--border2);
 padding-top:14px}
/* hub */
.tools{display:flex;gap:10px 14px;align-items:flex-end;flex-wrap:wrap;margin:0 0 12px}
/* Every input on this site carries a visible label, not just a placeholder, which is gone the
   moment anyone types. Same treatment as .stat .k and .dper. */
.field{display:flex;flex-direction:column;gap:5px;min-width:0}
.field>label{font-size:11px;text-transform:uppercase;letter-spacing:.06em;color:var(--dimmer);
 font-weight:600}
.tools .note{padding-bottom:9px}
/* width, not min-width. `min-width:min(320px,100%)` inside an auto-sized flex column is a
   circular measurement, so 320px never applied and the hub's box shrink-wrapped to 234px with
   its own placeholder clipped. The field below carries the size. */
input[type=search]{font:inherit;font-size:14px;padding:8px 11px;border-radius:7px;
 border:1px solid var(--border);background:var(--float);color:var(--ink);width:100%;min-width:0}
/* The box takes the whole row: "did:key:" plus 48 base58 characters is 56, which measures
   479px in this face, and the 460px cap that used to sit here left a reader who had just
   pasted one looking at the middle of it. Uncapped, because no fixed width suits both a phone
   and a 1,600px window. The match count therefore goes on its own line beneath, where nothing
   it says can move the box: sized to its own text it would eat the box's right edge, and in a
   fixed slot its longest wording wraps, growing the row and lifting the box the reader is
   typing into. It costs no space while empty; the rule further down takes an empty #count out
   of the flow. */
.tools .field{flex:1 1 340px}
.tools #count{flex:1 0 100%;padding-bottom:0;margin-top:0}
input[type=search]:focus-visible{outline:2px solid var(--pink);outline-offset:2px}
/* The list is the page, and a few thousand rows is a few thousand tab stops in front of the
   footer and the compare bar. Clipped rather than display:none, which would take it out of the
   tab order it exists to be in. */
.skip{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);
 white-space:nowrap}
.skip:focus{position:static;width:auto;height:auto;clip-path:none;display:inline-block;
 margin:0 0 9px;padding:8px 14px;border:1px solid var(--border);border-radius:8px;
 background:var(--float2);color:var(--ink);font-size:13px;font-weight:600;
 text-decoration:none;outline:2px solid var(--pink);outline-offset:2px}
#pastlist{display:block;height:0}
table{width:100%;border-collapse:separate;border-spacing:0}
th,td{text-align:left;padding:8px 14px;border-bottom:1px solid var(--border2);vertical-align:top}
th{position:sticky;top:0;z-index:2;background:var(--float);font-size:10.5px;text-transform:uppercase;
 letter-spacing:.06em;color:var(--dimmer);font-weight:600;border-bottom:1px solid var(--border);
 white-space:nowrap}
th:first-child{border-top-left-radius:7px}th:last-child{border-top-right-radius:7px}
th.n,td.n{text-align:right;font-variant-numeric:tabular-nums}
/* A figure never breaks: "194.8 MiB" split over two lines reads as two numbers. Given up
   below 560px, where the alternative is the whole document scrolling sideways. */
td.n{font-family:var(--mono);font-size:12.5px;color:var(--dim);white-space:nowrap}
tbody tr:last-child td{border-bottom:none}
tbody tr:hover{background:var(--float2)}
td a{color:var(--ink);font-weight:600;text-decoration:none;overflow-wrap:anywhere}
tbody tr:hover td a{color:var(--pink2)}
/* The repository name goes somewhere, and the table has to say so at REST. An underline on
   every name read as texture down 1,810 rows: in a column where every cell is a link it
   distinguishes nothing. The rest state is carried instead by the column header, by every cell
   behaving alike, and by `td a`'s weight of 600, a non-colour channel that satisfies WCAG
   1.4.1. The underline returns on hover and keyboard focus, and permanently under
   prefers-contrast or forced colours, where weight and hue are gone. A trailing chevron lost:
   it says "expands", and has to be hidden from a screen reader on every row. Scoped to
   .card.list, so the mixed tables inside a repository page's cards keep their underline; there
   the column uniformity does not hold. */
.list td a:hover,.list td a:focus-visible{text-decoration:underline;
 text-decoration-color:currentColor;text-decoration-thickness:1px;text-underline-offset:3px}
@media (prefers-contrast:more),(forced-colors:active){
 .list td a{text-decoration:underline;text-decoration-color:var(--dimmer);
  text-decoration-thickness:1px;text-underline-offset:3px}}
/* The tap target, grown to the 24px the coarse-pointer rules below already argue for, without
   moving the text a pixel. */
@media (pointer:coarse){.list td a{display:inline-block;padding:6px 0;margin:-6px 0}}
/* An unticked box is a possibility, not content, so it sits back until it is ticked, focused
   or under a pointer that has found the row. Opacity rather than a smaller box (which would
   undercut the 24px coarse-pointer target below) or a colour swap (which native checkbox
   rendering does not honour consistently). Never dimmed in forced colours. */
.pickbox{opacity:.5}
.pickbox:checked,.pickbox:focus-visible,tbody tr:hover .pickbox{opacity:1}
@media (forced-colors:active){.pickbox{opacity:1}}
/* A maintainer alias is a string its owner chose, and without a break opportunity the longest
   one sets the Repository column's min-content width: on a 360px phone it forced a 470px table
   inside a 332px card and pushed Seeds and Size off screen. Same declaration `td a` already
   carries. */
td .by{color:var(--dimmer);font-weight:400;font-size:12.5px;margin-left:7px;
 overflow-wrap:anywhere}
/* The identities hub's first column: a name, then the key fragment that says WHICH holder of
   that name this is. The fragment is dimmer and smaller because it is the qualifier, not the
   subject. overflow-wrap on the name and not the fragment: an alias is arbitrary text whose
   longest unbroken word would otherwise set the column's min-content width and push the figure
   columns off a phone, while the fragment is the same width on every row (IDENT_SHORT_CHARS
   characters plus the ellipsis) and breaking it would split an identifier across two lines.
   tabular-nums so two fragments compare character by character. */
.idname{overflow-wrap:anywhere;min-width:0}
.noname{font-size:13px;color:var(--dim)}
/* The reserved tenths slot: 2ch of the mono face is exactly ".5", so an integer and a half in
   one column put their decimal points on a single vertical line. inline-block because a bare
   span cannot be given a width. */
.frac{display:inline-block;width:2ch;text-align:left}
.idfrag{margin-left:7px;color:var(--dimmer);font-size:12px;font-variant-numeric:tabular-nums;
 white-space:nowrap}
/* The key half of the fragment, never allowed to break: ident_short_html() puts the sole break
   opportunity between the prefix and this, so a folded fragment reads as two whole pieces and
   never as a line opening mid-identifier. Redundant while .idfrag is nowrap, and the whole
   guarantee once the phone rule below lifts that. */
.idkey{white-space:nowrap}
/* One header, two spellings: "Repositories" is the longest label on the identities hub and a
   header cell does not wrap, so on a phone it alone sets that column's width. The pair sits in
   the markup rather than being swapped by script, so the short form is right from the first
   paint and costs nothing at runtime; identities-index.js MOVES a header's children into its
   sort button instead of copying its text, which is what keeps both spellings alive after the
   upgrade. */
.thshort{display:none}
.sortbtn{font:inherit;color:inherit;text-transform:inherit;letter-spacing:inherit;background:none;
 border:0;padding:0;cursor:pointer;display:inline-flex;align-items:center;gap:5px}
.sortbtn:hover{color:var(--pink2)}
.sortbtn:focus-visible{outline:2px solid var(--pink);outline-offset:3px;border-radius:3px}
.car{width:0;height:0;border-left:4px solid transparent;border-right:4px solid transparent;
 opacity:.55}
th[data-sort=none] .car{border-top:4px solid currentColor;border-bottom:0;opacity:.25}
th[data-sort=desc] .car{border-top:4px solid currentColor;border-bottom:0;opacity:1}
th[data-sort=asc] .car{border-bottom:4px solid currentColor;border-top:0;opacity:1}
th[data-sort=desc],th[data-sort=asc]{color:var(--pink2)}
.card.list{padding:0}
#empty,#cmpempty{padding:14px;color:var(--dim);font-size:13px;font-style:italic}
/* The movers strip, in the same vocabulary as a repo page's delta chips (signed number
   leading, glyph carrying the direction, colour only reinforcing). It scrolls sideways on a
   phone. */
.movers{display:grid;grid-template-columns:repeat(auto-fill,minmax(168px,1fr));gap:10px;
 margin:12px 0;padding:2px}
/* Wide enough for all six on one line, but auto-FIT, not a literal six: the strip renders from
   THREE gainers up (build-html.py's own condition) and compare_max may be raised as far as
   twelve, and pinned tracks squeeze three cards into the left half with a hole beside them.
   auto-fit collapses the tracks nothing fills, and 140px still leaves room for a sixth here.
   */
@media(min-width:1001px){.movers{grid-template-columns:repeat(auto-fit,minmax(140px,1fr))}}
@media(max-width:700px){.movers{grid-template-columns:none;grid-auto-flow:column;
 grid-auto-columns:minmax(158px,74%);overflow-x:auto;scroll-snap-type:x proximity;
 overscroll-behavior-x:contain}}
.mv{scroll-snap-align:start;display:block;text-decoration:none;padding:11px 13px;border-radius:11px;
 border:1px solid var(--border2);background:var(--float2);min-width:0;transition:.14s}
.mv:hover{border-color:var(--blue2);background:var(--float)}
.mv .d{font-family:var(--mono);font-size:20px;font-weight:600;line-height:1;color:var(--dim)}
.mv.up .d{color:var(--green)}
.mv.down .d{color:var(--red)}
.mv .nm{display:block;margin-top:6px;font-weight:600;color:var(--ink);font-size:14px;
 white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.mv:hover .nm{color:var(--pink2)}
/* No nowrap, and no ellipsis: they hid this whole line on every viewport for as long as it
   read "typically 29 seeds, was 8". Left able to wrap so no rewording can clip in silence
   again. */
.mv .w{display:block;margin-top:2px;font-size:12px;color:var(--dim)}
/* With the strip taken off the page, its 46px margin plus 28px padding leaves a hole under the
   subtitle that reads as a failed render, so the heading closes up behind it. [hidden] rather
   than a class, so the rule follows the one property the script sets. */
#hubmovers[hidden]+h2{margin-top:20px;padding-top:0}
/* Both ends of the distribution sit under one h2 as h3s, so a reader meets "Biggest movers"
   once and then two labelled rows. Quiet, because the cards under them are the content. */
h3{font-size:13px;font-weight:600;color:var(--dimmer);text-transform:uppercase;
 letter-spacing:.6px;margin:24px 0 8px}
.headrow{display:flex;align-items:baseline;gap:12px;flex-wrap:wrap;margin:46px 0 6px}
.headrow h2{margin:0}
/* .headrow.sub is a heading INSIDE a section, not one that opens a section, so it drops the
   section-opener's 28px padding and 38px margin. NOT scoped `main>`: these were, until the
   movers strip was wrapped in <section id=hubmovers>, at which point both rules stopped
   matching in silence and the base .headrow's 46px margin opened a hole on a phone.
   scripts/test-repo-pages.sh now fails on any main> rule that styles nothing on a built page.
   */
.headrow.sub{padding-top:0;margin:26px 0 8px}
h2+.headrow.sub{margin-top:8px}
.headrow.sub h3{margin:0}
.headrow .side{font-size:12.5px;margin:0}
/* The hub's movement column. A blank cell means "no move past the recording threshold", which
   is not the same as zero and must not be drawn as one. */
td.d{font-family:var(--mono);font-size:12.5px;text-align:right;font-variant-numeric:tabular-nums;
 color:var(--dim)}
/* --green and --red moved for these four selectors and the three like them above. Dark --red
   was #aa5078, 3.42 against the hovered row; light --green was #2f8d5a, 3.63 against the card
   the movers strip sits on. Both carry small text and need 4.5, and 600 weight is not WCAG
   "bold", so the large-text exemption does not apply. The steps here hold 4.6 against the
   worst surface each lands on in its theme. KEEP IN STEP with build-html.py, which declares
   the same two tokens. */
td.d.up{color:var(--green)}
td.d.down{color:var(--red)}
th.d{text-align:right}
/* Compare selection. The cells SHIP with the page (see index_rows) but stay out of the layout
   unless a script ran, because a tick box with no script behind it does nothing. The condition
   is [data-js], which /recent-theme.js sets from the head BEFORE first paint; deciding it
   later from repos-index.js cost a 188ms relayout of ~30,000 nodes on every load. */
[data-js] td.pick,[data-js] th.pick{display:table-cell}
td.pick,th.pick{display:none;width:1%;padding-right:0}
.pickbox{width:16px;height:16px;cursor:pointer;margin:2px 0 0}
tr.sel{background:color-mix(in srgb,var(--pink) 7%,transparent)}
/* #count is a role=status the script writes on a timer, so it is empty until somebody filters,
   and an empty block still took a flex line plus the row gap: 19px of nothing between the
   search box and the table. Taken out of the flow rather than display:none, because a live
   region that is display:none when its text arrives is the shape screen readers are known to
   miss; absolutely positioned it stays in the accessibility tree. */
.tools>#count:empty{position:absolute;width:0;height:0;overflow:hidden}
.cmpbar{position:sticky;bottom:0;z-index:3;display:flex;align-items:center;gap:12px;flex-wrap:wrap;
 margin-top:12px;padding:11px 15px;border:1px solid var(--border);border-radius:11px;
 background:var(--float);box-shadow:0 -6px 22px rgba(0,0,0,.22)}
.cmpbar .lbl{font-size:13px;color:var(--textbody);flex:0 0 auto}
/* What is actually picked, named, in the bar: a count alone left a reader returning to an open
   tab with 1,810 rows to search. These take the flexible space; the count shrinks to a prefix.
   */
.bchips{display:flex;flex-wrap:wrap;gap:6px;flex:1;min-width:120px}
.bchip{display:inline-flex;align-items:center;gap:2px;max-width:100%;
 border:1px solid var(--border2);border-radius:999px;background:var(--float2);
 font-size:12.5px;color:var(--ink);padding:2px 2px 2px 9px}
.bchip>span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.bchip .x{border:0;background:none;color:var(--dim);cursor:pointer;font-size:15px;line-height:1;
 padding:2px 6px;border-radius:999px}
.bchip .x:hover{color:var(--pink2)}
.bchip .x:focus-visible{outline:2px solid var(--pink);outline-offset:1px}
/* The delegate filter's maintainers, one removable chip each, inside the note above the table,
   which otherwise offered all-or-nothing as the only way out. Smaller than .bchip because this
   row sits inside a sentence, and inline-flex because its parent is a <p>. Every value is a
   token the theme blocks already redefine. NOT .dchip, which is the movers strip's delta card.
   */
.mchips{display:inline-flex;flex-wrap:wrap;gap:6px;vertical-align:middle;max-width:100%}
.mchip{display:inline-flex;align-items:center;gap:2px;max-width:100%;
 border:1px solid var(--border2);border-radius:999px;background:var(--float2);
 font-size:12px;color:var(--ink);padding:1px 2px 1px 9px}
.mchip>span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.mchip .x{border:0;background:none;color:var(--dim);cursor:pointer;font-size:14px;line-height:1;
 padding:2px 6px;border-radius:999px}
.mchip .x:hover{color:var(--pink2)}
.mchip .x:focus-visible{outline:2px solid var(--pink);outline-offset:1px}
/* .note carries no bottom margin and this one is inserted directly above the repository table,
   so its chips and its button sat on the table's top border. :has() rather than a class on the
   element, because scripts/test-hub-search.js pins that element's className exactly. */
p.note:has(.mchips){margin-bottom:12px}
.btn{display:inline-flex;align-items:center;gap:7px;font:inherit;font-size:13px;font-weight:600;
 padding:8px 14px;border-radius:8px;border:1px solid var(--border);background:var(--float2);
 color:var(--ink);text-decoration:none;cursor:pointer;transition:.14s}
.btn:hover{border-color:var(--blue2);color:var(--ink)}
.btn:focus-visible{outline:2px solid var(--pink);outline-offset:2px}
/* The filled CTA uses the site's button tokens, not a flat --pink2. White on #ff55ff is 2.6:1,
   which fails WCAG AA for this size; the --btnhi/--btnlo gradient the front page already ships
   for its own primary button measures 7.9:1, and is the same object visually. */
.btn.primary{background:linear-gradient(180deg,var(--btnhi),var(--btnlo));border-color:var(--btnbd);
 color:var(--btntext)}
.btn.primary:hover{filter:brightness(1.08);color:var(--btntext)}
.btn:disabled{opacity:.5;pointer-events:none}
.btn.ghost{background:none}
/* compare page */
.cmptools{display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin:0 0 10px}
.cmptools.segs{gap:8px 6px;margin-bottom:14px}
/* The same object as the front page's chart filters: a row of pills, on-state tinted rather
   than filled. The solid segmented control this replaced read as a different site and put
   white on #ff55ff at 2.6:1. Values copied from .range button on the front page, deliberately.
   */
.seg{display:inline-flex;gap:6px;flex-wrap:wrap}
/* Three groups on one row read as one row of seven buttons without a rule between them. Same
   arithmetic as the front page's `.range .grp+.grp`: the row's 6px column-gap sits left of the
   border, so margin-left 5 (6+5=11) matches padding-left 11 on the right and centres the line.
   compare.js adds .nosep to a group that has wrapped, so a new row never opens with a border.
   */
.segs .seg+.seg{margin-left:5px;padding-left:11px;border-left:1px solid var(--border)}
.segs .seg.nosep{margin-left:0;padding-left:0;border-left:none}
.seg button{font:inherit;font-family:var(--sans);font-size:12px;padding:3px 11px;cursor:pointer;
 border-radius:8px;background:var(--float2);border:1px solid var(--border);color:var(--dim);
 transition:.12s}
.seg button:hover{color:var(--ink);border-color:var(--blue2)}
.seg button[aria-pressed=true]{background:var(--pink-soft);border-color:var(--pink);
 color:var(--pink2);font-weight:600}
.seg button:focus-visible{outline:2px solid var(--pink);outline-offset:2px}
/* The roster row: the chips, then "Clear all" trailing them. On a phone it drops onto its own
   line after the chips, which is where a control that empties the list belongs. */
.chiprow{display:flex;flex-wrap:wrap;align-items:center;gap:8px;margin:0 0 14px}
/* "Nothing picked yet" is the page telling the reader about ITSELF, not a measurement; italic
   is the distinction #capnote and dl.facts dd.unknown already use for that. */
.noneyet,.cmpempty p{font-style:italic}
.chips{display:flex;flex-wrap:wrap;gap:8px}
/* 999px here and 8px on .seg button and .range button, on purpose. A chip is a removable token
   and is always far wider than it is tall, so the radius clamps to the height and draws a true
   pill. The buttons went to a fixed 8px because the coarse-pointer floor raises them to 44px
   without widening them, at which point 999px drew an oval standing on end. Nothing raises
   .chip's height; revisit if a min-height ever reaches it. */
.chip{display:inline-flex;align-items:center;gap:8px;padding:5px 6px 5px 10px;border-radius:999px;
 border:1px solid var(--border);background:var(--float2);font-size:13px;max-width:100%}
/* The swatch is the same marker glyph the tooltip and the chart's end labels use, so it
   carries colour AND shape. 13 matches the glyph's viewBox; a radius would mean nothing on an
   <svg>. */
.chip .sw{width:13px;height:13px;flex:none}
/* The name is the hide/show toggle now, so it is a <button> reset back to looking like the
   text it was, states and contrast reasoning lifted from the legend rules it replaced. */
/* font:inherit FIRST, then everything it would have reset: it is a shorthand, so a weight set
   in front of it is silently thrown away. */
.chip .cn{font:inherit;font-weight:600;color:var(--ink);background:none;border:0;padding:0;
 text-align:left;cursor:pointer;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.chip .cn:hover{color:var(--pink2)}
.chip .cn:focus-visible{outline:2px solid var(--pink);outline-offset:2px;border-radius:4px}
/* Hidden is a state the reader put the series into, and its own control is the only way back
   out, so it has to stay readable. opacity:.45 measured 3.57 dark and 2.49 light against the
   4.5 small text needs, and dropped the marker to 1.5 against the 3.0 a non-text mark needs.
   The state borrows the .seg pills' pressed idiom instead: --dim at normal weight.
   Deliberately NOT a strike, which is .nodata's mark below. The marker is redrawn hollow in
   assets/compare.js (markGlyph), not here: four of the six marks need their `fill` dropped and
   a stroke put back. */
.chip .cn[aria-pressed=false]{color:var(--dim);font-weight:400}
/* A series with nothing to draw keeps its chip, because its absence from the chart is itself a
   fact about it. Struck through so nobody hunts for a line that is not there; the toggle is
   genuinely `disabled`, and the x beside it still removes. */
.chip.nodata .cn{text-decoration:line-through;text-decoration-thickness:1px;cursor:default;
 color:var(--dim);font-weight:400}
.chip.nodata .cn:hover{color:var(--dim)}
/* A single pick has nothing to tell apart, so the name is a plain <span> then, not a toggle
   that can never act. Same look, no pointer. */
.chip span.cn{cursor:default}
.chip .x{display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px;
 border:0;border-radius:50%;background:none;color:var(--dimmer);cursor:pointer;font:inherit;
 font-size:15px;line-height:1;flex:none}
.chip .x:hover{background:var(--border2);color:var(--ink)}
.chip .x:focus-visible{outline:2px solid var(--pink);outline-offset:1px}
/* The series glyph in front of a table row's name, so the row reads as the same repository as
   the line on the chart. Pinned to the TOP of its grid column and centred on the first line of
   the name, never aligned on a baseline: under a coarse pointer the name beside it is an
   inline-block, and such a box reports its LAST line as its baseline, which walked the glyph
   down to the second line of every name that wrapped. The px is a floor for a browser without
   the lh unit, which lands the glyph a couple of pixels high and nothing worse. */
#cmptable td .sw{width:11px;height:11px;flex:none;align-self:start;margin-top:6px;
 margin-top:calc((1lh - 11px) / 2)}
/* The glyph gets a gutter of its own so a name too long for the column wraps under ITSELF and
   not under the glyph, which used to leave the glyph looking like a mark on its own line and
   start the by-line halfway down the row. A grid and not a hanging indent (padding-left with a
   negative text-indent), because the coarse-pointer rule above makes a td's <a> an
   inline-block for its tap target, and an atomic box ignores text-indent: the name printed ON
   TOP of the glyph on every touch screen. minmax(0,1fr) so the text column may shrink below
   its longest word, which is what keeps this off a phone's horizontal scrollbar. The compare
   table is the only one that needs any of it: the hub gives its checkbox a column of its own. */
#cmptable td .rn{display:grid;grid-template-columns:auto minmax(0,1fr);column-gap:6px}
.cmpbox{position:relative}
.cmpsvg{display:block;width:100%;height:340px;overflow:visible;touch-action:pan-y}
.cmpsvg .grid{stroke:var(--grid);stroke-width:1}
.cmpsvg .axis{stroke:var(--border);stroke-width:1}
.cmpsvg text{fill:var(--chartink);font:11px var(--sans)}
.cmpsvg text.lbl{font-weight:600;font-size:11.5px}
.cmpsvg .ln{fill:none;stroke-width:2;stroke-linejoin:round;stroke-linecap:round}
.cmpsvg .cross{stroke:var(--dim);stroke-width:1;stroke-dasharray:3 3}
/* Wider than it was, and bounded by the chart it sits in rather than by a flat pixel figure.
   At 260px both halves of a row wrapped, breaking a name mid-word and splitting "(typically
   7)" across three lines. The percentage keeps a 320px tooltip from covering a 330px chart. */
.cmptip{position:absolute;pointer-events:none;z-index:4;min-width:150px;max-width:min(320px,86%);padding:9px 11px;
 border:1px solid var(--border);border-radius:9px;background:var(--float);color:var(--textbody);
 font-size:12.5px;box-shadow:0 8px 26px rgba(0,0,0,.3);opacity:0;transition:opacity .1s}
.cmptip.on{opacity:1}
.cmptip .when{color:var(--dimmer);font-size:11px;text-transform:uppercase;letter-spacing:.06em;
 font-weight:600;margin-bottom:5px}
.cmptip .r{display:flex;align-items:center;gap:7px;margin-top:3px}
/* The tooltip's swatch is the series MARKER, drawn by markGlyph() in assets/compare.js, so it
   is an <svg> and not a coloured square. The chips, the labels at the ends of the lines and
   this all use it: one shape per repository is what makes the chart readable without colour.
   */
.cmptip .r .sw{width:12px;height:12px;flex:none}
/* The name is the only part allowed to give way: "-10 (typically 7)" split across lines reads
   as two separate figures, and that pair is what a reader is here to see together. */
.cmptip .r .nm{flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.cmptip .r .n{margin-left:auto;flex:none;white-space:nowrap;
 font-family:var(--mono);color:var(--ink);font-weight:600}
/* The .legend rules that stood here are gone with the legend itself; their states now live on
   .chip .cn above, which is the one roster that carries both hide/show and remove. */
/* The same disclosure the front page uses, rule for rule; it is the same control. */
.notes{margin:0 0 16px}
.notes>summary{cursor:pointer;color:var(--dim);font-size:12.5px;width:fit-content;
 padding:3px 0;list-style:none;display:flex;align-items:center;gap:7px}
.notes>summary::-webkit-details-marker{display:none}
.notes>summary::before{content:"";width:0;height:0;border:4px solid transparent;
 border-left-color:currentColor;transition:transform .15s}
.notes[open]>summary::before{transform:rotate(90deg) translateX(-1px)}
.notes>summary:hover{color:var(--pink2)}
.notes>summary:focus-visible{outline:2px solid var(--pink);outline-offset:3px;border-radius:4px}
.notes>p{margin:8px 0 0}
.picker{position:relative;flex:1;min-width:min(320px,100%)}
/* The listbox hangs off .picker, so the label inside it moves the anchor to the field's top;
   calc(100% + 5px) still lands 5px under the input, the field being exactly label plus input.
   */
.picker input{width:100%}
/* At the cap the input goes away and leaves its message, with the roster and its Clear all
   directly underneath, which is where the way out is; greying it kept a full-width box that
   still says "type here". display:none means the input leaves the accessibility tree and
   cannot hold focus: renderChips in assets/compare.js keeps it readOnly rather than `disabled`
   for the case where it IS on screen, and hands focus to the roster before hiding it, or the
   reader who added the sixth repository from the keyboard is dropped on <body>. Changing this
   rule without reading that one re-opens it. */
.picker.atcap>label,.picker.atcap>input{display:none}
/* Italic, because this is the page speaking rather than a label or a figure. */
#capnote{font-style:italic;margin:5px 0 0}
.sugg{position:absolute;top:calc(100% + 5px);left:0;right:0;z-index:6;margin:0;padding:4px;
 list-style:none;max-height:min(48vh,340px);overflow-y:auto;border:1px solid var(--border);
 border-radius:9px;background:var(--float);box-shadow:0 10px 30px rgba(0,0,0,.32)}
.sugg li{padding:0}
.sugg button{display:flex;width:100%;gap:10px;align-items:baseline;text-align:left;font:inherit;
 font-size:13px;padding:7px 10px;border:0;border-radius:6px;background:none;color:var(--textbody);
 cursor:pointer}
/* --float2 on a --float popup is a 4-value difference in dark mode, so the row a reader was
   pointing at looked like the forty around it; --pink-soft is translucent and reads on both.
   The keyboard cursor has to out-mark the pointer, because both can be on the popup at once
   and Return follows the arrowed one; it does that with a DEEPER fill rather than the inset
   rail it used to wear, which read as a rendering fault. */
.sugg button:hover{background:var(--pink-soft);color:var(--ink)}
.sugg button[aria-selected=true]{background:color-mix(in srgb,var(--pink) 26%,transparent);
 color:var(--ink)}
/* The part of a result that matched what was typed, written by assets/match-mark.js on the
   hub's rows and on this popup's options. A browser's own <mark> is black on yellow, which
   belongs to neither theme, and the fill has to beat --pink-soft, which already fills a
   pointed-at row. */
mark{background:color-mix(in srgb,var(--pink) 30%,transparent);color:var(--ink);
 border-radius:3px;padding:0 1px;text-decoration:none}
.sugg .nm{font-weight:600;color:var(--ink);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.sugg .meta{margin-left:auto;color:var(--dimmer);font-family:var(--mono);font-size:11.5px;flex:none}
.cmpempty{text-align:center;padding:26px 14px}
.cmpempty p{color:var(--dim);font-size:13.5px;max-width:60ch;margin:0 auto 16px}
/* Coarse pointers get a 44px target on every control that changes what is on screen: WCAG
   2.2's 2.5.5 Target Size (Enhanced). The AA floor is 2.5.8's 24px, and nothing here goes
   below it. Same rule and the same reason as the front page's chart filters. */
@media(pointer:coarse){
 .seg button,.btn,.sortbtn{min-height:44px}
 .seg button,.btn{display:inline-flex;align-items:center}
 /* 24px, the AA floor: a fifth column cannot take 44 without pushing the other four off a
    360px screen, and the row's own padding gives it the spacing 2.5.8 asks for. .pickbox
    (16px) and .chip .x (20px) are BELOW 24 at a fine pointer and pass only through 2.5.8's
    spacing exception: the nearest other target to a tick box measures 33.3px away. If either
    control ever gains a neighbour, the size is what has to move. */
 .pickbox{width:24px;height:24px}
 td.pick,th.pick{padding-left:12px;padding-right:6px}
 .chip .x{width:34px;height:34px}
 /* The hub's maintainer chips never had a rule here, so their remove control stayed a 14px
    glyph on a phone. 30 rather than the compare chip's 34, because these sit inline in a
    sentence and a 34px control drags its own line height with it. */
 .mchip .x{min-width:30px;min-height:30px}
}
/* Size goes before the phone block does, because four columns stop fitting well above phone
   width. Measured on the real hub, all 917 rows: at 660px the Identity column is down to 222px
   and every aliased row stands 87px tall; at 561px the table is 5px wider than the viewport.
   700 is the width this file already breaks .movers at. Size is the column to drop rather than
   Repositories or Typical seeds, which are what the page ranks by, and every row links to an
   identity page whose Size tile prints the figure. */
@media(max-width:700px){.sz{display:none}}
/* 560, because four table columns need about 520px and a phone has 380. The usual fix,
   wrapping the table in an overflow-x container, is exactly what would break the sticky
   header, so the padding and the type come down instead. Lifted with its reason from
   recent-live.py. */
@media(max-width:560px){
 main{padding:26px 14px 40px}
 /* 6px, not 9: with the by-line able to break, the five columns fit a 360px screen at 9px
    but still sit 4px wider than the card that holds them at 320px. */
 th,td{padding:8px 6px}
 td.n{white-space:normal}
 /* On a phone the Identity column has ~150px, so an alias plus a shortened key wraps. Left to
    itself it wrapped mid-string and each aliased row took a different height; given its own
    line it is two clean lines every time, the shape td .by takes on the other hub.

    white-space:normal is what arms the <wbr> ident_short_html() writes after the type prefix,
    and it is safe ONLY because .idkey above is nowrap: normal on its own would also let the
    key break at the ellipsis. The prefix used to be display:none here instead; see that
    function's docstring for why keeping it is worth the fold.

    11px and not the desktop 12: with the prefix restored the fragment measured 151px against
    143 of column at 375, and the 8 it was short came out of the two sort headers' carets,
    which only exist once identities-index.js has upgraded them and so are invisible to any
    preview built without it. Taken off this element's own type rather than off th,td padding,
    which every table on the site shares and which is already tuned to fit /compare at 320.
    Nothing here is load-bearing though, and that is the point of the fold: if a future header
    or caret eats the slack, the fragment takes a second line instead of a scrollbar. */
 .idfrag{display:block;margin-left:0;margin-top:2px;white-space:normal;font-size:11px}
 /* Header cells do not wrap, so at 320 the hub's Identity + Typical seeds + Repositories came to
    343 and the whole page scrolled sideways. "Typical seeds" is the only header here with a space
    in it to give back, and 74 instead of 115 is more than the 23 that was over. Scoped by id and
    not to .list th, because that is shared with the repository tables in two other generators;
    #th-seeds there reads "Seeds", one word, so this rule cannot move them.

    Applied from 560 and not from 374, which is where it sat while it only had that overflow to
    fix: the 41px it gives back is what keeps the restored type prefix on ONE line with its key
    at 375. Without it the fragment folds from 375 down instead of from ~370, costing every
    phone row a third line to save a header a second one. */
 #th-seeds{white-space:normal}
 /* "Repos" for "Repositories", worth about 65px of a column that has none to spare. The word
    is only a label for the figure under it, and the sort button's own title still says
    "repositories" in full for anyone who asks what the column is. */
 .thlong{display:none}
 .thshort{display:inline}
 /* The compare table has four columns where the hub has five, so a size keeps its ~30px on one
    line; letting "636 KiB" break added a second line to every row of a six-row table. */
 #cmptable td.n{white-space:nowrap}
 /* h1 has no override here any more: clamp(26px,4.6vw,33px) already lands on its 26px floor at
    this width, and the flat 22px this used to force put the page title below the section
    headings under it. Same floor as the front page's .headline. */
 dl.facts{grid-template-columns:1fr;gap:2px 0}
 dl.facts dd{margin-bottom:8px}
 .cmpsvg{height:270px}
 .cmpbar{padding:10px 12px;gap:9px}
 /* The picked-repositories bar. Its three parts shared one wrapping flex line, so on a 375px
    screen the two buttons claimed 164px of the chips' row and six chips took six rows, with
    the buttons stranded in the stack and the bar 233px tall. A band each, in the order the bar
    is read: what is picked, which ones, what to do with them. Two equal columns rather than
    free flow, which is what produced the ragged one-chip rows. */
 .cmpbar .lbl{flex:1 1 100%;text-wrap:pretty}
 /* minmax(0,1fr), never a bare 1fr: a grid track's automatic minimum is min-content, so one
    43-character repository name widened its own column, the equal one beside it and the page
    under both, taking the pair to 628px inside a 331px bar. Zero lets the ellipsis do its job.
    */
 .bchips{flex:1 1 100%;display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:6px;
  min-width:0}
 .bchip{max-width:100%;justify-content:space-between}
 .cmpbar .btn{flex:0 0 auto}
 .cmpbar .btn.primary{flex:1 1 auto;justify-content:center}
 /* The budget in the comment above was written for four columns. The pick column is a fifth,
    and at its coarse-pointer padding it put the table 8px wider than the card that holds it.
    The tick keeps its 24px target; only the gap around it comes in. */
 td.pick,th.pick{padding-left:5px;padding-right:2px}
 /* A figure and its unit are one value, and at this width the Size column was narrow enough to
    put "640" on one line and "KiB" on the next. The name beside it wraps already. */
 td.n,th.n{white-space:nowrap}
 /* Phone-only compaction, on purpose: on a 375x667 screen /compare put its picker, its roster
    and three rows of filters above the chart, so a reader arriving from a "chart these
    together" link saw no chart at all. Nothing here goes under the 24px AA floor; this trades
    some of the coarse-pointer block's 44 back for the fold. */
 .chip{font-size:12.5px;padding:2px 4px 2px 9px;gap:6px}
 .chip .x{width:26px;height:26px;font-size:14px}
 /* Same two equal columns as the bar above, for the same reason: at 375 free flow leaves a
    lone chip on the last row. 26px keeps the remove control clear of the 24px AA floor. */
 .chips{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:6px}
 /* Equal cells mean the name no longer decides the pill's width, so left to itself the remove
    control lands wherever the name stops and the six sit at six different offsets. The name
    takes the slack instead and ellipsises. */
 .chip .cn{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
  text-align:left}
 .chiprow{gap:6px;margin-bottom:10px}
 /* Deliberately overrides the 44px of the (pointer:coarse) block above, on EVERY button on the
    page: same specificity, and this block is written later. 44 is 2.5.5 Target Size
    (Enhanced), which is AAA; the AA requirement is 2.5.8's 24px and 34 clears it. What the
    10px buys is the fold: seven filter buttons, a sort row and a roster reset at 44 each
    pushed the first chart off a 667px phone screen. Only below 560px. Revisit if the phone
    layout ever gets a chart above the fold without spending this. */
 .seg button,.btn,.sortbtn{min-height:34px}
 /* A 34px button next to 17px of text, both top-aligned, put "Pick" and "Repository" 8px above
    the headers beside them. The min-height above is what made them uneven, so this block
    corrects it. */
 thead th{vertical-align:middle}
 /* 10px each side put the seven filter buttons 18px over a 375px row, so the three groups took
    two rows and the chart paid for the wrap. At 6 they total 307 and fit on one row, and the
    buttons keep their 34px height. */
 .seg button{padding:3px 6px}
 .btn{padding:6px 12px}
 /* The roster's reset, which wraps to a row of its own under the chips. Small and pushed right
    so it reads as the roster's footer rather than a third stranded control. */
 #clearall{min-height:30px;padding:4px 11px;font-size:12.5px;margin-left:auto}
 /* A chart's title is a label on a card, not a section heading for the page, and at 19px this
    one ran to three lines and spent 88px of the fold saying so. */
 #chartcard h2{font-size:16px}
 .cmptools{margin-bottom:8px}
 .cmptools.segs{margin-bottom:10px}
 /* The maintainer bar is a sentence, a chip row and a button sharing one paragraph, which at
    this width wraps into a cramped block with no seam between the three. One line each. */
 .mchips{display:flex;margin:8px 0 0}
 .note>.btn{display:flex;width:100%;justify-content:center;margin-top:8px}
 /* The stat cards' 190px track means one per row here, so six tiles are six screens' worth of
    mostly empty space. 138 fits two at 320px. */
 .card{padding:13px 14px}
 .grid{grid-template-columns:repeat(auto-fit,minmax(138px,1fr));gap:12px}
 .stat .v{font-size:21px;--vbox:33px}
 .stat .n{font-size:11.5px}
 /* 46+28 of lead above every h2 is a third of a phone screen spent on whitespace between
    sections that are already separated by a card edge. */
 h2{margin:34px 0 8px}
 main>h2{padding-top:16px}
 /* "Put up to 6 repositories on one chart" is the right first sentence for an empty /compare
    and spent copy for a reader who arrived with six already on it, and on a phone it is 52px
    of the fold. Only when something is actually charted, and only at this width. */
 main:has(#chips .chip)>p.sub{display:none}
}
/* Every transition on the page, and the list is meant to stay complete: .seg button and
   .themebtn were both still cross-fading at 0.12s and 0.14s under an emulated reduce. */
@media(prefers-reduced-motion:reduce){.mv,.btn,.cmptip,.seg button,.themebtn,.copyable,#toast,
 .notes>summary::before{transition:none}}
