:root {
  --bg: #fbfbfa;
  --fg: #1a1a18;
  --muted: #6b6b66;
  --line: #e3e3df;
  --card: #ffffff;
  --accent: #2f6f4e;
  /* The token tints, which are the logo's (token strip tints spec). The old
     pair was 1.08:1 apart and the boundary between two tokens, the thing the
     page exists to show, was close to invisible; these are 1.23:1.
     test/contrast.test.js holds them, and everything drawn on them, to their
     ratios in both themes. */
  --tok-a: #e1ede6;
  --tok-b: #c2dacc;
  /* Each token's edge, so a boundary does not rest on the tints alone: a lone
     one-letter token such as "I" sat on a tint 1.1:1 from the card and read as
     plain text. test/contrast.test.js holds it to 3:1 against the card, the
     non-text contrast WCAG 1.4.11 asks of a graphic that carries meaning. */
  --tok-edge: #5f9479;
  /* Words the Findings card flags as costing tokens without doing work. The
     same two-tint alternation as the strip, in red, so a flagged run still
     shows where its tokens fall. */
  --waste-a: #fbe4df;
  --waste-b: #f4cbc1;
  --waste-edge: #b8472f;
  /* The mark's whole token in the lockup (docs/brand/web/brand.css): the other
     theme's accent. The lockup's spans are --tok-a and --tok-b, and the
     whetted token and the letters are --accent. */
  --brand-whole: #7fc9a2;
  --alert-bg: #fdf1ee;
  --alert-line: #e4b8ac;
  --guess-bg: #fbf1dc;
  --guess-line: #b07a1e;
  --badge-bg: #f3f4f1;
  --badge-line: #dcdcd6;
  --shadow: 0 1px 2px rgb(26 26 24 / 6%), 0 4px 12px rgb(26 26 24 / 4%);
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme='light']) {
    --bg: #16171a;
    --fg: #ececea;
    --muted: #9a9a94;
    --line: #2c2e33;
    --card: #1d1f23;
    --accent: #7fc9a2;
    --tok-a: #1f2c25;
    --tok-b: #2d4638;
    --tok-edge: #5a8f73;
    --waste-a: #3b2421;
    --waste-b: #56322b;
    --waste-edge: #e07a64;
    --brand-whole: #2f6f4e;
    --alert-bg: #2a1e1b;
    --alert-line: #5c3a30;
    --guess-bg: #33291a;
    --guess-line: #d9a54a;
    --badge-bg: #25282d;
    --badge-line: #383b42;
    /* A shadow on a dark page is a darker patch on a dark background. The
       raised --card surface already does the lifting here. */
    --shadow: none;
  }
}
:root[data-theme='dark'] {
  --bg: #16171a;
  --fg: #ececea;
  --muted: #9a9a94;
  --line: #2c2e33;
  --card: #1d1f23;
  --accent: #7fc9a2;
  --tok-a: #1f2c25;
  --tok-b: #2d4638;
  --tok-edge: #5a8f73;
  --waste-a: #3b2421;
  --waste-b: #56322b;
  --waste-edge: #e07a64;
  --brand-whole: #2f6f4e;
  --alert-bg: #2a1e1b;
  --alert-line: #5c3a30;
  --guess-bg: #33291a;
  --guess-line: #d9a54a;
  --badge-bg: #25282d;
  --badge-line: #383b42;
  --shadow: none;
}
* {
  box-sizing: border-box;
}
body {
  margin: 0;
  background: var(--bg);
  color: var(--fg);
  font:
    16px/1.55 ui-sans-serif,
    system-ui,
    -apple-system,
    Segoe UI,
    sans-serif;
}
.wrap {
  max-width: 820px;
  margin: 0 auto;
  padding: 40px 16px 80px;
}
h1 {
  margin: 0 0 4px;
}
/* The lockup (brand spec §5.2). 2.4rem makes its token spans 29px tall, over
   the 24px the brand sets as the floor for spans. The artwork carries 18 units
   of clear space around a height of 152.21, about 4.5px here, and the negative
   left margin takes it off that side so the mark's edge is the tagline's edge. */
.brand-lockup {
  display: block;
  height: 2.4rem;
  width: auto;
  margin-left: calc(2.4rem * -18 / 152.21);
}
.brand-lockup .bw {
  fill: var(--brand-whole);
}
.brand-lockup .bc {
  fill: var(--accent);
}
.brand-lockup .sa {
  fill: var(--tok-a);
}
.brand-lockup .sb {
  fill: var(--tok-b);
}
/* In forced colours, the lockup is drawn the way the brand draws it in one
   colour (BRAND.md, Rules, 4): both tokens and the letters solid in the text
   colour, the spans gone. Opted out of the browser's own adjustment so these
   system colours are the ones used, rather than fills it may or may not
   force. */
@media (forced-colors: active) {
  .brand-lockup {
    forced-color-adjust: none;
  }
  .brand-lockup .bw,
  .brand-lockup .bc {
    fill: CanvasText;
  }
  .brand-lockup .sa,
  .brand-lockup .sb {
    fill: Canvas;
  }
}
.sub {
  color: var(--muted);
  margin: 0 0 28px;
}
/* The tagline drawn as its own tokens (self-tokenising tagline spec §5). A
   sweep changes colour only, background and text, never padding or margin, so
   it moves no text. Odd and even tokens take the token strip's two tints, so
   the tagline and the strip read as the same kind of thing. Text on a tint is
   --fg rather than the tagline's --muted, for contrast, which is the choice
   the token strip makes on the same tints: on 2026-09-26 --muted on --tok-b
   measured 3.62:1 in the light theme and 3.63:1 in the dark (4.28:1 and 4.07:1
   on the paler tints before them), under the 4.5:1 that 16px text needs. */
#strapline > span[data-sweep],
#strapline > span[data-still] {
  border-radius: 3px;
}
#strapline > span[data-sweep] {
  animation: strapline-sweep-a 3.2s ease-in-out both;
  animation-delay: calc(var(--i) * 70ms);
}
#strapline > span[data-sweep]:nth-child(even) {
  animation-name: strapline-sweep-b;
}
#strapline > span[data-still] {
  background: var(--tok-a);
  color: var(--fg);
}
#strapline > span[data-still]:nth-child(even) {
  background: var(--tok-b);
}
@keyframes strapline-sweep-a {
  0% {
    background: transparent;
    color: var(--muted);
  }
  12%,
  55% {
    background: var(--tok-a);
    color: var(--fg);
  }
  100% {
    background: transparent;
    color: var(--muted);
  }
}
@keyframes strapline-sweep-b {
  0% {
    background: transparent;
    color: var(--muted);
  }
  12%,
  55% {
    background: var(--tok-b);
    color: var(--fg);
  }
  100% {
    background: transparent;
    color: var(--muted);
  }
}
/* The script already refuses to sweep under reduced motion. This is the second
   lock, for a preference changed while the page is open. Both selectors are
   named so this beats #strapline > span[data-sweep]:nth-child(even) above:
   that rule's higher specificity would otherwise leave even tokens still
   naming strapline-sweep-b and sweeping regardless of the preference. */
@media (prefers-reduced-motion: reduce) {
  #strapline > span[data-sweep],
  #strapline > span[data-sweep]:nth-child(even) {
    animation: none;
  }
}
/* The count of the tokens above, in the stats bar's badge style. It holds its
   width from first paint and stays invisible while empty, so filling it once
   the vocabulary arrives cannot push the sentence onto a new line. min-width
   is held above the filled width, because another platform's font renders
   the same text wider: on 2026-09-26, with min-width removed, the filled
   badge measured 162px wide against the 204px this reserves, about 26% of
   headroom. Centred so the spare space lands evenly on both sides. */
.strapline-count {
  display: inline-block;
  min-width: 17em;
  text-align: center;
  margin-left: 8px;
  padding: 0 8px;
  font-size: 0.75rem;
  line-height: 1.6;
  vertical-align: 1px;
  border: 1px solid var(--accent);
  border-radius: 6px;
  background: var(--badge-bg);
  color: var(--accent);
  font-variant-numeric: tabular-nums;
}
.strapline-count:empty {
  visibility: hidden;
}
textarea {
  width: 100%;
  min-height: 160px;
  padding: 14px;
  resize: vertical;
  background: var(--card);
  color: var(--fg);
  border: 1px solid var(--line);
  border-radius: 10px;
  font:
    14px/1.6 ui-monospace,
    SFMono-Regular,
    Menlo,
    monospace;
}
/* The boot failure message. Decorated only when it has something to say: the
   element itself stays in the document from first paint, because that is what
   lets role="alert" announce the text when it arrives. So `:empty` governs the
   box, never whether the region is there.
   `:empty` counts whitespace as content, so the div in index.html has to stay
   written on one line with nothing between its tags. test/markup.test.js
   asserts that, because a reformat would otherwise leave an empty box drawn on
   every load and nothing would say why. */
.boot-error:not(:empty) {
  margin: 0 0 20px;
  padding: 12px 14px;
  border: 1px solid var(--alert-line);
  border-radius: 10px;
  background: var(--alert-bg);
  font-size: 0.9rem;
}
/* The sighted half of the same message. The box is readonly rather than
   disabled so it keeps its place in the tab order, which leaves it looking
   perfectly usable unless something says otherwise. */
textarea[readonly] {
  opacity: 0.6;
  cursor: not-allowed;
}
/* The sighted half of aria-busy, the same way (#307). While a slow count is
   being made, the strip and the Wordiness card are still the text before it,
   and the count line says it is counting. Dimmed rather than hidden, so
   nothing moves when the count lands; the headings and the picker are left
   as they are, since they are not drawn from the count. The Checks card's
   results and the Versions card's comparison are the text before as well
   (#311); the form that adds a check and the list of versions are not. */
#tokens-card[aria-busy='true'] #tokens,
#findings[aria-busy='true'] #findings-body,
#checks[aria-busy='true'] :is(.checks-summary, .checks-list, .checks-placeholders),
#versions[aria-busy='true'] .versions-compare {
  opacity: 0.6;
}
/* No margin: every card sits in a workbench column, and the column's gap is
   what spaces them (workbench spec §7). */
.card {
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: 10px;
  padding: 16px;
  box-shadow: var(--shadow);
}
/* A card's title. The prompt card's is its label, which names the box as well,
   so it is drawn like the other cards' h2 rather than given a second name.
   One rule for both, so the two cannot drift apart. The prompt card's title is
   a row, because the examples button sits at its right-hand end. */
.card h2,
.prompt-head {
  font-size: 0.78rem;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--muted);
  margin: 0 0 12px;
  padding-bottom: 8px;
  border-bottom: 1px solid var(--line);
  font-weight: 600;
}
.prompt-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
}
/* The line under the title saying the prompt stays in the browser (#442). Pulled
   up toward the rule above it, so it reads as part of the title row. */
.privacy-note {
  margin: -4px 0 10px;
  font-size: 0.8rem;
}
/* The label keeps the title's look rather than the plain label's below. */
.prompt-head > .field-label {
  display: inline;
  margin: 0;
  font-size: inherit;
  color: inherit;
}
#tokens {
  font:
    13px/1.9 ui-monospace,
    SFMono-Regular,
    Menlo,
    monospace;
  white-space: pre-wrap;
  word-break: break-word;
  /* A height of its own, with its own scroll (#415, D48). Drawn in full, up
     to its 2,000 tokens, a 15,000 character paste made the strip 3,939 px
     tall at 1440 × 900 on 2026-10-11 and put the recommendation five screens
     down. The keys bring the token they land on into view (render.js), and
     the strip is a tab stop whenever it holds tokens, so it scrolls from the
     keyboard too. */
  max-height: min(30rem, 60vh);
  overflow-y: auto;
}
/* Every token is a box: its tint, and an edge that holds 3:1 against the card
   whatever the tint beside it. Adjacent edges meet as a line between two
   tokens, so a boundary is visible without comparing two tints, and a token
   of one narrow letter is still visibly a token. An inset shadow rather than a
   border, so nothing about the text's layout moves: the strip shows the prompt
   verbatim and a border would widen every token by two pixels. */
#tokens span {
  border-radius: 3px;
  padding: 1px 0;
  box-shadow: inset 0 0 0 1px var(--tok-edge);
}
/* background-color, not the shorthand, here and on the red below: the shorthand
   resets background-image, which is where the caret's bar is drawn. */
#tokens span:nth-child(odd) {
  background-color: var(--tok-a);
}
#tokens span:nth-child(even) {
  background-color: var(--tok-b);
}
/* Tokens that would otherwise draw nothing. A marker is what makes them
   countable, and it is also what keeps the stripe above honest: a span with
   no width still takes a stripe slot, so it flips the colour of every token
   after it and two neighbours end up looking like one. */
#tokens span.newline::before {
  content: '⏎';
  opacity: 0.55;
}
#tokens span.zero-width::before {
  content: '·';
  opacity: 0.55;
}
/* Whitespace that is not a newline. It has a width, so it never needed a marker
   to keep the stripe honest -- but the stripe is a few percent of background
   and on its own it does not say where an indent starts, where it ends, or
   whether it is one token or two. An inset edge turns the token into a visible
   box without a glyph over text the page shows verbatim, and without changing
   `display`, which `white-space: pre-wrap` is relying on. */
#tokens span.blank {
  /* --muted, not --line. Measured against the stripe this sits on, on
     2026-09-26: --line is a contrast ratio of 1.07 against --tok-a in both
     themes and at most 1.32 against --tok-b, which is an edge nobody can see --
     the same complaint as the tint it was added to fix. --muted is 3.62 at
     worst in the light theme and 3.63 in the dark, clear of the 3:1 that
     non-text contrast asks for, and test/contrast.test.js holds it there. */
  box-shadow: inset 0 0 0 1px var(--muted);
}
/* Words the Findings card flags as waste, in red where the rest is green. After
   .blank, so a space inside a flagged run takes the run's edge. */
#tokens span[data-waste]:nth-child(odd) {
  background-color: var(--waste-a);
}
#tokens span[data-waste]:nth-child(even) {
  background-color: var(--waste-b);
}
#tokens span[data-waste] {
  box-shadow: inset 0 0 0 1px var(--waste-edge);
}
/* The tokens the prompt's caret or selection is in (token inspector spec §2).
   A bar along the bottom rather than an outline, so it reads as a different
   thing from the strip's own cursor below and both can show at once. Drawn as
   a background image so it sits on whichever tint or red is there and leaves
   each kind of token its own edge. --accent is held to 3:1 against all four
   tints in both themes by test/contrast.test.js. */
#tokens span[data-caret] {
  background-image: linear-gradient(var(--accent), var(--accent));
  background-size: 100% 3px;
  background-position: bottom;
  background-repeat: no-repeat;
}
/* What the two colours mean, under the strip, shown only when both are on it. */
.token-legend {
  display: flex;
  flex-wrap: wrap;
  gap: 4px 14px;
  margin: 8px 0 0;
  color: var(--muted);
  font-size: 0.8rem;
}
.token-legend[hidden] {
  display: none;
}
.token-legend span::before {
  content: '';
  display: inline-block;
  width: 0.9em;
  height: 0.9em;
  margin-right: 6px;
  vertical-align: -1px;
  border-radius: 2px;
}
.token-legend .legend-work::before {
  background: var(--tok-b);
  box-shadow: inset 0 0 0 1px var(--tok-edge);
}
.token-legend .legend-waste::before {
  background: var(--waste-b);
  box-shadow: inset 0 0 0 1px var(--waste-edge);
}
/* Where the keyboard is. The strip is one tab stop and the cursor moves inside
   it, so this is the only thing on screen saying which token the detail line
   below is describing. */
#tokens span[data-current] {
  outline: 2px solid var(--accent);
  outline-offset: -1px;
}
#tokens:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
}
/* Only in the layout when it has something to say, the same rule the note
   under the verdict follows: an empty line here reads as something that failed
   to load. */
.token-detail:not(:empty) {
  margin-top: 10px;
}
/* The highlight stops after MAX_TOKEN_SPANS spans. This is the line that says
   so, and it is a div rather than a span so the stripe above cannot select it. */
#tokens .truncated {
  color: var(--muted);
  font-size: 0.85rem;
  margin-top: 10px;
}
/* The way back from a category override. No `display` here on purpose, so the
   user agent's [hidden] rule still takes it out of the layout when there is
   nothing to go back to. */
button.link {
  background: none;
  border: 0;
  padding: 0;
  margin-left: 10px;
  font: inherit;
  /* Matches the select beside it. The generic `select, button` rule sets this
     too, but `button.link` outranks it and would otherwise inherit the card. */
  font-size: 0.88rem;
  color: var(--muted);
  text-decoration: underline;
  cursor: pointer;
}
button.link:hover,
button.link:focus-visible {
  color: inherit;
}
/* The stats bar under the box (workbench spec §5). It holds the rows its
   badges take once there is a count, so the first keystroke does not push the
   link row down. On a phone and in the workbench's left column the count
   takes one row and the figures the next; where the single column is wide
   enough, all four fit on one. A badge is 0.8rem at a line height of 1.5,
   with 2px of padding and a 1px border above and below, and the rows are 6px
   apart. Measured in Chromium on 2026-09-26 at 375, 768, 1024 and 1440 wide,
   with each example in the box: one row of badges needed up to 538px of card,
   which the single column has from about 605px wide, and 640px leaves room
   for a wider system font. On a phone, a prompt long enough to count its
   characters in thousands can still wrap the figures onto a third row. */
.stats {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
  min-height: calc(2 * (1.2rem + 6px) + 6px);
  margin-top: 10px;
}
@media (min-width: 640px) and (max-width: 1023px) {
  .stats {
    min-height: calc(1.2rem + 6px);
  }
}
.figures {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  list-style: none;
  margin: 0;
  padding: 0;
}
/* Hidden while there are no figures. The display above would otherwise beat
   the user agent's [hidden] rule, the same as .lint-control's below. */
.figures[hidden] {
  display: none;
}
.count:not(:empty),
.figures li {
  font-size: 0.8rem;
  line-height: 1.5;
  padding: 2px 9px;
  border: 1px solid var(--badge-line);
  border-radius: 6px;
  background: var(--badge-bg);
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}
/* The count is the answer, so it is the badge that stands out. */
.count[data-state='count']:not(:empty) {
  color: var(--fg);
  font-weight: 600;
  border-color: var(--accent);
}
/* Loading, slow, failed, or an encoding with no count. A sentence squeezed
   into a badge reads as a broken number, so it gets the whole row as a plain
   line. After the badge rule, which it has to beat at equal specificity. */
.count[data-state='message'] {
  flex-basis: 100%;
  padding: 0;
  border: 0;
  background: none;
  font-size: 0.85rem;
}
/* The caveat under the count, which is empty whenever there is nothing to
   qualify. The gap arrives with the text, the same way the token detail line
   above does, so an empty region is not a hole in the card. */
#encoder-scope:not(:empty) {
  margin-top: 6px;
}
/* The prompt's count at the head of the Tokens card (#417). Plain bold text
   rather than the badge under the box, so the card has one number to read and
   the badge stays the page's count. Its gap arrives with its text, as above. */
.tokens-total:not(:empty) {
  margin-top: 8px;
  font-weight: 600;
  font-size: 1.05rem;
}
/* The guesses under the caveat (D32): plain lines in the note's colour, not
   badges, because a badge is how this card draws a count. Nothing here sets
   display, so the user agent's [hidden] rule still hides an empty list. */
.guesses {
  list-style: none;
  margin: 4px 0 0;
  padding: 0;
}
.row {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
  align-items: center;
}
select,
button,
input[type='search'] {
  font: inherit;
  font-size: 0.88rem;
  padding: 6px 10px;
  background: var(--card);
  color: var(--fg);
  border: 1px solid var(--line);
  border-radius: 7px;
}
/* The Checks card (checks-view.js): the reader's own checks, each with its
   result as a word on a badge as well as a colour, then the way to add one. */
.checks-summary {
  margin: 0;
  font-weight: 600;
}
.checks-list {
  list-style: none;
  margin: 8px 0 0;
  padding: 0;
  display: grid;
}
.checks-list li {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 4px 10px;
  padding: 7px 0;
  border-top: 1px solid var(--line);
}
.checks-list li button {
  margin-left: auto;
  font-size: 0.8rem;
  padding: 2px 9px;
  cursor: pointer;
}
.check-result {
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  padding: 1px 8px;
  border-radius: 999px;
  border: 1px solid var(--badge-line);
  background: var(--badge-bg);
  color: var(--muted);
}
.check-pass {
  border-color: var(--tok-edge);
  background: var(--tok-a);
  color: var(--fg);
}
.check-fail {
  border-color: var(--waste-edge);
  background: var(--waste-a);
  color: var(--fg);
}
.check-name {
  font-size: 0.92rem;
}
.checks-placeholders {
  margin: 8px 0 0;
  padding: 6px 10px;
  border-left: 3px solid var(--guess-line);
  background: var(--guess-bg);
  color: var(--fg);
}
.checks-add {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-top: 12px;
}
.checks-add input {
  flex: 1 1 10rem;
  min-width: 0;
  font: inherit;
  font-size: 0.88rem;
  padding: 6px 10px;
  background: var(--card);
  color: var(--fg);
  border: 1px solid var(--line);
  border-radius: 7px;
}
.checks-add input:focus-visible,
.checks-add select:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}
.checks-add button {
  cursor: pointer;
}
/* The model filter's opt-in is built the same way (#446). */
.checks-remember,
.filter-remember {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: 10px;
  font-size: 0.88rem;
  cursor: pointer;
}
.filter-remember[hidden] {
  display: none;
}
.checks-remember input,
.filter-remember input {
  width: 18px;
  height: 18px;
  margin: 0;
  accent-color: var(--accent);
}
.checks-status:not(:empty) {
  margin-top: 8px;
}
/* The Versions card (versions-view.js): a list of earlier versions, newest
   first, and the one being compared with the prompt now. */
.versions-list {
  list-style: none;
  margin: 8px 0 0;
  padding: 0;
  display: grid;
}
.versions-list li {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 4px 10px;
  padding: 8px 0;
  border-top: 1px solid var(--line);
  /* A grid item is as wide as its content by default, and the one-line start
     of a long version would widen the row past the card. */
  min-width: 0;
}
.versions-name {
  font-weight: 600;
  font-size: 0.9rem;
}
.versions-actions {
  display: flex;
  gap: 6px;
  margin-left: auto;
}
/* The start of each version's text (#422), on a line of its own under the
   name and the buttons, cut to the row's width. `order` puts it last on
   screen while a screen reader still reads it before the buttons. */
.versions-start {
  order: 1;
  flex-basis: 100%;
  min-width: 0;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  color: var(--muted);
  font-size: 0.85rem;
}
.versions-actions button {
  font-size: 0.8rem;
  padding: 3px 9px;
  cursor: pointer;
}
.versions-actions button[aria-expanded='true'] {
  border-color: var(--accent);
}
.versions-compare {
  display: grid;
  gap: 6px;
  margin-top: 6px;
  padding: 10px 12px;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--badge-bg);
}
.versions-compare[hidden] {
  display: none;
}
.versions-head {
  margin: 0;
  font-size: 0.88rem;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}
/* The comparison itself, in the strip's colours: what went is red and struck
   through, what arrived is green and underlined, so the two differ by more
   than colour. */
.diff {
  margin: 0;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.85rem;
  line-height: 1.8;
}
.diff del {
  background: var(--waste-a);
  color: inherit;
  text-decoration: line-through;
  text-decoration-color: var(--waste-edge);
  border-radius: 2px;
}
.diff ins {
  background: var(--tok-a);
  color: inherit;
  text-decoration: none;
  box-shadow: inset 0 -2px 0 var(--accent);
  border-radius: 2px;
}
.versions-status:not(:empty) {
  margin-top: 8px;
}
/* The recommendation at the top of the advice card: a label, the model in
   large type, the tier control, and one sentence of why. Boxed in the accent
   so it reads as the card's answer and the controls under it as its
   settings. */
.recommend {
  display: grid;
  gap: 6px;
  margin-bottom: 14px;
  padding: 12px 14px;
  border: 1px solid var(--accent);
  border-radius: 8px;
  background: var(--badge-bg);
}
.recommend-label {
  color: var(--muted);
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.07em;
  text-transform: uppercase;
}
.recommend-model {
  font-size: 1.25rem;
  font-weight: 600;
  line-height: 1.3;
  overflow-wrap: anywhere;
}
.recommend-model[hidden],
.tier-row[hidden] {
  display: none;
}
.recommend-why {
  margin: 0;
  font-size: 0.9rem;
}
.tier-row .tier-why::before {
  content: '· ';
}
/* The tie and the numbers behind it, closed by default: the recommendation
   is what the card is for, and this is what it was picked from. */
.evidence {
  margin-top: 14px;
}
.evidence > summary {
  cursor: pointer;
  color: var(--muted);
  font-size: 0.85rem;
  padding: 4px 0;
}
.evidence > summary:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 5px;
}
.evidence[open] > summary {
  margin-bottom: 6px;
}
/* Secondary to the recommendation above it, so set at the size of the notes
   around it rather than as the card's body text. */
.evidence .model-line {
  font-size: 0.85rem;
}
/* The "which models do you have" filter. It is a disclosure because 46 rows
   open by default would be the longest thing in a card whose job is the three
   lines of advice above it, and because the summary can carry the state -- so a
   closed filter still says whether it is filtering anything. */
.filter {
  margin-top: 14px;
}
.filter > summary {
  cursor: pointer;
  color: var(--muted);
  font-size: 0.85rem;
  /* A summary is a button. Give it the hit area of one. */
  padding: 4px 0;
}
.filter > summary:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 5px;
}
.filter-state::before {
  content: ' · ';
}
/* A fieldset for the group name, with the border and padding a fieldset brings
   with it taken back off: the card is already a box and a second one inside it
   is noise. */
.filter-body {
  border: 0;
  margin: 0;
  padding: 0;
  min-width: 0;
}
.filter-controls {
  margin: 8px 0;
}
.filter-controls .note {
  margin: 0;
}
/* Capped rather than as long as the list. Eleven groups of 46 rows is taller
   than most screens, and the results this filter narrows are directly below it:
   scrolling the list rather than the page is what keeps them both reachable. */
.filter-list {
  max-height: 320px;
  overflow-y: auto;
  border: 1px solid var(--line);
  border-radius: 7px;
  padding: 6px;
}
.org-group + .org-group {
  margin-top: 10px;
}
.org-name {
  color: var(--muted);
  font-size: 0.75rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  padding: 2px 4px;
}
/* The whole row is the label, so the name is part of the target rather than
   text beside a 13 px box. */
.model-option {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 5px 4px;
  border-radius: 5px;
  font-size: 0.88rem;
  cursor: pointer;
}
/* Hidden by a search. The display above would otherwise beat the user
   agent's [hidden] rule, as .figures' does, and a search for one model drew
   its vendor's whole group (#324). */
.model-option[hidden] {
  display: none;
}
.model-option:hover {
  background: var(--tok-a);
}
.model-option input {
  /* 16px is the browser default and it is a small target on a phone. */
  width: 18px;
  height: 18px;
  margin: 0;
  accent-color: var(--accent);
  flex: none;
}
.note {
  color: var(--muted);
  font-size: 0.85rem;
}
/* A rating, its margin and its vote count, one model per line. The page is set
   in a proportional sans, so without tabular figures a 1 is narrower than a 7
   and a column of numbers written to be compared does not line up. */
.model-line {
  font-variant-numeric: tabular-nums;
}
/* A refused tokeniser pick. Same rule as .boot-error: the element is in the
   document from first paint so the live region is registered before the text
   arrives, and only the box around it is conditional. Empty it draws nothing,
   which is why it can sit under the picker on every load. */
.picker-status:not(:empty) {
  margin-top: 8px;
  padding: 6px 10px;
  border: 1px solid var(--alert-line);
  border-radius: 7px;
  background: var(--alert-bg);
  font-size: 0.85rem;
}
/* The copy-link button under the prompt (#57). It starts the row, so it does
   not want the gap `button.link` keeps from a select beside it. The status line
   under it takes its gap only when it has something to say, like
   #encoder-scope. */
.link-row button.link {
  margin: 6px 0 0;
}
/* Where the link goes when the clipboard refuses it. It takes the rest of the
   row, because the link is long and the reader has to be able to see that it
   is all selected. */
.link-row input[type='text'] {
  flex: 1;
  min-width: 12rem;
  margin-top: 6px;
  padding: 4px 8px;
  font-size: 0.85rem;
  background: var(--card);
  color: var(--fg);
  border: 1px solid var(--line);
  border-radius: 7px;
}
.prompt-status:not(:empty) {
  margin-top: 4px;
}
/* A label that sits on its own line above its control. `.note` is the inline
   version and is used where the label shares a line with the control. */
.field-label {
  display: block;
  color: var(--muted);
  font-size: 0.85rem;
  margin-bottom: 6px;
}
/* Announced, never drawn. Used for the advice card's live summary, which is a
   one-line version of the card next to it: drawing it would print the same
   sentence twice, and not having it means a screen reader is told nothing when
   the advice changes. `clip-path` rather than the old `clip` rectangle, and
   `white-space: nowrap` so the text is not collapsed into a 1px column. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}
footer {
  margin-top: 36px;
  padding-top: 16px;
  border-top: 1px solid var(--line);
  color: var(--muted);
  font-size: 0.82rem;
}
footer a {
  color: var(--accent);
}

/* The Optimise block at the top of the Wordiness card: the button, what it
   saves, and the count before and after as two bars. The [hidden] rules are
   restated because the display values here would beat the user agent's. */
.optimise {
  display: grid;
  gap: 8px;
  margin-bottom: 14px;
  padding-bottom: 14px;
  border-bottom: 1px solid var(--line);
}
.optimise[hidden],
.optimise-button[hidden],
.optimise-undo[hidden] {
  display: none;
}
/* Beside the line it undoes, so no gap of its own on the left. */
.optimise-row button.optimise-undo {
  margin-left: 0;
}
.optimise-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px 12px;
}
/* The one filled button on the page: it is the action the card is for. */
.optimise-button {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--card);
  font-weight: 600;
  cursor: pointer;
}
.optimise-button:hover,
.optimise-button:focus-visible {
  filter: brightness(1.08);
}
.optimise-button:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
.optimise-summary {
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}
.optimise-cost {
  margin: 0;
}
.compare {
  display: grid;
  gap: 6px;
  margin: 0;
  padding: 0;
  list-style: none;
  font-size: 0.85rem;
  font-variant-numeric: tabular-nums;
}
.compare li {
  display: grid;
  grid-template-columns: 3.5rem minmax(0, 1fr) 6.5rem;
  align-items: center;
  gap: 10px;
}
.compare-label {
  color: var(--muted);
}
.compare-bar {
  height: 10px;
  border-radius: 5px;
  background: var(--badge-bg);
  overflow: hidden;
}
.compare-fill {
  display: block;
  height: 100%;
  width: var(--share, 0%);
  border-radius: 5px;
}
/* Before in the waste colour and after in the strip's green, so the bars say
   the same thing the strip does. */
.compare li[data-kind='before'] .compare-fill {
  background: var(--waste-edge);
}
.compare li[data-kind='after'] .compare-fill {
  background: var(--accent);
}
.compare-value {
  text-align: right;
}

/* The Findings card (slice 2). A mark's state is carried by an inset underline
   as well as its tint, so cost, guess and dismissed differ by more than colour. */
.lint-copy {
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.85rem;
  line-height: 1.9;
  margin: 8px 0 0;
}
.lint-copy mark {
  color: inherit;
  border-radius: 2px;
  cursor: pointer;
}
.lint-copy mark[data-type='cost'] {
  background: var(--waste-a);
  box-shadow: inset 0 -2px 0 var(--waste-edge);
}
.lint-copy mark[data-type='guess'] {
  background: var(--guess-bg);
  box-shadow: inset 0 -2px 0 var(--guess-line);
}
.lint-copy mark[data-dismissed] {
  background: none;
  box-shadow: none;
  color: var(--muted);
  /* Struck through, so a dismissed finding does not read as the dimmed pasted
     material below, which is muted with no line. */
  text-decoration: line-through;
}
.lint-copy mark[data-current] {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}
/* What the linter treated as pasted material and did not read (zones spec
   §2.3). Dimmed rather than hidden: it is still the reader's prompt. A long
   run is closed behind one line and a toggle instead (#415), and shown
   dimmed when the reader opens it. */
.lint-copy [data-skipped] {
  color: var(--muted);
}
.lint-collapsed {
  display: inline-flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 8px;
  color: var(--muted);
  font-family: inherit;
}
.lint-collapsed button {
  font-size: 0.8rem;
  padding: 1px 9px;
  cursor: pointer;
}
/* A flex row with a gap rather than a left margin on the button, so a button
   that wraps to the start of a line lines up with the text above it. The
   [hidden] rule is restated because a display value here would beat the user
   agent's. */
.lint-control {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 4px 8px;
}
.lint-control[hidden] {
  display: none;
}
.lint-control button {
  font: inherit;
  font-size: 0.8rem;
}
.lint-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin: 8px 0;
}
.lint-chips button {
  font: inherit;
  font-size: 0.8rem;
  border: 1px solid var(--line);
  border-radius: 12px;
  padding: 1px 9px;
  background: var(--card);
  color: var(--fg);
}
.lint-chips button[aria-expanded='true'] {
  border-color: var(--accent);
}
.lint-chips button[data-type='guess'] {
  border-style: dashed;
}
.lint-list {
  list-style: none;
  padding: 0;
  margin: 4px 0 8px;
  font-size: 0.85rem;
}
.lint-list li {
  display: flex;
  gap: 8px;
  align-items: baseline;
  padding: 3px 0;
  border-top: 1px solid var(--line);
}
/* Findings about the whole prompt (whole-prompt findings spec §5): no words to
   mark, so a row each above the copy, in the guess tint with the guess line on
   its edge. Dashed, like a guess chip, so it differs by more than colour. */
.lint-whole {
  list-style: none;
  padding: 0;
  margin: 8px 0 0;
  font-size: 0.85rem;
}
.lint-whole li {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 4px 8px;
  padding: 6px 10px;
  background: var(--guess-bg);
  border-left: 3px dashed var(--guess-line);
  border-radius: 2px;
}
.lint-whole li + li {
  margin-top: 6px;
}
/* The sentence on its own row, the buttons under it, as in the detail line. */
.lint-whole li > span:first-child {
  flex-basis: 100%;
}
.lint-whole li[data-dismissed] {
  background: none;
  border-left-color: var(--line);
  color: var(--muted);
}
.lint-whole li[data-dismissed] > span:first-child {
  text-decoration: line-through;
}
.lint-whole button {
  font: inherit;
  font-size: 0.8rem;
}
.lint-detail:not(:empty) {
  margin-top: 10px;
  padding: 6px 10px;
  border-left: 3px solid var(--accent);
  font-size: 0.9rem;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 6px;
}
/* The sentence takes its own row and the buttons start the next one at the
   same edge, instead of trailing the sentence and wrapping indented. */
.lint-detail > span {
  flex-basis: 100%;
}
/* What Optimise did, said to a screen reader on the card's own voice and not
   drawn, since the block above shows it (#438). The whole line is hidden, rule
   and padding too, the way .visually-hidden hides an element. */
.lint-detail:has(> .lint-said:only-child) {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}
/* No left margin: "Next finding" starts a line, and the buttons beside
   something else are spaced by their container's gap. */
.lint-detail button,
.lint-list button,
.lint-next {
  font: inherit;
  font-size: 0.8rem;
}
@media (max-width: 600px) {
  .lint-detail:not(:empty) {
    flex-direction: column;
    align-items: flex-start;
  }
}

/* The workbench (workbench spec §4). One column until there is room for two.
   From 1024px the prompt card is pinned on the left and the results scroll past
   it on the right, so the prompt and its token strip are on screen together. */
.bench,
.bench-results {
  display: grid;
  gap: 16px;
}
/* Grid items default to min-width: auto, which lets a long unbroken token strip
   widen its column past the page instead of wrapping inside it. */
.bench > *,
.bench-results > * {
  min-width: 0;
}
@media (min-width: 1024px) {
  .wrap {
    max-width: 1240px;
  }
  .bench {
    grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
    /* Stretched to the row, the left column would be as tall as the results,
       and a sticky element as tall as its container has nowhere to stick. */
    align-items: start;
  }
  .bench-input {
    position: sticky;
    top: 16px;
  }
  /* The prompt card is a column as tall as the screen allows, and the box
     takes whatever height the label, the examples, the stats bar, the link
     row and the line under it leave, so all of them are on screen with it. A
     box sized from the screen alone pushed the link row and that line into
     the card's own scroll at the height a laptop's browser leaves.
     dvh after vh, so a browser that knows the visible height uses it (an iPad
     held landscape is exactly as wide as this breakpoint) and one that does
     not keeps the line before. The card still scrolls if the screen is too
     short for even the smallest box; that is on the card rather than the
     column, so the column does not clip the card's own shadow.
     The height is the screen less where the card starts before the page has
     scrolled, not less the 16 px it sticks at. Sized for the stuck card, it
     ran about 100 px past the fold on load, so the count and the copy buttons
     under the box were off screen until the reader scrolled (#448). On
     2026-10-12 the card started 135 px down at 1024, 1280, 1366 and 1440 px
     wide; 135 and 16 below the card make 151. Stuck, it ends that far short
     of the screen's foot, which costs the box that much and nothing else. */
  .prompt-card {
    display: flex;
    flex-direction: column;
    max-height: calc(100vh - 151px);
    max-height: calc(100dvh - 151px);
    overflow-y: auto;
  }
  /* Everything but the box keeps the height of what it holds. */
  .prompt-card > * {
    flex: none;
  }
  /* The height here is the most the box is given, and the textarea rule's
     min-height the least. It is the basis rather than a flex-basis, because a
     hand resize writes a height and a flex-basis would ignore it: dragging
     the corner still makes the box shorter, and taller again up to the room
     the card has. */
  .bench-input textarea {
    flex: 1 1 auto;
    height: 640px;
  }
}
/* On a short screen the box gives way before the card has to scroll. With an
   example over the reader's text, its line and "Put my prompt back" under the
   box, the rest of the card was 284 px in Chromium on Windows at 1280 x 610
   (2026-10-12), leaving the box 13 px over its 160 px least, and on CI's
   Linux fonts the card scrolled. A box of 6rem still shows five lines. */
@media (min-width: 1024px) and (max-height: 720px) {
  .bench-input textarea {
    min-height: 6rem;
  }
}
/* The example prompts: one button at the end of the prompt card's title row,
   and the list it opens laid over the top of the box. The title's capitals and
   spacing are put back to the page's, since the button sits inside it. */
.presets {
  position: relative;
  text-transform: none;
  letter-spacing: normal;
  font-weight: 400;
}
.presets-toggle {
  font-size: 0.8rem;
  padding: 3px 11px;
  border-radius: 999px;
  cursor: pointer;
}
/* The chevron is decoration: the empty alternative text keeps it out of the
   button's name where the syntax is supported, and the first declaration is
   the fallback where it is not. */
.presets-toggle::after {
  content: ' ▾';
  content: ' ▾' / '';
}
.presets-toggle:hover,
.presets-toggle:focus-visible,
.presets-toggle[aria-expanded='true'] {
  border-color: var(--accent);
}
.presets-menu {
  position: absolute;
  top: calc(100% + 6px);
  right: 0;
  z-index: 10;
  min-width: 13rem;
  margin: 0;
  padding: 4px;
  list-style: none;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: 8px;
  box-shadow: 0 6px 20px rgb(0 0 0 / 18%);
}
.presets-menu[hidden] {
  display: none;
}
.presets-menu button {
  display: block;
  width: 100%;
  text-align: left;
  border: 0;
  border-radius: 5px;
  background: none;
  padding: 6px 10px;
  font-size: 0.85rem;
  cursor: pointer;
}
.presets-menu button:hover,
.presets-menu button:focus-visible {
  background: var(--tok-a);
}
.presets-menu button:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}

/* 404.html, the page an address with no file behind it gets (#450). One card,
   in the page's own colours, with the way back to the page. */
.not-found {
  max-width: 32rem;
  margin: 40px auto 0;
}
.not-found h1 {
  font-size: 1.3rem;
  margin: 0 0 8px;
}
.not-found p {
  margin: 0 0 8px;
}
.not-found a {
  color: var(--accent);
}
