/* notification-center.css — the top-bar bell, its recent list, and the arrival popups.
 *
 * Shell-level, not Chaos: the bell and its popups span every app, so they cannot live in an app's
 * stylesheet. The Notifications PAGE is a Chaos surface and its styles stay in apps/chaos.css.
 *
 * These lived in core.css, which index.html does not link — it is loaded only by controls-lab. So
 * every rule here was inert: the badge fell back to inline text beside the bell, and the recent list
 * lost its `position: fixed` and landed in normal document flow at the end of <body>, which is what
 * put a scrollbar on the page instead of a panel under the bell.
 */
/* ── Notification Center: the top-bar bell, its list, and arrival popups ──────────────────────
   Shell-level rather than app-level: the bell lives in the top bar and the popups appear over
   whatever app is open, so neither can be scoped to one app's stylesheet. */

.nc-bell { position: relative; }

/* A count, drawn as a count: a filled circle over the bell rather than a number beside it.
   The ring in the button's own colour is what separates it from the glyph underneath — without it
   the badge and the bell read as one smudge at this size. */
.nc-bell-badge {
    position: absolute;
    top: -2px;
    right: -2px;
    min-width: 18px;
    height: 18px;
    padding: 0 4px;
    box-sizing: border-box;
    border-radius: 9px;
    border: 2px solid var(--bg-primary);
    background: var(--accent-magenta, var(--accent));
    color: #fff;
    font-size: 11px;
    font-weight: 700;
    line-height: 14px;
    text-align: center;
}

/* Deliberately the same surface as `.account-menu` in shell.css — same background, radius, shadow and
   padding. They sit inches apart in the same bar, and two popups from one header that do not match
   read as two different products. */
.nc-recent {
    position: fixed;
    top: var(--header-popup-top);
    z-index: 10002;
    min-width: 280px;
    max-width: 360px;
    padding: 6px;
    border: 1px solid var(--border);
    border-radius: 12px;
    background: var(--bg-surface);
    box-shadow: 0 8px 32px rgba(0, 0, 0, 0.45);
    overflow: hidden;
}

/* Matches `.account-name` in shell.css — 13px, 600, primary — because the two popups sit inches
   apart and a muted 11px label next to a 13px one reads as a lesser panel rather than a sibling.
   Centred: it names the panel rather than labelling a column, and the rows below it are the only
   things with a left edge worth aligning to. */
.nc-recent-heading {
    padding: 8px 12px;
    border-bottom: 1px solid var(--border);
    font-size: 13px;
    font-weight: 600;
    color: var(--text-primary);
    text-align: center;
}

.nc-recent-empty { padding: 14px 12px; font-size: 12px; color: var(--text-muted); }

/* The app mark is load-bearing: with no grouping, it is the only thing saying where a row came from. */
.nc-recent-kind {
    grid-area: kind;
    display: flex;
    align-items: center;
    color: var(--text-secondary);
}

/* Dimmer than the kind, because it answers a question the reader is not usually asking. */
.nc-recent-app {
    grid-area: app;
    display: flex;
    align-items: center;
    color: var(--text-muted);
}

/* App manifests draw at 22px for the rail. Overriding the attributes here rather than editing the
   markup keeps one copy of each app's icon. */
.nc-recent-app svg {
    width: 14px;
    height: 14px;
}

.nc-recent-row {
    display: grid;
    grid-template-areas: "kind sentence app" "kind title app";
    grid-template-columns: auto minmax(0, 1fr) auto;
    align-items: center;
    gap: 2px 8px;
    width: 100%;
    padding: 8px 12px;
    /* No rule between rows: hover is what separates them, and a line under every one turned five
       notifications into a table. */
    border: 0;
    background: transparent;
    color: var(--text-primary);
    font: inherit;
    text-align: left;
    cursor: pointer;
}


.nc-recent-row { border-radius: 6px; }

/* Unread: the app mark on the right turns blue. The page uses a bar and a tint; this surface uses
   the mark it already draws, in the SAME blue, because the two are different shapes of list — a bar
   reads as an edge marker on a full-width table and crowds a 12px gutter in a narrow popover.
   The shared token is what keeps them one signal: the shape differs, the colour cannot drift.

   Nothing is added to the row, so nothing moves when a row is read — only the mark's colour
   changes, and it is already there on every row.

   The mark now says two things at once: which app, and unread. That is affordable because it is
   the only coloured thing in the row, so a change reads as a state rather than as decoration — and
   because colour is not carrying it alone. The bold sentence below is the redundant cue, which is
   what a reader who cannot separate blue from grey is left with.

   No tint, deliberately. A tinted row plus a hover tint gave three background states to tell
   apart — read, unread, hovered — and the gap between the first two was smaller than the one hover
   made, so hovering a READ row looked more unread than an unread row did. Hover therefore owns the
   background outright and says one thing: where the cursor is. */
/* The bar lives in the popup's own 6px padding, NOT on the row's edge. `.nc-recent` insets every
   row by 6px, and that gutter was empty — so the bar goes where the page's border-left would read
   from, without the row having to give up any of its 12px of text padding.
   Hence `left: -6px`: the row is the positioning context, so -6px lands flush against the popup's
   inner edge. Full height rather than a fixed one, so it tracks a two-line row.
   A 2px radius because this bar floats inside a 12px-radius popover, where the page's hard corners
   would read as a rendering artefact rather than a marker. */
.nc-recent-row { position: relative; }
.nc-recent-unread::before {
    content: "";
    position: absolute;
    left: -6px;
    top: 0;
    bottom: 0;
    width: 3px;
    border-radius: 2px;
    background: var(--notif-unread-bar);
}
.nc-recent-unread .nc-recent-app { color: var(--notif-unread-bar); }
.nc-recent-unread .nc-recent-sentence { font-weight: 600; }

.nc-recent-row:hover { background: var(--bg-hover); }
.nc-recent-sentence { grid-area: sentence; font-size: 12px; }
.nc-recent-title { grid-area: title; font-size: 11px; color: var(--text-muted); }

/* The footer's two bulk actions, pushed to opposite edges: Mark all read leads because it is the
   gentler action and the one reached for repeatedly, Clear all sits at the far end so neither is hit
   by aiming at the other. The rule that used to live on the button — a full-width centred link with a
   top border — moves here, because the border belongs to the footer rather than to either button. */
.nc-recent-actions {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 8px;
    border-top: 1px solid var(--border);
}

.nc-recent-all {
    padding: 8px 12px;
    border: 0;
    background: transparent;
    color: var(--accent);
    font: inherit;
    font-size: 11px;
    text-align: center;
    cursor: pointer;
}

.nc-recent-all:hover:not(:disabled) { background: var(--bg-hover); }

/* Off reads as off — the same half-opacity, not-allowed pattern the app page's bulk actions use,
   and for the same reason: an accent-coloured control under a pointer cursor that refuses to act is
   worse than one that is plainly unavailable. The hover is gated too, or the row still lights up
   under the cursor while disabled and undoes the whole signal. */
.nc-recent-all:disabled {
    opacity: 0.5;
    cursor: not-allowed;
}

/* The popup stack sits in the top-right corner, over everything, and does not take pointer events
   except on the popups themselves — an empty corner must stay clickable. */
.nc-popup-stack {
    position: fixed;
    top: 56px;
    right: 16px;
    z-index: 10003;
    display: flex;
    flex-direction: column;
    gap: 8px;
    pointer-events: none;
}

.nc-popup {
    position: relative;
    pointer-events: auto;
    display: flex;
    align-items: flex-start;
    gap: 8px;
    width: 300px;
    padding: 10px 12px;
    border: 1px solid var(--border);
    border-left: 3px solid var(--accent);
    border-radius: 8px;
    background: var(--bg-panel);
    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.28);
    cursor: pointer;
    animation: nc-popup-in 140ms ease-out;
}

@keyframes nc-popup-in {
    from { opacity: 0; transform: translateX(12px); }
    to   { opacity: 1; transform: translateX(0); }
}

.nc-popup:hover { background: var(--bg-hover); }
.nc-popup-body { flex: 1; min-width: 0; }
.nc-popup-sentence { font-size: 12px; color: var(--text-primary); }
.nc-popup-title {
    margin-top: 2px;
    font-size: 11px;
    color: var(--text-muted);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.nc-popup-close {
    flex-shrink: 0;
    border: 0;
    background: transparent;
    color: var(--text-muted);
    font-size: 15px;
    line-height: 1;
    cursor: pointer;
}

.nc-popup-close:hover { color: var(--text-primary); }

/* ══════════════════════════════════════════════════
   The Notification Center page (AB#13311)
   ══════════════════════════════════════════════════

   Moved out of apps/chaos.css, where these sat while the page was Chaos's. The page is a shared
   component now — Chaos and Chat both mount it — and an app stylesheet loads only with its own
   app, so a page styled from chaos.css renders unstyled everywhere else.

   Here rather than in a third file because this IS the notification-center control's stylesheet:
   it already carries the bell and its popups, and index.html already links it for every app.

   Chaos's per-trigger settings dialog stayed behind in apps/chaos.css, with the dialog chrome it
   depends on — the same split the JS made when the component moved (arc D-3). */
/* The page wears the house content-page look (AB#13390), so Notifications sits beside Prompts,
   Files, Scheduled and Assistants rather than looking like a panel that wandered in. The values
   below are COPIED from controls/content-page.css and content-page-tabs.css rather than shared with
   them: this file may not restyle `.lp-content-*`, because those classes are the other pages and a
   change here must not reach them. The cost is two places to edit if the house style moves; the
   alternative was giving the notification surfaces a stake in every content page.

   Scoped to the page classes only. `.nc-recent*` (the bell popover), `.nc-popup*` (the toasts) and
   `.nc-settings*` (the trigger dialog) share this file and NOT these rules, so none of them move. */
.nc-panel {
    display: flex;
    flex-direction: column;
    height: 100%;
    /* --bg-primary, as every content page — not the --bg-panel a docked panel would use. */
    background: var(--bg-primary);
    /* The page's own margin. Wider than content-page's 32px rather than narrower: a notification row
       is one line of text across a full-width pane, and without a real gutter the sentence and its
       timestamp end up at opposite edges of the screen with a stretch of nothing between them. The
       rows have their own truncation, so the width they lose here costs them nothing. */
    padding: 24px 100px 0;
    box-sizing: border-box;
}

/* The title row: the page's name, and the things that act on the whole page. Both were absent —
   the name because a title was once cut for vertical space, the actions because they lived at the
   end of the tab strip. Restored and gathered here so the shape matches the sibling pages, where
   the name sits left and the actions sit right on one line above the tabs (AB#13390). */
.nc-head-title-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    margin-bottom: 16px;
}

.nc-title {
    margin: 0;
    font-size: 24px;
    font-weight: 700;
    color: var(--text-primary);
    line-height: 1.2;
    letter-spacing: -0.01em;
}

/* The bulk actions, now in the title row rather than the tab strip. `margin-left: auto` is gone
   with the strip that needed it — the title row spaces them itself. */
.nc-actions {
    display: flex;
    align-items: center;
    gap: 8px;
}

/* Buttons, not accent-coloured text — and the SOLID variant, which is what the sibling pages
   actually put in this position ("+ New prompt" on Prompts). The bordered variant was the first cut
   here, on the reasoning that three solid buttons would each claim to be the primary one; on screen
   beside Prompts it simply read as a different page, which is the whole thing this is fixing.

   It inverts with the theme — near-white on dark, near-black on light — because it is painted in
   --text-primary on --bg-panel rather than in a fixed colour. */
.nc-mark-all {
    padding: 8px 16px;
    border-radius: var(--radius, 8px);
    border: 1px solid var(--text-primary);
    background: var(--text-primary);
    color: var(--bg-panel);
    font-size: 13px;
    font-weight: 600;
    line-height: 1;
    font-family: inherit;
    cursor: pointer;
    transition: filter 120ms ease, opacity 120ms ease;
}

/* Hover belongs to controls that would do something. This applied unconditionally, so an off button
   still reacted under the pointer. Now a button, it lifts the way the sibling pages' buttons do
   rather than underlining the way a link does. */
.nc-mark-all:hover:not(:disabled) {
    filter: brightness(1.12);
}

.nc-mark-all:active:not(:disabled) { filter: brightness(0.95); }

/* Off reads as off. There was no disabled rule at all, so Mark all as read and Clear all kept the
   accent colour and the pointer cursor while refusing to do anything — a control that looks live and
   is not, which is worse than one that is simply absent.

   The house pattern, the same as every `.lani-btn-*` and `.btn-*`: half opacity, not-allowed cursor.

   Left as a <button> rather than reshaped into a styled <span>. A browser owns a native checkbox's
   appearance when it is disabled, which is why the settings toggles beside this needed a span — but a
   disabled button is fully styleable, so there is nothing to buy by giving up the semantics, the
   keyboard behaviour and the implicit aria-disabled that come free with the element. */
.nc-mark-all:disabled {
    opacity: 0.5;
    cursor: not-allowed;
}


/* Tabs — the same strip as the view tabs above the board (`.chaos-view-tab`), because they are the
   same control in the same app and there is no reason for a reader to learn two of them.

   Sized to their labels rather than stretched to thirds. Three tabs divided across the panel put
   "Other" in the middle of nowhere and made the strip read as a segmented control — something whose
   options are alternatives to each other — when it is a set of places to look. Content width also
   survives a fourth section, or a translation, without re-dividing the width. */
/* The same 48px as `.chaos-sidebar-header` beside it, stated rather than derived. Padding above the
   tabs was the first attempt and it landed a few pixels short, because a tab is as tall as its own
   metrics: the font, whether an unread badge sits in the line box. Those change; the row it has to
   line up with does not. So the height lives here, and the tabs stretch into it.

   `align-items` stays at its `stretch` default deliberately. A centred tab would float its active
   underline in the middle of the strip; a stretched one puts its bottom border on the border of the
   strip itself, which is what `margin-bottom: -1px` on the tab is for. */
/* Spaced rather than boxed, matching the sibling pages: the tabs are separated by a gap and the
   active one is named by its underline, so the strip needs no height of its own (AB#13390). The
   fixed 48px went with the actions that used to share this row. */
.nc-tabs {
    display: flex;
    align-items: stretch;
    gap: 24px;
    border-bottom: 1px solid var(--border);
    flex-shrink: 0;
    margin-bottom: 16px;
}

.nc-tab {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    padding: 6px 2px 12px;
    background: none;
    border: none;
    border-bottom: 2px solid transparent;
    color: var(--text-secondary);
    font-size: 14px;
    font-weight: 500;
    font-family: inherit;
    line-height: 1;
    cursor: pointer;
    transition: color 120ms ease, border-color 120ms ease;

    /* The underline sits ON the strip's divider rather than under it, so the active tab breaks the
       line instead of drawing a second one below it. */
    margin-bottom: -1px;
}

.nc-tab:hover {
    color: var(--text-primary);
}

/* Neutral, not accent. The sibling pages mark the open tab with a full-strength label, a heavier
   weight and an underline in the same ink — the accent underline this used to draw was the one
   thing that kept Notifications looking like a different kind of surface (AB#13390). */
.nc-tab.active {
    color: var(--text-primary);
    border-bottom-color: var(--text-primary);
    font-weight: 600;
}

.nc-tab:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
    border-radius: 4px;
}

/* List */
/* The list, with the same thin scrollbar every other scrolling list in this app uses
   (`.chaos-comments-list`, `.chaos-detail-content`, and the settings dialog beside it). This one was
   left on the browser default, so the widest vertical line on a page of one-line rows was the bar
   beside them. */
/* The head stays put and only the list scrolls, as on the sibling pages — the bottom padding lives
   on the scroll area rather than the page so the last row can reach the edge instead of stopping
   above a dead gap (AB#13390). */
.nc-head { flex-shrink: 0; }

.nc-list {
    flex: 1;
    min-height: 0;
    overflow-y: auto;
    padding-bottom: 24px;
    scrollbar-width: thin;
    scrollbar-color: var(--border) transparent;
}

.nc-list::-webkit-scrollbar { width: 4px; }
.nc-list::-webkit-scrollbar-track { background: transparent; }
.nc-list::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
.nc-list::-webkit-scrollbar-thumb:hover { background: var(--text-muted); }

/* The per-section unread count. A badge rather than a dim parenthetical: the number is the reason a
   reader looks at the tab strip at all, and at 10px and 60% opacity it read as decoration.

   Raised off the baseline rather than sitting on it, which is what makes it read as a badge against
   the tab's label instead of as a second word. */
/* The sibling pages' tab badge (AB#13390): a neutral pill sitting ON the baseline beside the label,
   not a magenta superscript above it. The accent-coloured dot read as an alert — which is what it
   was for, and what a count on Primary does not need to be: the tab is already the thing the reader
   came to, and the number is there to say whether scrolling further is worth it. The gap comes from
   `.nc-tab`'s own `gap`, so the badge no longer carries a margin of its own.

   `tabular-nums` so the pill does not change width between 8 and 11. */
.nc-tab-count {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 20px;
    height: 20px;
    padding: 0 6px;
    border-radius: 999px;
    background: var(--bg-hover);
    color: var(--text-secondary);
    font-size: 11px;
    font-weight: 600;
    font-variant-numeric: tabular-nums;
    line-height: 1;
}

/* On the open tab the badge picks up the same ink as the label, at a tint rather than a fill. */
.nc-tab.active .nc-tab-count {
    background: color-mix(in srgb, var(--text-primary) 12%, transparent);
    color: var(--text-primary);
}

/* `hidden` has to be spelled out, because the UA sheet's `[hidden] { display: none }` and the class
   above have equal specificity — so the later author rule wins and a hidden badge stays visible. That
   is why Cleared was showing a zero: the element was correctly hidden and the display rule outvoted
   it. Ordering the declaration here keeps the JS honest, which sets `hidden` and expects it to mean
   something. */
.nc-tab-count[hidden] {
    display: none;
}

/* One line per notification: kind icon, the task, who did what, when, and the two row actions.

   Every fixed column is an explicit width, and that is what makes the list line up. Each row is its
   OWN grid — `display: grid` sits on the row, not on the list — so `auto` and `fr` are resolved per
   row rather than across them. With `auto` on the timestamp, a row reading "just now" gave its fr
   columns less room than one reading "2h ago", and every column left of the time shifted with it, so
   the avatars and sentences stopped forming a line down the list.

   The timestamp is therefore pinned, and pinned for the widest thing it can hold rather than the
   widest thing usually seen. `relativeTime` counts in minutes, hours and days for the first week and
   then hands over to a full short date — "Aug 24, 2026", or a CJK form whose glyphs are wider still.
   116px carries that with room; the relative forms never approach it. Each action button is its own
   24px. Only the two text columns flex, and they flex by the same amount on every row because
   everything around them is fixed.

   The width went 96px → 116px with the row's type (AB#13390): the old figure was measured for an
   11px timestamp, and at 13px the full-date form would have been ellipsised by the very rule below
   that keeps a long value from widening the row — a date silently reading "Aug 24, 20…". */
.nc-item {
    display: grid;
    grid-template-columns: 28px minmax(0, 1fr) minmax(0, 1.4fr) 116px 24px 24px;
    align-items: center;
    gap: 16px;
    /* The sibling pages' row rhythm: 12px, and no side padding of its own now that the page carries
       a margin (AB#13390). */
    padding: 12px 0;
    border-bottom: 1px solid var(--border);
    /* The unread stripe, drawn transparent until a row is unread. Reserved on every row rather than
       added to the unread ones: a border is part of the box, so adding 3px to some rows and not
       others pushed their whole contents 3px right and no amount of fixing the COLUMNS could line
       them up — the rows themselves disagreed about where they started.

       It keeps a padding of its own so the stripe does not sit flush against the text. */
    border-left: 3px solid transparent;
    padding-left: 9px;
    cursor: pointer;
    transition: background 120ms ease;
}

.nc-item:hover {
    background: var(--bg-hover);
}















/* The filter row. An active filter has to be obvious at a glance, so the bar itself carries a mark
   as well as the chip — a single lit chip in a row of three is easy to miss on a long list. */
/* The filter row, as the sibling pages' pill toolbar: no rule under it and no band around it — the
   lit pill is the mark, the same way it is on Files and Scheduled (AB#13390). The bar-level tint and
   accent edge that used to shout "a filter is on" went with the accent; a solid pill in a row of
   outlined ones carries that on its own. */
.nc-filter {
    display: flex;
    align-items: center;
    gap: 8px;
    flex-wrap: wrap;
    margin-bottom: 16px;
}

.nc-filter-chip {
    padding: 7px 16px;
    border: 1px solid var(--border);
    border-radius: 999px;
    background: var(--bg-panel);
    color: var(--text-secondary);
    font-family: inherit;
    font-size: 13px;
    font-weight: 500;
    line-height: 1;
    cursor: pointer;
    transition: background 120ms ease, border-color 120ms ease, color 120ms ease;
}

.nc-filter-chip:hover {
    border-color: var(--text-muted);
    color: var(--text-primary);
}

.nc-filter-on,
.nc-filter-on:hover {
    background: var(--text-primary);
    border-color: var(--text-primary);
    color: var(--bg-panel);
}

.nc-filter-clear {
    display: flex;
    align-items: center;
    margin-left: auto;
    padding: 3px;
    border: 0;
    border-radius: 4px;
    background: transparent;
    color: var(--text-muted);
    cursor: pointer;
}

.nc-filter-clear:hover { background: var(--bg-surface); color: var(--text-primary); }

/* A month heading. Sticky so the group a row belongs to stays legible while scrolling a long list —
   the heading is the only thing saying which month the rows under it happened in. */
/* The month divider, as the sibling pages' column header (AB#13390): uppercase, tracked out, and
   carried by its rule alone rather than by a filled band. The band was invisible while the page was
   --bg-panel and became a grey stripe the moment the page moved to --bg-primary — the background is
   not decoration though, it is what a STICKY header needs so rows do not scroll through the text, so
   it follows the page's colour instead of being dropped.

   Left-padded to 12px, which is where the rows' own text starts (3px unread stripe + 9px), so the
   label lines up with the column it heads rather than with the stripe. */
.nc-month {
    position: sticky;
    top: 0;
    z-index: 1;
    padding: 12px 0 12px 12px;
    background: var(--bg-primary);
    border-bottom: 1px solid var(--border);
    font-size: 11px;
    font-weight: 600;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--text-muted);
}

/* Load more sits at the foot of the list, full width, and is absent rather than disabled when there
   is nothing older — see the panel for why. */
.nc-load-more {
    width: 100%;
    padding: 10px 16px;
    border: 0;
    border-top: 1px solid var(--border);
    background: transparent;
    color: var(--accent);
    font: inherit;
    font-size: 12px;
    cursor: pointer;
}

.nc-load-more:hover { background: var(--bg-hover); }
.nc-load-more:disabled { color: var(--text-muted); cursor: default; }

/* The per-row actions, always drawn.

   They were revealed on hover, on the reasoning that one on every row is visual noise. That reads
   as noise only if the controls are incidental, and they are not: clearing is what section 11 calls
   the core interaction of the feature, and read state is the other thing a reader came to change.
   A control you have to hover to discover is a control most people never find — and on a touch
   screen there is no hover at all, so it was unreachable rather than merely hidden. */
.nc-item-clear {
    flex-shrink: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 24px;
    height: 24px;
    padding: 0;
    border: 0;
    border-radius: 4px;
    background: transparent;
    color: var(--text-muted);
    cursor: pointer;
    transition: background 0.1s, color 0.1s;
}

/* Hovering the row still lifts them, so they read as belonging to the row under the cursor rather
   than competing with the text on every other one. */
.nc-item:hover .nc-item-clear {
    color: var(--text-secondary);
}

.nc-item-clear:hover {
    background: var(--bg-surface);
    color: var(--text-primary);
}

.nc-item-clear:disabled {
    cursor: default;
    opacity: 0.4;
}

/* Shared with the bell popup's own unread row — one definition in shell.css, so the two
   surfaces cannot drift into showing the same state two different ways. */
.nc-unread {
    background: var(--notif-unread-bg);
    border-left-color: var(--notif-unread-bar);
}
.nc-unread .nc-item-name {
    font-weight: 600;
}

.nc-item-icon {
    flex-shrink: 0;
    width: 28px;
    height: 28px;
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--bg-surface);
    border-radius: 50%;
    color: var(--text-secondary);
}

/* Both text columns truncate rather than wrap. A notification that wraps makes its row taller than
   its neighbours, and a list of different-height rows cannot be scanned. */
/* One size for the whole row — 13px, as the sibling pages' grid (AB#13390). The three columns used
   to step 13 / 12 / 11 down the row, which made a single line of text read as three ranks of
   importance; the grid says the rank in COLOUR instead and keeps one size, so the eye crosses a row
   without re-focusing.

   No weight is added with it. The grid's name cell is 600, and copying that here would have every
   row shouting — the weight on this list means UNREAD, and it has to stay the only thing that does
   (see `.nc-unread .nc-item-name` above). */
.nc-item-name {
    font-size: 13px;
    color: var(--text-primary);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.nc-item-action {
    display: flex;
    align-items: center;
    gap: 6px;
    min-width: 0;
    font-size: 13px;
    color: var(--text-secondary);
}

.nc-item-action .ch-avatar { flex-shrink: 0; }

.nc-item-sentence {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.nc-item-time {
    font-size: 13px;
    color: var(--text-muted);
    white-space: nowrap;
    text-align: right;
    /* Its column is fixed, so an unexpectedly long value is clipped rather than widening the row and
       putting every column back out of line. */
    overflow: hidden;
    text-overflow: ellipsis;
}

/* ══════════════════════════════════════════════════
   The notification settings screen (AB#13312)
   ══════════════════════════════════════════════════

   Moved out of apps/chaos.css, where these sat while the settings screen was Chaos's. It is a
   shared component now — Chaos and Chat both open it — and an app stylesheet loads only with its
   own app, so Chat's dialog would have rendered unstyled.

   The AB#13311 note left here said these stayed behind because they style `.kanban-dialog` chrome
   "that only this app has". That was wrong: the chrome lives in controls/dialog.css, which
   index.html links for every app. What is Chaos-specific is nothing — the two
   `.nc-settings-overlay .kanban-dialog` rules below are OVERRIDES of that shared chrome (a wider
   card so two columns of trigger names fit, and a line under the title), and they belong wherever
   the screen they shape belongs. */
/* Notification settings. A locked row reads as on and says why, rather than looking broken. */
/* The scrolling list, bled to the card edge. `.kanban-dialog` pads 24px on each side, which the
   scrollbar sat inside — a thin bar floating in a gutter, reading as though the list continued
   behind it. The negative margin cancels that padding for this element only, so the title above
   keeps its inset, and the rows take the padding back so their text still stops short of the bar.

   Thin, in the same terms as every other scrolling list in this app (`.chaos-comments-list`,
   `.chaos-detail-content`): a settings list is not a place the scrollbar should be the widest
   vertical line on screen. */
/* `.kanban-dialog` caps itself at 500px, which two columns of full titles do not fit: a half column
   would be about 185px, and "Checklist item assignee changed" beside a 36px switch wraps to two
   lines and leaves the rows ragged. Raised for this dialog alone, not for the shared card. */
.nc-settings-overlay .kanban-dialog {
    max-width: 720px;
}
/* A line under the title, edge to edge. The card pads 24px, so the header is inset from both sides
   and a border on it would have stopped short at each end and read as a divider between two things
   in the body rather than the end of the header. Cancelling the padding with margins and taking it
   back as padding puts the line on the card edge while the title stays where it was.

   Scoped to this overlay. Every dialog in the app shares `.kanban-dialog-header`, and most of them
   are short enough that a rule under the title would be a line for its own sake. This one has a body
   that scrolls, so the header has something to be separated from. */
.nc-settings-overlay .kanban-dialog-header {
    margin: 0 -24px;
    padding: 0 24px 14px;
    border-bottom: 1px solid var(--border);
}
/* The list itself carries no size and no scroll (AB#13469).
   Everything that used to be here — a 60vh cap, `overflow-y: auto`, a 640px floor and a negative
   right margin — is the shape of a DIALOG CARD, and it sat on the component's own class because the
   dialog was the only thing that mounted it. Settings → Notifications mounts the same control in a
   page, where those rules produced a second scrollbar inside a pane that already scrolls: the reader
   got a short scrolling box in the middle of a long still one, and the page's own bar did nothing.
   Below, under `.nc-settings-overlay`, is the dialog's half. */
.nc-settings {
    scrollbar-width: thin;
    scrollbar-color: var(--border) transparent;
}

/* The dialog's half: a card cannot grow with its content, so in there the list is capped and scrolls
   itself. Scoped to the overlay the three openers pass, so a page mount is untouched. */
.nc-settings-overlay .nc-settings {
    max-height: 60vh;
    overflow-y: auto;
    min-width: 640px;
    margin-right: -24px;

    /* The negative margin above takes this element out to the card edge so the scrollbar sits on it.
       This padding is what then keeps the content off the bar — and it belongs here, on the one
       element that knows where the bar is, rather than on the rows and the headings separately.

       That is what went wrong the first time: the clearance was a right padding on each row and each
       heading, while the left inset came from the dialog. So the two sides were measured from
       different places and could not agree — rows ended up 42px from the left and 24px from the
       right, headings 28 and 24. With both sides derived from this box, everything below lines up
       with the title at 24px on both edges. */
    padding-right: 24px;
}
.nc-settings::-webkit-scrollbar { width: 4px; }
.nc-settings::-webkit-scrollbar-track { background: transparent; }
.nc-settings::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
.nc-settings::-webkit-scrollbar-thumb:hover { background: var(--text-muted); }
.nc-settings-group { border-bottom: 1px solid var(--border); }
.nc-settings-group:last-child { border-bottom: 0; }
/* Title and count on one row. Stacked, the count read as a second heading under the first; beside it
   the count annotates the title, which is what it is. Immediately after rather than pushed to the far
   edge — at the edge it would read as a control. */
.nc-settings-group summary {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 10px 0;
    cursor: pointer;
}
.nc-settings-group-title { font-size: 13px; font-weight: 650; color: var(--text-primary); }
.nc-settings-summary {
    font-size: 11px;
    color: var(--text-muted);
    line-height: 1.4;
}
/* Two columns of equal width, identically on every group, so a column edge is a straight line down
   the whole list. A group with one row leaves the second cell empty, which is correct — a single row
   stretched across both would break that line.

   The gap between them is a column gap, not padding on the cells: padding would also have inset the
   outer edges, and those are set once on the scrolling container so both sides agree. */
.nc-settings-rows {
    display: grid;
    grid-template-columns: 1fr 1fr;
    column-gap: 16px;
}
.nc-settings-row {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 6px 0;
    font-size: 12px;
    color: var(--text-secondary);
    min-width: 0;
}
/* No hover. It was inherited from a list of things you pick from, and this is a list of things you
   set: nothing here is selected, so lighting a row on the way past the one below it says something
   is about to happen that is not. The switch has its own affordance. */

/* The switch and its label take the row; the lock sits at the end, so the locks line up with each
   other rather than with the ends of four different labels. */
.nc-settings-toggle {
    flex: 1;
    min-width: 0;
}
/* ── The switch, one size down, HERE only ──────────────────────
   30×16 with a 12px knob, against the shared control's 36×20 with a 16px.

   Sized against the LABEL rather than in the abstract, which is why it is scoped rather than
   changed at the source: this list runs its rows at 12px (see the rule below), so the shared
   switch stood 1.67× the text beside it and read as the subject of the row rather than the control
   for it. Every other caller of `toggle()` — the wizard's Yes/No steps, Chaos's sub-task toolbar
   and its Duplicate dialog — labels at the control's own 13px and is proportioned for it, so
   shrinking `controls/toggle.css` would have re-sized three surfaces to fix one.

   The three numbers are not independent. The knob keeps a 2px inset on all four sides, so the
   checked travel is width − knob − 4: 30 − 12 − 4 = 14. Change one and the other two follow, or
   the knob stops sitting centred at one end.

   Scoped on `.nc-settings-toggle` — the class `notificationSettingsList` passes to `toggle()` —
   for the same reason the label override below uses it: it names this list's switches exactly,
   without reaching for a descendant selector that a future surface inside `.nc-settings` would
   inherit by accident. */
.nc-settings-toggle {
    gap: 6px;
}

.nc-settings-toggle .ch-toggle-slider {
    width: 30px;
    height: 16px;
    border-radius: 8px;
}

.nc-settings-toggle .ch-toggle-slider::after {
    width: 12px;
    height: 12px;
}

.nc-settings-toggle .ch-toggle-input:checked + .ch-toggle-slider::after {
    transform: translateX(14px);
}

/* The control labels at 13px on `--text-primary`; this list runs at 12px on `--text-secondary`, and
   a settings row is not the place for the label to outweigh the group heading above it. */
.nc-settings-toggle .ch-toggle-label {
    font-size: 12px;
    color: var(--text-secondary);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
/* A required row reads exactly like an optional one that is switched on, because that is the state
   it is reporting: you are getting this. It used to dim its label and grey its box, which said
   "unavailable" — and a reader scanning the two always-on groups first met four rows that looked
   disabled before they met one that looked on.

   Nothing here needs to force that any more. The switch is a styled span keyed off `:checked`, so a
   disabled one is drawn identically to an enabled one — which is why the earlier attempt at this,
   `opacity: 1` on a native disabled checkbox, could not have worked: the browser greys those itself
   and `accent-color` does not apply to them at all. */
.nc-settings-required { cursor: default; }
/* A glyph, not a badge. This was a pill with a border, a background, and the word "Required" beside
   the lock — three ways of saying one thing, louder than the setting it annotated. */
.nc-settings-required-mark {
    display: inline-flex;
    align-items: center;
    color: var(--text-muted);
}

/* ── The run-duration threshold (AB#13371) ──────────────────────────────
   Above the groups, separated by the same rule that divides them, so it reads as a setting of the
   whole screen rather than as a member of the first group. */
.nc-settings-threshold {
    border-bottom: 1px solid var(--border);
    padding: 12px 0 14px;
}
/* Label, field and unit on one line: the sentence reads "runs longer than [ 60 ] seconds", which is
   what the control means — a stacked field would leave the unit orphaned under the number. */
.nc-settings-threshold-label {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 8px;
    font-size: 13px;
    color: var(--text-primary);
}
.nc-settings-threshold-input {
    width: 84px;
    padding: 5px 8px;
    font: inherit;
    font-size: 13px;
    color: var(--text-primary);
    background: var(--bg-input, var(--bg-surface));
    border: 1px solid var(--border);
    border-radius: 6px;
}
.nc-settings-threshold-input:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 1px;
}
.nc-settings-threshold-input:disabled { opacity: 0.6; }
.nc-settings-threshold-unit { font-size: 13px; color: var(--text-muted); }
.nc-settings-threshold-hint {
    margin: 6px 0 0;
    font-size: 12px;
    line-height: 1.45;
    color: var(--text-muted);
}
