/*
 * 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
 *   tile grid, 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).
 *
 * - 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; }

/* 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); }

/* ------------------------------------------------------------------- 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. */
.sheet {
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius); box-shadow: var(--shadow);
  padding: 22px; max-width: 600px;
  /* 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;
}
/*
 * 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;
}

/* ------------------------------------------------------------ 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); }

.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. Today that is
 * the backup folder in all three products; @lautstark/sicherung drives it.
 *
 * 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 @lautstark/sicherung's `Status['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
 * the 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*.
 */
.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); }

.standing[data-state="needs-permission"],
.standing[data-state="failed"] { color: var(--danger); }
.standing[data-state="needs-permission"] .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; }
}

/* ------------------------------------------------------------------ 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; }
}
