/*
 * The components layer: a button, a field, a chip, a menu, a sheet and the
 * focus policy, each written once against the token names, for every product
 * to import beside its token file.
 *
 * This file exists because the products were retyping it. vorlaut and mitreden
 * carried the identical button.primary rule; bildhaft carried the same values
 * under .btn--primary; bildhaft's one-line focus policy reached mitreden by
 * hand-copy, in a commit that says it is doing so. Shared in fact, unshared in
 * name — which is how a rule drifts.
 *
 * Two rules govern what is in here:
 *
 * - Only what the products demonstrably duplicated. Product layout — vorlaut's
 *   board of keys, mitreden's phrase list, bildhaft's print styles — is
 *   identity, not vocabulary, and stays home. So do list rows: separator-rows
 *   against cards is density, and density is per product (design.md §2).
 *   (This line used to say "vorlaut's tile grid", and the picker grid below is
 *   not the exception it looks like: vorlaut's board is a layout of *keys*,
 *   drawn once for that product. What moved here is the grid of labelled
 *   picture buttons that wochenwerk and bildhaft wrote separately, in the same
 *   words, under the same two class names.)
 *
 * - Colour comes only from the tokens. There is no literal in this file, so it
 *   renders contrast-checked under any accent the generator accepts.
 *
 * The class vocabulary is the agreement's, not any one product's: a base class
 * and plain modifier words, the way vorlaut and mitreden already speak.
 * bildhaft migrates its BEM names (.btn--primary becomes class="btn primary")
 * when it adopts. Everything is opt-in by class; importing this file restyles
 * nothing by itself except :focus-visible — deliberately, because the one rule
 * that was already true everywhere should not be optional.
 */

/* ------------------------------------------------------------------- focus */

/* Never removed, never replaced by a border change alone. Fields add a lift
   and an accent border on :focus below; the ring itself is this rule's. */
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* ----------------------------------------------------------------- buttons */

/* Three tiers, no more. Primary takes the accent fill and there is one per
   view. The unmodified base is the normal tier. Quiet is transparent.
   Destructive is not a fourth tier but a colour on a quiet button; only the
   confirm button of a destructive dialog takes .filled. */
.btn {
  display: inline-flex; align-items: center; gap: 7px; white-space: nowrap;
  font: inherit; font-size: 14px; font-weight: 600;
  border-radius: var(--radius-sm); padding: 9px 16px;
  border: 1px solid var(--line); background: var(--surface-2); color: var(--text);
  cursor: pointer;
}
.btn:hover:not(:disabled) { background: var(--surface-3); }
.btn:disabled { opacity: .4; cursor: default; }

.btn.primary { background: var(--accent); color: var(--accent-ink); border-color: transparent; }
.btn.primary:hover:not(:disabled) { background: var(--accent-hover); }

.btn.quiet { background: transparent; border-color: transparent; color: var(--text-dim); }
.btn.quiet:hover:not(:disabled) { background: var(--surface-2); color: var(--text); }

.btn.destructive { background: transparent; border-color: transparent; color: var(--danger); }
.btn.destructive:hover:not(:disabled) { background: var(--danger-soft); }

/* The filled confirm holds its fill under the pointer: there is no
   --danger-hover token, and inventing the shade here is how 2.48:1 happened. */
.btn.destructive.filled { background: var(--danger); color: var(--danger-ink); border-color: transparent; }
.btn.destructive.filled:hover:not(:disabled) { background: var(--danger); }

/* An icon-only button is a normal or quiet button at pill radius. */
.btn.icon { border-radius: var(--radius-pill); padding: 8px 11px; line-height: 1; }

/* The small tier. All three products wrote `.btn.sm` in their markup and this
   file never defined it, so each invented one: 6px 9px at 13px, 5px 11px at
   13.5px, and - in the third - nothing at all, which meant the class was inert
   and the button rendered full size. A 7px spread across the family, from a
   class everybody thought they were using.

   These are bildhaft's numbers. The horizontal padding stays proportional to
   .btn's 16px, which is what keeps a small button reading as the same object
   at a smaller size rather than as a differently shaped one. */
.btn.sm { padding: 5px 11px; font-size: 13.5px; }

/* This file sets display on .btn and .toast, and an author display rule beats
   the browser's own [hidden] { display: none } — so the file that takes
   hidden's meaning away must give it back. */
.btn[hidden], .toast[hidden] { display: none; }

/* ------------------------------------------------------------------ fields */

/* A field is a fill, not a hole: --surface-2 at rest with a transparent
   border, lifting to --surface with an accent border on any focus. The ring
   arrives only via :focus-visible — a mouse click gets the lift without the
   ring, which is the behaviour bildhaft already has and design.md intends.
   font: inherit, always: a sentence typed in must look like the same sentence
   read back out. */
.field {
  font: inherit; font-size: 15px; width: 100%;
  background: var(--surface-2); color: var(--text);
  border: 1px solid transparent; border-radius: var(--radius-sm);
  padding: 10px 13px;
}
.field::placeholder { color: var(--text-faint); }
.field:focus { background: var(--surface); border-color: var(--accent); }

.lbl { display: block; font-size: 13px; color: var(--text-dim); margin-bottom: 7px; }

/* ------------------------------------------------------------------- chips */

/* Filters are pills, actions are boxes. A chip changes what is shown, never
   what is stored. Selected takes the accent tint, never the full fill — the
   full fill belongs to the primary action alone. The state lives on
   aria-pressed rather than a class, because a filter a screen reader cannot
   hear toggling is not a filter; mitreden's .chip.on migrates to the
   attribute. */
.chip {
  font: inherit; font-size: 13px; cursor: pointer;
  background: transparent; color: var(--text-dim);
  border: 1px solid transparent; border-radius: var(--radius-pill);
  padding: 6px 13px;
}
.chip:hover { background: var(--surface-2); color: var(--text); }
/* The count is NOT faded. The .55 the audit once blessed re-breaks the very
   contrast the tokens are solved for: mitreden measured a faded count at
   2.91:1 over --accent-soft, and took the fade out. Tokens carry the
   hierarchy; opacity would undo their guarantee. */
.chip .n { font-variant-numeric: tabular-nums; margin-left: 5px; }
.chip[aria-pressed="true"] {
  background: var(--accent-soft); color: var(--accent-strong); font-weight: 600;
}

/* ---------------------------------------------------------- overflow menu */

/* The closest agreement in the audit: the two products differed by one glyph,
   10px of width and 2px of radius. This is the agreed one. Destructive rows
   sit last, in --danger, over --danger-soft. The trigger carries
   aria-haspopup="menu" and a live aria-expanded; the popup carries
   role="menu", its rows role="menuitem" — or role="menuitemradio" where the
   menu is a set of alternatives rather than a list of commands, in which case
   exactly one carries aria-checked="true".

   The keyboard half of that contract is not drawing and so is not in this
   file, but it is part of the same pattern and the products got it wrong
   separately: opening a menu moves focus to its first enabled row, the arrows
   and Home/End walk them, and Escape closes and hands focus back to the
   trigger. A popup nobody can reach is styled, not built. */
.menu {
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius-sm); box-shadow: var(--shadow);
  padding: 6px; min-width: 196px;
}
.menu button {
  display: block; width: 100%; text-align: left; white-space: nowrap;
  font: inherit; font-size: 14px; cursor: pointer;
  background: none; border: none; color: var(--text);
  border-radius: var(--radius-item); padding: 8px 11px;
}
.menu button:hover:not(:disabled) { background: var(--surface-2); }
.menu button:disabled { opacity: .4; cursor: default; }
.menu hr { border: none; border-top: 1px solid var(--line); margin: 5px 0; }
.menu button.danger { color: var(--danger); }
.menu button.danger:hover:not(:disabled) { background: var(--danger-soft); }

/* The visible half of menuitemradio, and the reason it is here rather than in
   a product: aria-checked said which alternative was in force and nothing said
   it to anyone not using a reader. vorlaut shipped two such menus that way —
   the selection could only be inferred from the trigger behind the open list —
   and mitreden's language menu was about to be a third.

   The accent, because "the one in force" already looks like this everywhere
   else in this family: it is what the voice picker's selected row uses. Space
   is reserved on every row of a radio menu rather than only the checked one,
   so the labels do not shift by 19px when the choice moves. */
.menu button[role="menuitemradio"] { position: relative; padding-right: 30px; }
.menu button[aria-checked="true"] { color: var(--accent-strong); font-weight: 650; }
.menu button[aria-checked="true"]::after { content: "\2713"; position: absolute; right: 11px; }

/* Anchored under its trigger with a 6px gap, right-aligned to it. The
   geometry lives on the wrapper so a menu can also stand alone — the gallery
   shows one unanchored. */
.menu-anchor { position: relative; display: inline-flex; flex: none; }
.menu-anchor .menu { position: absolute; right: 0; top: calc(100% + 6px); z-index: 30; }

/* Right-aligned suits a trigger sitting at the right edge of a row, which is
   where the overflow ⋯ lives and why it is the default. A trigger at the left
   of a wide panel — a settings row's language or rendering picker — wants the
   opposite, or the list hangs leftward off the panel it belongs to. Same
   promotion as .dropdown above: mitreden wrote it, vorlaut is the second
   product to need it. */
.menu-anchor.start > .menu { left: 0; right: auto; }

/* Upward, where there is more room above the trigger than below it. The
   measuring half is `svelte/fit.js`, which also writes the height cap and the
   scroll that cap needs; this is the drawing half and it has to be here,
   because `@lautstark/design/svelte/Overflow` emits the class and §4.12 says a
   module that emits a class name ships the rule for it.

   Without the flip, a menu near the foot of a sheet — vorlaut's eleven word
   classes — is capped at nothing worth opening. vorlaut wrote both halves and
   is still the only implementation of either; it comes here with the component
   rather than staying behind it. */
.menu-anchor--up > .menu { top: auto; bottom: calc(100% + 6px); }

/* The trigger that opens one: a button with a chevron, because pressing it asks
   a question rather than doing the thing. mitreden wrote this first and said in
   the margin that it became shared the day a second product needed the same
   trigger; vorlaut's language and rendering pickers are that day, so here it is.

   Masked rather than drawn as a background image, so the mark takes the
   button's own colour in both schemes instead of needing a second SVG per
   scheme — design.md §8.1's desync trap, of which vorlaut's select chevron was
   the last example. The hex inside the URI is the one literal in this file and
   it never renders: a mask uses the shape and throws the colour away, which is
   the whole reason it is a mask.

   It must not restate the button's shape. An earlier version of this rule also
   set border-radius and padding, which silently outranked .btn — the trigger
   drew itself at 999px while a plain .btn beside it drew itself at 9px. A
   trigger adds an affordance to a button; it does not redraw one. */
.dropdown {
  --chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='7'%3E%3Cpath d='M1 1l4 4 4-4' fill='none' stroke='%23000' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
}
.dropdown::after {
  content: ""; width: 10px; height: 7px; flex: none; background: currentColor;
  -webkit-mask: var(--chevron) no-repeat center/10px 7px;
  mask: var(--chevron) no-repeat center/10px 7px;
}
.dropdown[aria-expanded="true"] { background: var(--surface-2); color: var(--text); }

/* The `.field` variant, which §6.10 argues for in a form row and which did not
   draw until 2026-09-17.
   
   `.dropdown::after` is `flex: none` and presumes a flex container. `.btn`
   supplies `display: inline-flex`, so `.btn.dropdown` has always worked;
   `.field` supplies no display at all, which suits the `<input>` it was written
   for and not a `<button>` with a mark at its far end. Measured on a live
   `<button class="field dropdown">`: the trigger computes `display: block` and
   takes the user agent's `text-align: center`, and the chevron computes
   `display: inline`, where its 10x7 does not apply — so no mark is drawn and
   the answer centres itself.

   Two products wrote this rule before it came here, which is this file's own
   admission bar met twice: vorlaut has carried it in `ui.css` with a comment
   saying `.field` leaves display alone, and wochenwerk met the same wall and
   declined to write it a second time, using `.btn.dropdown` instead and saying
   a display on `.field` is design's to add. It was right. Layout only —
   everything the trigger is drawn with, the fill, the border, the radius, the
   padding and the focus, is still `.field`'s. */
.field.dropdown {
  display: flex; align-items: center; justify-content: space-between; gap: 7px;
  text-align: left; cursor: pointer;
}

/* ------------------------------------------------------------------- sheet */

/* The dialog silhouette both products drew, with bildhaft's head/body/foot
   anatomy — adopted because it survives a dialog growing a second action.
   Works on a <div> or a native <dialog>; the plumbing (backdrop, focus trap,
   scrolling) stays per product, and the backdrop weight is deliberately not
   settled here — the audit left that fork open. A sheet with both a corner ✕
   and a footer dismiss must not give them the same accessible name. */
/* Two widths, as tokens, because `wide` was a class this file *named* and did
   not draw — dialog.d.ts offers it, `openDialog` marks it, and nothing here
   gave it a meaning. So three products invented one: bildhaft
   `max-width: min(100% - 48px, 1060px)`, wochenwerk `min(900px, 94vw)`,
   mitreden a plain `calc(100% - 40px)` on the ordinary sheet. That is not four
   products disagreeing about design; it is one hole in this file, filled four
   ways. A product that wants a different figure sets the token and says why,
   which is a decision somebody can find. */
:where(.sheet) { --sheet-w: min(600px, calc(100% - 40px));
  --sheet-w-panels: min(900px, calc(100% - 40px));
  /* The air above and below a sheet, and the only place it is written.
     The anchoring rule and the height limit are both derived from it, so
     they cannot disagree: a sheet at its tallest reaches exactly the gap
     it starts at. Left to the browser these were two figures from the UA
     stylesheet that happened to match. */
  --sheet-gap: 19px;
  --sheet-w-wide: min(1060px, calc(100% - 48px)); }

.sheet {
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius); box-shadow: var(--shadow);
  padding: 22px; width: var(--sheet-w); max-width: 100%;
  /* A sheet is usually a grid or flex item, and such an item's automatic
     minimum size is its content's min-content width — so one long unbroken
     line inside inflates the whole dialog past the space it was given, and it
     hangs off the side of a phone. It must clamp to its container and let the
     content deal with the width it gets. */
  min-width: 0;
}
.sheet.panels { width: var(--sheet-w-panels); }
.sheet.wide { width: var(--sheet-w-wide); }
/* Anchored rather than centred. Both figures are derived from `--sheet-gap`
   rather than chosen separately, so a sheet at its tallest reaches exactly the
   gap it starts at and the heading is in the same place in either state.

   A dialog's `margin: auto` centres it vertically, so a sheet that grows spends
   half the growth upwards. These sheets are columns of <details>, and opening
   one is the commonest thing anybody does in them: measured in wochenwerk at
   1280x900, the heading travelled 149px upward while the reader was looking at
   something near the bottom. Reported as "it jumps whenever you pick
   something", which is exactly what it is. */
.sheet {
  margin-block: var(--sheet-gap) auto;
  max-height: calc(100% - var(--sheet-gap) * 2);
}
/*
 * The three regions are the sheet's OWN children, and the selectors have to
 * say so. As descendants they reached anything nested that happened to share
 * a name — and a panel's body is a .body, so every panel inside a sheet was
 * quietly taking the sheet body's colour, its font size and its padding. The
 * vocabulary is deliberately small and reused, which is exactly why it has to
 * be scoped.
 *
 * The other half of that rule belongs to the products: a name this file owns
 * must not double as a JavaScript hook. mitreden carried a paragraph marked
 * class="sub body" only so its code could find it, and it wore the sheet
 * body's font size from the day it adopted this file — invisibly, because a
 * name that appears solely inside a querySelector cannot be found by reading
 * stylesheets. The check has to be a live one, and it generalises past this
 * component: for any name here, enumerate the elements carrying it whose role
 * in the markup is not the component's — a .body whose parent is not the
 * sheet, a .field on a <div> that wraps other elements. The narrow version of
 * this check, written first, was scoped to sheet regions and would have missed
 * the second of those.
 */
.sheet > .head {
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px; margin-bottom: 8px;
}
.sheet > .head :is(h2, h3, h4) { margin: 0; font-size: 17px; font-weight: 650; letter-spacing: -.01em; }
.sheet > .body { color: var(--text-dim); font-size: 14px; }
.sheet > .body > p { margin: 0; }
.sheet > .body > p + p { margin-top: 10px; }
.sheet > .foot {
  display: flex; align-items: center; gap: 9px; justify-content: flex-end;
  border-top: 1px solid var(--line); margin-top: 18px; padding-top: 14px;
}

/* --------------------------------------------------------------- tile grid */

/* A grid of labelled picture buttons: a symbol picker, a chosen-symbol row, a
   list of things whose difference is a picture rather than a word.

   wochenwerk (src/kalender.css) and bildhaft (src/styles/app.css) wrote this
   separately, in the same declarations, under the same two class names — and
   wochenwerk's copy says „bildhaft's picker, at this size", which is this
   file's admission bar met in writing. Three differences between the two and
   none of them is a disagreement: the tile's minimum width (84px against
   102px), a `margin-top` bildhaft spends on the grid and wochenwerk does not,
   and one pixel of vertical padding.

   The width is the only one that is really per product, so it is the token.
   `repeat(auto-fill, minmax(…, 1fr))` is what makes this a grid of squares
   rather than a row that scrolls, and the figure in the minmax is what "at
   this size" means; wochenwerk's tight variant is the same rule with a smaller
   number, so a token is also what stops that being a second grid rule. The
   margin above the grid stays with the page that puts it under something.

   The BEM spelling is deliberate and is the one exception in this file. These
   are the names both products already emit, and wochenwerk carries five more
   `.picker__item` rules that stay home — the drag affordance, the carried
   tile, the thumbnail size, the tight variant's hidden thumbnail, and the
   dashed empty slot. Renaming here would orphan all five for nothing. */
:where(.picker__grid) {
  /* The smallest a tile may be before the row wraps. wochenwerk's tight
     variant sets 56px; bildhaft, whose tiles hold a 82px picture, sets 102px. */
  --tile-min: 84px;
}
.picker__grid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(var(--tile-min), 1fr)); gap: 8px;
}
/* `color` and `font` because this is a <button>: the user agent gives it
   neither the page's colour nor the page's font, and a tile that reads in the
   system UI font is the one thing about this control that looks unfinished.
   wochenwerk's copy has both and bildhaft's leans on a reset elsewhere. */
.picker__item {
  display: flex; flex-direction: column; align-items: center; gap: 5px; padding: 8px 6px;
  border: none; border-radius: var(--radius-sm); background: var(--surface-2); color: var(--text);
  font: inherit; cursor: pointer; transition: background .13s, box-shadow .13s;
}
.picker__item:hover { background: var(--surface-3); }
/* A class rather than `[aria-pressed="true"]`, which is how `.chip` marks its
   choice. The two controls are not the same question: a filter chip toggles and
   says so, and half these tiles do not — bildhaft's close the dialog and hand
   back an answer, so `aria-pressed` on them would announce a state they do not
   have. Which of the two a tile is is the caller's to say. */
.picker__item--active { background: var(--accent-soft); box-shadow: inset 0 0 0 2px var(--accent); }
/* The label, and the reason a tile stays square. One long word without this is
   a tile twice as wide as its neighbours, and `auto-fill` then re-lays the
   whole grid around it. Verbatim in both products. What is not here is the
   label's size and colour: wochenwerk uses `.small` and the page's colour,
   bildhaft 12px and `--text-dim`, and that is a real difference. */
.picker__item > span {
  max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* ------------------------------------------------------------------ text */

/* Three words for text that is not the main text: smaller, dimmer, dimmest.
   Found on 2026-09-16 by the first CSS contract test (sicherung's, written for
   conventions.md §4.12): both of sicherung's panels put `small`, `muted` and
   `faint` on the hint under the folder, and nothing here drew them. bildhaft
   and wochenwerk had each written the three for themselves with the same
   values; mitreden and vorlaut-editor had not, so there the hint stood at body
   size in body colour. That is the §4.12 drift one level down from the panel.
   The values are the ones the two products agreed on, and 13.5px is what
   `.btn.sm` already uses for the same job. A product that still carries its
   own copy is shadowing these and shadows.js says so. */
.small { font-size: 13.5px; }
.muted { color: var(--text-dim); }
.faint { color: var(--text-faint); }

/* ------------------------------------------------------------ empty states */

/* An empty state teaches the one thing the user does not yet know; a
   filtered-empty is a different, shorter sentence and must never be confused
   with the first. */
.empty {
  text-align: center; padding: 34px 20px;
  border: 1px dashed var(--line); border-radius: var(--radius-sm);
  color: var(--text-dim);
}
.empty b { display: block; color: var(--text); font-size: 15px; margin-bottom: 5px; }
.empty small { font-size: 13px; color: var(--text-faint); }

/* --------------------------------------------------------------- messages */

/* Three kinds of message, three treatments (design.md §4.3). The notice is
   the outcome line: it stays until something replaces it, and is never
   auto-dismissed if it names a failure or a number. The toast is the aside —
   an acknowledgement of something already visible — and may clear itself. */
.notice {
  border-radius: var(--radius-sm); padding: 11px 14px; font-size: 14px;
  background: var(--accent-soft); color: var(--accent-strong);
}
.notice.bad { background: var(--danger-soft); color: var(--danger); }

/*
 * The way out.
 *
 * "Stays until something replaces it" was written about a line that reports a
 * refusal, and it is right about that one. Applied to „Alltag zu Hause
 * hinzugefügt." it left a sentence about last Tuesday sitting at the top of the
 * list, because nothing ever came to replace it. A ✕ is not auto-dismissal —
 * §4.3's bar is still that the product may not clear the line on a timer — it
 * is the reader deciding they have read it.
 *
 * It is a `.btn.quiet.icon`, not a control this component owns. The sheet
 * already puts a corner ✕ on one and there is no reason for the notice to have
 * a second kind; what the notice overrides is one declaration, below.
 *
 *   <p class="notice">
 *     „Alltag zu Hause" hinzugefügt.
 *     <button class="btn quiet icon" aria-label="Meldung ausblenden">✕</button>
 *   </p>
 *
 * The ✕ needs an accessible name that says what it dismisses. Two cautions,
 * both learned next door: the sheet's rule about not giving a corner ✕ and a
 * footer dismiss the same name applies here whenever both are on screen; and a
 * notice is usually inside a live region, whose announcement is its whole text
 * content — so the button's name is read out again every time the line
 * changes, which is an argument for „Meldung ausblenden" over anything longer.
 */

/* Keyed off :has() rather than a modifier class, the way .collections__item
   reads its own second line: the row layout and the thing that needs it stay
   one fact instead of two that have to agree. That is not only tidiness here.
   Both siblings compose this class with others — bildhaft writes
   `standing notice bad` onto its backup line, mitreden toggles `notice` onto
   the same line — and mitreden's settings notice is a `<p>` holding a <b> and a
   <span> in inline flow, which display:flex on the base rule would break into
   two columns. Hung off :has(), the plate they use cannot change, rather than
   being promised not to. Where :has() is missing the ✕ falls back into flow:
   below the sentence, still a button, still named.

   flex-start, not the centre .sheet > .head uses for its corner ✕. A sheet head
   holds a title; this holds the sentence that says what happened *including
   what partly failed*, which is the message kind most likely to run to two
   lines — and a ✕ centred against a paragraph sits level with nothing. Against
   the first line it sits level with the text it dismisses. Same reasoning as
   .collections__item:has(.collections__sub), and the same trigger. */
.notice:has(> .btn) { display: flex; gap: 12px; align-items: flex-start; }

/* margin-left rather than a grow on the text: the sentence is usually a bare
   text node, and an anonymous flex item cannot be given flex-grow. */
.notice > .btn { flex: none; margin-left: auto; }

/* The one declaration the notice overrides, and the reason is measured. The
   quiet tier hovers to --surface-2 because it assumes it is sitting on the
   page's ground; on this plate that fill is 1.08:1 in the light scheme — under
   the 1.15 floor the audit uses for "these two planes can be told apart", so
   the hover is invisible in exactly the scheme most people run. --surface-3 is
   1.20:1 light and 1.75:1 dark over both plates, and §4.2 already reserves it
   for "under the pointer, never at rest", which is where the pointer is.

   Only the fill. The tier keeps --text-dim at rest (5.60:1 over the plates at
   worst, all three products, both schemes) and --text under the pointer (7.57:1
   over --surface-3 at worst). Taking the plate's own ink instead, which the
   Compose port does, is right on a touch screen that has no hover and not
   available here: --accent-strong is not solved against --surface-3 and
   measures 3.73:1 over it in wochenwerk's dark scheme (2.88:1 in vorlaut's
   before it was solved against --surface and --surface-2 too), so an ink ✕
   would have nowhere legible to go. */
.notice > .btn.quiet:hover:not(:disabled) { background: var(--surface-3); }

.toast {
  display: inline-block;
  background: var(--text); color: var(--bg);
  border-radius: var(--radius-pill); padding: 9px 17px;
  font-size: 14px; box-shadow: var(--shadow);
}

/* Empty, it draws nothing - and it has to stay in the page while it is empty,
   because it is a live region (§3.8). Not [hidden] and not display:none: both
   take it out of the accessibility tree, which is the arrangement that made
   every message in one of these products silent. Stripped of its fill and
   padding an empty inline-block measures 0x0 and paints nothing.

   The rule above this one - `.toast[hidden] { display: none }` - is the trap
   next door. It exists because an author display rule beats the browser's own
   [hidden], so the file that takes the attribute's meaning away has to give it
   back; it must not be pointed at the region itself. */
.toast:empty { background: none; box-shadow: none; padding: 0; }

/* ---------------------------------------------------------------- rename */

/*
 * The name of the thing you are looking at, as the field that renames it.
 *
 * §1.6 settles that renaming is typing in the title, and @lautstark/design/rename
 * owns the timing - the debounce, the write on the way out, the guard against a
 * repaint typing over somebody mid-word. What was never shared is how it looks,
 * so the three drifted to 19px, 17px and 15px, at two weights and two paddings:
 * the same control, the same behaviour, three sizes.
 *
 * 17px is the middle of the three. A title field has to read as the page's
 * heading without becoming a display size in a product whose work head is
 * already a full-width band.
 *
 * It is a field that does not look like one until it is asked to. A border that
 * appears on hover is what says "this is editable" without drawing a box around
 * the page's title at rest.
 */
.title-input {
  flex: 1; min-width: 0; margin: 0;
  font: inherit; font-size: 17px; font-weight: 650;
  padding: 4px 8px;
  border: 1px solid transparent; border-radius: var(--radius-sm);
  background: none; color: var(--text);
}
.title-input:hover { border-color: var(--line); }
.title-input:focus {
  outline: none; border-color: var(--accent); background: var(--surface);
}
.title-input::placeholder { color: var(--text-dim); font-weight: 400; }
.title-input:disabled { color: var(--text-dim); }

/* -------------------------------------------------------------- standing */

/*
 * The state of a standing arrangement — something set up once that then runs
 * unattended, and can therefore stop without anybody being told. Two of them
 * now: the backup folder, which @lautstark/sicherung drives, and the licensed
 * symbol folder, which @lautstark/bildquelle does. Both are a folder somebody
 * connected once and will not be told about again, which is the whole of what
 * this line is for, and the two panels sit in the same settings column — so
 * they read the same by using the same class rather than by two modules
 * agreeing to.
 *
 * A fourth kind of message, and it earns the place §4.3 does not yet give it.
 * A .notice is the outcome of an action just taken and a .toast is an aside
 * about something already visible — both are about a moment. This is about a
 * condition that persists between visits, which is exactly the thing neither
 * of them can say. It is not called .status: mitreden already owns that name
 * for its inline message line (design.md §2, "Status and toast"), and two
 * meanings on one class in one file is how a rule drifts.
 *
 * ## The dot answers one question: is it writing?
 *
 * Three colours, no more, and the restraint is the point rather than a budget.
 * The tempting palette here is a four-state traffic light — green, amber, red,
 * grey — which would need --ok and --warn, a trio that only mitreden declares
 * and that exists for per-item state (recorded / changed / not yet recorded),
 * not for this.
 *
 * It is also the wrong model. "Permission needs re-confirming" feels like an
 * amber, but what it MEANS is that nothing is being written — the same as a
 * failure, from the only point of view that matters. A backup that has quietly
 * stopped is worse than no backup, because it manufactures confidence, and
 * painting that amber is how it gets read as fine. So:
 *
 *   accent-strong  it is writing
 *   danger         it is not writing, and you would not otherwise know
 *   text-faint     it is off, and you knew that
 *
 * Every one of those pairings against --bg is in the generator's audit, so
 * this file adds no ratio the tokens do not already guarantee.
 *
 * ## data-state takes the Status kind verbatim
 *
 * The attribute values ARE the emitting package's own `kind`, passed through
 * with no translation table in between. A mapping in each product is three
 * chances to disagree about what "failed" looks like, and the drift would be
 * invisible until the day it mattered. The cost is that a new kind in either
 * package is a breaking change for the CSS too — which RELEASING.md says out
 * loud, because an unstyled state falls back to the grey "off" dot and would
 * read as *deliberately not set up*.
 *
 * **Two packages write into one attribute, and their vocabularies are
 * disjoint.** @lautstark/sicherung says `off`, `idle`, `saving`,
 * `needs-permission`, `held`, `failed`, `unsupported`; @lautstark/bildquelle
 * says `ready`, `needs-setup`, `loading`, `error`. Checked on 2026-09-03,
 * because a shared attribute whose two writers collided on one word would draw
 * a symbol folder in the colours of a backup and nothing would say so. Either
 * package minting a kind the other already uses is the thing to refuse.
 */
.standing {
  display: flex; align-items: baseline; gap: 8px;
  font-size: 13px; line-height: 1.45; color: var(--text-dim);
}

/* Baseline-aligned text needs the dot nudged onto the x-height by hand; it has
   no baseline of its own to align. */
.standing .dot {
  flex: none; width: 8px; height: 8px; border-radius: var(--radius-pill);
  background: var(--text-faint); transform: translateY(-1px);
}

.standing[data-state="idle"] .dot,
.standing[data-state="saving"] .dot { background: var(--accent-strong); }

/* @lautstark/bildquelle's two working states, by the same question the block
   above answers: is the thing doing its job? A folder that is indexed answers
   every search, and one that is being read will. `needs-setup` and `error` are
   left to fall through — `no-folder` is "off, and you knew that", which is the
   grey the base rule already gives, and the two that are not are coloured by
   `.bad` below rather than by a state name, because whether a state is somebody
   to act on is the package's answer and not this file's. */
.standing[data-state="ready"] .dot,
.standing[data-state="loading"] .dot { background: var(--accent-strong); }

/* The dot follows the plate. Both panel modules put `notice bad` on this line
   in exactly the states their package's `needsAttention()` returns true for, so
   this rule says "coloured as a warning" once instead of listing state names
   twice over — and a state added to either package is drawn correctly without
   this file being touched.

   It changes nothing that was already drawn, and the argument is what shows it
   rather than a screenshot: `backup-panel` reaches `notice bad` only where
   @lautstark/sicherung's `needsAttention()` is true, which is `needs-permission`
   and `failed`, and the rule below already paints both dots danger. So the set
   this adds to is empty for every product on 2026-09-03. No visual baseline was
   re-recorded then, because no product emitted `.metacom-panel` yet. Three do
   now — bildhaft, vorlaut-editor and wochenwerk import
   `@lautstark/bildquelle/metacom-panel` — and their own visual suites are
   where that surface is checked. */
.standing.bad .dot { background: var(--danger); }

/* `held` joins these two rather than getting a colour of its own, and the rule
   at the top of this block is what decides it: the dot answers one question,
   is it writing, and held is not writing. It arrived with
   @lautstark/sicherung v1.3.0 — an export came back empty over a folder that
   is not, so the write was refused and somebody has to say which is right.
   Nothing is broken, but nothing is being saved either, and this file has
   already argued that those two want the same colour: "es funktioniert nicht"
   is a sentence somebody can put off and so is "nichts wird gesichert".

   Left out, it would have fallen through to --text-faint and read as calm.
   The products take data-state from the status kind verbatim, so a new kind
   is silently grey here until someone adds it — which is the one failure this
   whole feature exists to stop, arriving in the stylesheet instead. */
.standing[data-state="needs-permission"],
.standing[data-state="held"],
.standing[data-state="failed"] { color: var(--danger); }
.standing[data-state="needs-permission"] .dot,
.standing[data-state="held"] .dot,
.standing[data-state="failed"] .dot { background: var(--danger); }

/* The age of the last good copy, which is the one number that tells somebody
   whether they are safe. Never faded: it is most worth reading in the states
   that are already coloured, and an opacity on top of a solved token is how
   the family broke contrast three times (§1.1). */
.standing .age { color: inherit; font-variant-numeric: tabular-nums; }

/* --------------------------------------------------------------- segmented */

/*
 * One row of mutually exclusive choices with the live one lifted out of the
 * track. Both products drew it and one of them says in a comment that it took
 * the other's shape, which is this file's admission bar met out loud.
 *
 * The grounds were settled by looking, not by taste. The track is --bg and the
 * lifted segment is --surface, because "a raised plane sitting on --bg" is what
 * --surface *means* — and it is the only pairing that reads as raised in both
 * schemes. Marking selection with --surface-3 over a --surface-2 track lifts in
 * dark and sinks in light, because the plane ladder runs darker in light and
 * lighter in dark; a shadow under a segment that looks pressed in is the tell.
 * It also spends --surface-3, which §4.2 reserves for "under the pointer, never
 * at rest", and a selected tab is a rest state.
 *
 * Selection rides aria-pressed. Watch for the helper that writes a boolean true
 * as a bare attribute: aria-pressed="" matches nothing here and is invalid to a
 * screen reader — a selection that silently never shows, which has now happened
 * in this family twice.
 */
.segmented {
  display: flex; gap: 2px; padding: 3px;
  background: var(--bg); border: 1px solid var(--line);
  border-radius: var(--radius-pill);
}
.segmented button {
  flex: 1; padding: 6px 10px;
  border: none; border-radius: var(--radius-pill);
  background: transparent; color: var(--text-dim);
  font: inherit; font-size: 13.5px; cursor: pointer;
  transition: background .13s ease, color .13s ease;
}
.segmented button:hover { color: var(--text); }
.segmented button[aria-pressed="true"] {
  background: var(--surface); color: var(--text);
  font-weight: 600; box-shadow: var(--shadow-sm);
}

/* Labels long enough to need a second row — four German compound nouns will —
   wrap into two columns rather than crushing, and the track gives up its pill
   radius, which reads wrong drawn around two rows. */
@media (max-width: 560px) {
  .segmented { flex-wrap: wrap; border-radius: var(--radius); }
  .segmented button { flex: 1 1 45%; }
}

@media (prefers-reduced-motion: reduce) {
  .segmented button { transition: none; }
}

/* ------------------------------------------------------------------ panels */

/*
 * A folded section whose heading carries its state, so a look is enough and
 * opening is a decision. vorlaut built this first and got it right; it is
 * here rather than hand-copied into the second product, which is the whole
 * point of this file.
 *
 *   <details class="panel">
 *     <summary><span class="section">…</span><span class="state">…</span></summary>
 *     <div class="body">…</div>
 *   </details>
 *
 * Native <details>, so this component asks nothing of a product's JavaScript:
 * the browser owns the toggle, the keyboard behaviour and the semantics — the
 * same bargain <dialog> makes for the sheet. It is also what §3.4 asks for
 * structurally ("a settings block states its status in a sentence before it
 * offers a control") rather than as something each author must remember.
 *
 * A summary may not contain a button: a control nested in it would toggle the
 * panel when clicked, and reads as two things at once. Actions belong in the
 * body.
 *
 * The heading must say something from the first frame. A state fetched from a
 * database or a network answer leaves the summary blank until it arrives —
 * which is this component's one promise broken at the exact moment somebody is
 * reading it. Where the value cannot be known synchronously, say that instead:
 * "Wird geladen …" is a state, empty is not.
 */
.panel { border-top: 1px solid var(--line); }
.panel:last-of-type { border-bottom: 1px solid var(--line); }

.panel > summary {
  display: flex; align-items: baseline; gap: 10px;
  padding: 14px 16px; cursor: pointer;
  list-style: none;
}
/* Safari draws its own triangle in front of the row, on top of the one below. */
.panel > summary::-webkit-details-marker { display: none; }
.panel > summary:hover { background: var(--surface-2); }

.panel > summary .section { flex: none; font-size: 14px; font-weight: 600; }
/* The state gives way before the heading does: it is the line that can be
   long, and truncating it costs less than wrapping the row. */
.panel > summary .state {
  flex: 1 1 auto; min-width: 0;
  color: var(--text-dim); font-size: 12px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* The chevron, in the corner, pointing the way the panel will move. Two
   borders and a rotation — no SVG and no data URI, so it takes its colour
   from the token instead of from a hex baked into a URL that cannot read
   one (§8.1's desync trap, in the one place it keeps appearing). */
.panel > summary::after {
  content: ""; flex: none; width: 7px; height: 7px; margin-left: auto;
  border-right: 1.6px solid var(--text-dim);
  border-bottom: 1.6px solid var(--text-dim);
  transform: translateY(-2px) rotate(45deg);
  transition: transform .12s ease;
}
.panel[open] > summary::after { transform: translateY(1px) rotate(-135deg); }

/* Accent tint means "this one" (§4.3), and "the current source" is named in
   that rule. aria-current carries it, so the state is in the markup rather
   than in a class the assistive layer cannot see. */
.panel > summary[aria-current="true"] { background: var(--accent-soft); }
.panel > summary[aria-current="true"] .section { color: var(--accent-strong); }

/* The body is a region of its own, and needs to read as one: air under the
   heading it belongs to, and more under its last control than a hairline's
   width, since what follows is the next panel's edge. The 2px this started
   with was measured for a scroll container's clipping, not for reading. */
.panel > .body { padding: 10px 16px 22px; }

/* Narrow: the state gets its own line rather than an ellipsis. Truncating it
   would break the one promise this component makes — that the heading tells
   you the state — and a phone is exactly where opening every panel to find
   out costs the most. Grid, because in a wrapped flex row the chevron would
   follow the state onto a third line instead of staying with the heading. */
@media (max-width: 560px) {
  .panel > summary {
    display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 2px 10px;
    align-items: center;
  }
  .panel > summary .state {
    grid-column: 1;
    white-space: normal; overflow: visible; text-overflow: clip;
  }
  .panel > summary::after { grid-column: 2; grid-row: 1 / -1; margin-left: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .panel > summary::after { transition: none; }
}

/* ------------------------------------------------- the shared panels' own look
 *
 * `@lautstark/sicherung/ablage-panel` and `/backup-panel` hand a product a
 * finished block of markup — and every product then drew it itself. The rules
 * below were byte-alike in bildhaft, mitreden, vorlaut-editor and wochenwerk:
 * identical values, identical properties, four files.
 *
 * That is the failure this file exists to end, one level up from where it was
 * caught the first time. A module that emits class names into products where
 * nothing draws them has not shared a component; it has shared the *markup* and
 * distributed the *work*, and the four results then look different — which is
 * exactly what a shared panel was supposed to prevent.
 *
 * The test is `.collections` further down: module and rules in the same package.
 * That was the only surface in the family that had both, and it was the only one
 * that looked the same everywhere.
 */

/* The block a folder panel is: a column of short rows. */
.where-panel { display: grid; gap: 11px; }
.where-panel p { margin: 0; }

/* One statement about where things are, boxed so it reads as a state and not as
   another sentence. `.bad` is the folder that cannot be reached. */
.where {
  display: grid; gap: 2px; padding: 10px 12px;
  border: 1px solid var(--line); border-radius: var(--radius-sm);
  background: var(--surface);
}
.where b { font-size: 13px; font-weight: 600; }
.where.bad { border-color: var(--danger); }

/* Consent, under the folder it is about. */
.where-share { display: grid; gap: 6px; }
.where-panel .check { display: flex; align-items: center; gap: 9px; font-size: 13px; cursor: pointer; }
.where-panel .check input { flex: 0 0 auto; margin: 0; }

/* A picture of the folder, the way a file manager draws it: somebody who cannot
   follow a sentence about nesting recognises a shape they have seen a thousand
   times. Monospace because the indentation is the information. */
.tree {
  margin: 0; padding: 10px 12px; border: 1px solid var(--line);
  border-radius: var(--radius-sm); background: var(--surface); color: var(--text);
  font-family: var(--mono); font-size: 12px; line-height: 1.8; overflow-x: auto;
}

/* A row of things you can do. Wraps, because a panel narrows before its buttons
   do, and three of the four products had written exactly this. */
.acts { display: flex; flex-wrap: wrap; gap: 8px; }

/* The standing backup's block. Its status line is `.standing` above; this is
   only the column it sits in.
   
   **This one is a decision, not an extraction**, and the distinction matters
   because the rule at the head of this file is "only what the products
   demonstrably duplicated". Nobody drew `.backup-panel` — the class was emitted
   into four products and none of them styled it, so the block fell back to
   paragraph margins and each product's happened to differ. A class this file's
   own modules emit has to be drawn here (conventions.md §4.12), and drawing it
   means choosing a value. 10px is the gap `.where-panel` already uses at 11px,
   pulled in one step because this block is shorter and its rows are closer in
   kind. Visual baselines in all four products recorded the change. */
.backup-panel { display: grid; gap: 10px; }
.backup-panel > p { margin: 0; }

/* The licensed symbol folder's block, from `@lautstark/bildquelle/metacom-panel`.
   Same shape as the backup's and deliberately the same gap: the two are the two
   folder questions conventions.md §4.9 names, they sit in the same settings
   column, and a reader moving between them should be reading two answers rather
   than two designs. Everything else on it is vocabulary this file already owns —
   `.notice` for the licence, `.notice.bad` for the state that has to be acted
   on, `.standing` for the state line, `.acts` for the buttons.

   §4.12 is why the two rules below are here at all rather than in three
   products: the module emits these names, so this file draws them. */
.metacom-panel { display: grid; gap: 10px; }
.metacom-panel > p { margin: 0; }

/* The small print under a folder panel: where a licence comes from, why this
   browser will forget the folder, and — on an English page — that METACOM is
   named in German.

   A class of its own rather than the `.small muted` pair three of the four
   products happen to declare. Those two names are not in this file, so a module
   emitting them would be handing vorlaut-editor markup nothing there draws,
   which is exactly the failure §4.12 was written after. Scoped under the block
   for `.collections__*`'s reason: a name general enough to collide once will do
   it again, and `.note` is already vorlaut-editor's own.

   No `display`, deliberately. The module hides two of these three with the
   `hidden` property, and an author rule setting `display` here would beat the
   UA's `[hidden] { display: none }` and paint them anyway. */
.metacom-panel__note { margin: 0; font-size: 13px; line-height: 1.5; color: var(--text-dim); }

/* ------------------------------------------------------------ voice picker */

/* The list a voice is chosen from, from `@lautstark/stimmquelle/voice-picker`.
   The block: a field to narrow it with, the language pills, and the list.

   **This one is the opposite case to `.backup-panel` above, and it is worth
   saying which.** Nobody drew that class; three products drew every one of
   these. `.voices`, `.voice`, `.voice__name`, `.voice__facts` and
   `.voice__hint` were already the same five names in mitreden, vorlaut-editor
   and wochenwerk — the vocabulary had converged on its own and the *drawing*
   had not, which is §4.12's failure in its purest form. So most of what is
   below is an extraction and reads like one: where the three agreed, their
   agreed value stands here unchanged.

   Where they did not, the module's header carries the argument. Two are worth
   repeating here because they are decisions about appearance rather than about
   markup, and this is the file that owns those:

   - **No scroll box.** Two of the three put `max-height: min(46vh, 340px);
     overflow: auto` on `.voices`, on the sound argument that a personal key
     turns three voices into three hundred and pushes the panel holding that key
     off the sheet. vorlaut-editor had that too, measured what it did and took
     it out: a wheel gesture latches to the element under the pointer for its
     whole run, so a 340px list inside a 460px body meant the sheet did not
     scroll and the button below could not be reached. The sheet rule one
     screen up already says two nested scrolling areas is how a Save button is
     lost. One scroll, with the filters directly above it.
   - **Selection is `aria-checked`, never a class.** All three had already
     landed here, and it is the same rule `.menu button[aria-checked="true"]`
     keeps: what a screen reader is told and what an eye is shown cannot come
     apart if there is only one thing carrying both.

   The accent tint and no ring, for design.md §4.3's reason and wochenwerk's
   wording of it: a row that is the answer to a question is not a row that is
   being pointed at, and 2px of accent around it reads as a focus marker parked
   on the wrong row. */
.voice-picker { display: grid; gap: 10px; }
/* A wrapping <label>, so the caption above the field cannot come apart from it.
   Block, because a bare label is inline and its two children would not stack. */
.voice-picker__search { display: block; }
/* 4px rather than `.acts`' 8px: these are pills that narrow a list, sitting
   directly above it, not a row of actions. The module empties this rather than
   hiding it — a catalogue of one language has nothing to narrow by — and
   `:empty` is what keeps the grid gap from being spent on nothing. `[hidden]`
   would have needed a second rule here to beat this file's own `display`, which
   is the trap `.metacom-panel__note` records one block up. */
.voice-picker__filters { display: flex; flex-wrap: wrap; gap: 4px; }
.voice-picker__filters:empty { display: none; }

.voices { display: flex; flex-direction: column; gap: 2px; padding: 2px; }
/* Two buttons to a row: one to hear the voice, one to choose it. A button
   inside a button does not render, so the row is a wrapper — and it is drawn
   even where a product offers no preview, so that adding one later moves
   nothing. */
.voices__row { display: flex; align-items: center; gap: 8px; }
.voices__play {
  width: 40px; flex: none; justify-content: center; padding: 8px 0;
  font-variant-numeric: tabular-nums;
}
/* The filtered-empty line, and deliberately not `.empty`: that is the taught
   empty state above, a dashed box with 34px of padding, and its own comment
   says the two must never be confused. This is the shorter sentence, on the
   list's own left edge. */
.voices__none { margin: 0; padding: 6px 11px; font-size: 13px; color: var(--text-dim); }

.voice {
  flex: 1 1 auto; min-width: 0; display: block; text-align: left;
  font: inherit; cursor: pointer; padding: 8px 11px;
  border: 1px solid transparent; border-radius: var(--radius-item);
  background: none; color: var(--text);
}
.voice:hover { background: var(--surface-2); }
.voice[aria-checked="true"] { background: var(--accent-soft); }
.voice[aria-checked="true"] .voice__name { color: var(--accent-strong); font-weight: 650; }
.voice__name { font-size: 14.5px; }
/* Where it comes from, what it speaks, whose voice it is, and what it costs —
   the four facts a bare list of names withheld, on the line under the name.

   The size and the colour are on this class rather than on a `.small muted`
   pair in the markup, which is how one of the three reached the same effect and
   missed it by 1.5px and one token step. Neither of those two names is drawn in
   this file, so a module emitting them would be handing two products markup
   nothing there draws — §4.12's failure exactly. */
.voice__facts { display: block; font-size: 12px; color: var(--text-faint); }
/* A note the catalogue attaches to one voice, under the facts and deliberately
   in the same size and the same faint colour. It is a sentence rather than a
   fact, so it wraps — no accent, no border, no icon, because every one of those
   would say "this voice is broken" about a voice that is not. */
.voice__hint {
  display: block; margin-top: 3px; font-size: 12px; color: var(--text-faint);
  white-space: normal; text-wrap: pretty;
}

/* ------------------------------------------------------------------ footer */

/* The footer joined this file late, and on evidence: design.md's audit called
   the two footers "not comparable" — then mitreden's rewrite grew legal pages,
   needed one after all, and built it by copying bildhaft's values with a
   comment saying so. Centred, by decision. The shell is shared; the content is
   not — bildhaft's attribution is a licence obligation, and what a footer may
   claim is governed by "prose that denies must also disclose" (§4.3). */
.footer {
  text-align: center;
  padding: 14px 16px 20px;
  font-size: 11.5px; line-height: 1.5;
  color: var(--text-faint);
}
.footer a { color: var(--text-dim); }

/* A button that has to read as a link, because it opens a dialog rather than
   navigating — a footer should not look like it has two kinds of thing in it. */
.linklike {
  padding: 0; border: none; background: none;
  font: inherit; color: var(--text-dim);
  text-decoration: underline; cursor: pointer;
}
.linklike:hover { color: var(--text); }

/* -------------------------------------------------------- the Sammlung list

   The rows down the side: a name, how much is in it, and which are open. The
   behaviour is docs/lib/collections.js; this is what draws it.

   Called `.collections` rather than the `.list__item` two products had, and the
   gallery is why: gallery.css uses `.list` and `.item` for a demo of *sentence*
   rows, so a `.list` rule here would reach into a different component that
   happens also to be a list. A name generic enough to collide once will do it
   again. See the head of collections.js. */
.collections { display: flex; flex-direction: column; gap: 1px; }

.collections__item {
  display: flex; align-items: center; gap: 8px;
  width: 100%; text-align: left; padding: 8px 10px;
  border: 0; border-radius: var(--radius-sm); background: none;
  color: var(--text-dim); font: inherit; cursor: pointer;
}
.collections__item:hover { background: var(--surface-2); color: var(--text); }

/* The accent, not another grey. All three marked the open row this way -
   --accent-soft behind --accent-strong - and a row differing from its
   neighbours only by being a slightly lighter grey is one to look at twice. */
.collections__item--active {
  background: var(--accent-soft); color: var(--accent-strong); font-weight: 600;
}

/* A long name truncates rather than wrapping the row to two lines: the rows are
   read as a column of names, and an uneven column is harder to scan than a
   clipped one. The whole name is in the work head of whichever is open. */
.collections__name {
  flex: 1; min-width: 0; font-size: 14px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* No colour of its own, and no opacity either: it inherits the row, so the
   count follows the name into the accent when the row is the open one.

   The opacity half is a measured finding rather than a preference, carried here
   from bildhaft's sheet so that it is not re-discovered a fourth time: at .7
   over --accent-soft this fell to 2.91:1, under the 4.5 the tokens are checked
   against. A dimmed count is the obvious thing to reach for and it is the one
   thing that cannot be done here. Tabular figures so a column of them lines up. */
.collections__count {
  flex: none; font-size: 12px; font-variant-numeric: tabular-nums;
}

/* The two-line row: a name with a second line under it, where a product passes
   one. docs/mocks/vorlaut-sammlung-zeile.html is where this was argued out
   against five other answers, and it carries the measurements.

   Only .collections__text and .collections__sub came from that page. The mock
   also defines .collections__mark, .collections__grid and .collections__unit;
   those belong to candidates that lost and are deliberately not here. */
.collections__text { flex: 1; min-width: 0; }
.collections__text .collections__name { display: block; }
.collections__sub {
  display: block; font-size: 11.5px; line-height: 1.35; color: var(--text-faint);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-variant-numeric: tabular-nums;
}

/* And the count sits on the name's line rather than in the middle of the block.

   The rule above centres a row's contents, which is right while there is one
   line: the middle of the block is the line. With two, the number drifts into
   the gap and reads as belonging to the second line or to neither.

   `baseline` and not `flex-start`, because the name is 14px and the count 12px
   - aligning their boxes leaves the baselines about 3px apart, which is the
   part the eye reads. Keyed off :has() rather than a modifier class, so the
   second line and the alignment it forces stay one fact instead of two that
   have to agree. */
.collections__item:has(.collections__sub) { align-items: baseline; }

/* The open row tints what is inside it, and the second line has to come along:
   left at --text-faint it would be the one grey thing on an accented row.

   Not dimmed to sit under the name, and that is measured rather than felt. At
   11.5px this is text, so the bar is 4.5:1, and --accent-strong over
   --accent-soft at .8 gives 3.20 in the light scheme and 3.83 in the dark. At
   full strength: 4.52 and 5.20. The row is already the quiet end of the scale
   and a second dimming puts it through the floor. */
.collections__item--active .collections__sub { color: inherit; }

/* ------------------------------------------------------------------ motion */

/* 130ms ease for a colour or fill change. Nothing in this file changes size
   or position, so the 220ms tier does not appear; a product that adds motion
   of its own must not invent a third duration. */
.btn, .chip, .menu button, .linklike, .collections__item {
  transition: background .13s ease, color .13s ease;
}
.field { transition: background .13s ease, border-color .13s ease; }

@media (prefers-reduced-motion: reduce) {
  .btn, .chip, .menu button, .linklike, .field, .collections__item { transition: none; }
}
