:root {
  color-scheme: light dark;
  --bg: #ffffff;
  --fg: #1a1a1a;
  --muted: #666666;
  --border: #dddddd;
  --accent: #2a6df4;
  /* Pale in light mode; a bright pale-yellow/pale-green would glare in dark
     mode, so the dark values below are muted/desaturated versions of the
     same hue, not a lightened --bg. */
  --state-new-bg: #fdf6d0;
  --state-read-bg: #e2f3e0;
  /* Stats charts (m10): a single bar color, tuned per theme rather than
     inverted — see ARCHITECTURE.md for why. */
  --stat-bar: #2a6df4;
  --stat-hit-hover: rgba(42, 109, 244, 0.12);
  /* Marks (m11): favorite/work active button — a third hue distinct from
     the yellow/green read-state colors and from --accent, so a marked
     button doesn't get mistaken for "Прочтено" or a primary action. */
  --mark-active-bg: #e7e0fb;
  --mark-active-fg: #4b3fa0;
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #14161a;
    --fg: #eaeaea;
    --muted: #9a9a9a;
    --border: #33363c;
    --state-new-bg: #3a331c;
    --state-read-bg: #1f3322;
    /* Lighter/desaturated step of the same hue, chosen for contrast on the
       dark surface — not the light --stat-bar value run through an
       inversion, which would come out acid-bright. */
    --stat-bar: #3987e5;
    --stat-hit-hover: rgba(57, 135, 229, 0.18);
    /* Independently tuned, not the light value inverted — an inverted
       #e7e0fb/#4b3fa0 pair comes out acid-yellow, the same failure mode
       documented for --stat-bar above. */
    --mark-active-bg: #332b5c;
    --mark-active-fg: #c3b5f7;
  }
}

/* Manual toggle (theme.js) always wins over the OS preference above —
   an attribute selector on :root is more specific than a bare :root
   inside @media, regardless of source order. */
:root[data-theme="light"] {
  --bg: #ffffff;
  --fg: #1a1a1a;
  --muted: #666666;
  --border: #dddddd;
  --state-new-bg: #fdf6d0;
  --state-read-bg: #e2f3e0;
  --stat-bar: #2a6df4;
  --stat-hit-hover: rgba(42, 109, 244, 0.12);
  --mark-active-bg: #e7e0fb;
  --mark-active-fg: #4b3fa0;
}

:root[data-theme="dark"] {
  --bg: #14161a;
  --fg: #eaeaea;
  --muted: #9a9a9a;
  --border: #33363c;
  --state-new-bg: #3a331c;
  --state-read-bg: #1f3322;
  --stat-bar: #3987e5;
  --stat-hit-hover: rgba(57, 135, 229, 0.18);
  --mark-active-bg: #332b5c;
  --mark-active-fg: #c3b5f7;
}

* { box-sizing: border-box; }

html {
  /* Only html, not body: setting overflow-x on body too would force its
     computed overflow-y to "auto" (per the CSS overflow spec, an axis set
     to non-visible forces the other to auto), turning body into its own
     scroll container instead of the page using the normal viewport
     scroller — which then breaks `position: sticky` for direct children
     of body (e.g. the offline banner), since sticky resolves against the
     nearest actual scroll container. */
  max-width: 100%;
  overflow-x: hidden;
  /* The service worker's offline fallback injects a banner at the very top
     of the cached page before the browser parses anything. Without this,
     Chrome's scroll anchoring "helpfully" scrolls the page down by exactly
     the banner's height to keep whatever was previously at the top (e.g.
     the topbar) visually anchored in place — which hides the banner it was
     just told to show. */
  overflow-anchor: none;
}

.theme-toggle {
  position: fixed;
  right: 1rem;
  bottom: 1rem;
  width: 2.75rem;
  height: 2.75rem;
  border-radius: 50%;
  border: 1px solid var(--border);
  background: var(--bg);
  color: var(--fg);
  font-size: 1.2rem;
  line-height: 1;
  cursor: pointer;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
  z-index: 10;
}

body {
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  background: var(--bg);
  color: var(--fg);
}

.login-page {
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1rem;
}

.login-form {
  width: 100%;
  max-width: 320px;
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
}

.login-form label {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  font-size: 0.9rem;
  color: var(--muted);
}

.login-form input {
  font-size: 1rem;
  padding: 0.6rem;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--bg);
  color: var(--fg);
}

.login-form button {
  font-size: 1rem;
  padding: 0.7rem;
  border: none;
  border-radius: 6px;
  background: var(--accent);
  color: #fff;
  cursor: pointer;
}

.error {
  color: #c0392b;
}

.notice {
  color: var(--accent);
}

/* --- reader shell --- */

a { color: inherit; text-decoration: none; }

.topbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  padding: 0.75rem 1rem;
  border-bottom: 1px solid var(--border);
}

.topbar h1 { font-size: 1.1rem; margin: 0; }

.topbar-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: flex-end;
  gap: 0.5rem 1rem;
}

.link-button {
  background: none;
  border: none;
  color: var(--muted);
  font-size: 0.9rem;
  cursor: pointer;
  padding: 0;
}

/* Collection-picker floating bar (m8, task Фаза 4): sticky right under the
   topbar so it stays visible while scrolling past tiles further down the
   page. Hidden by default (collections.js toggles the `hidden` attribute);
   [hidden] is redundant with the UA default but kept explicit since a
   sticky element with display other than none would otherwise still
   occupy a stacking position. */
.collection-bar {
  position: sticky;
  top: 0;
  z-index: 4;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
  padding: 0.6rem 1rem;
  background: var(--bg);
  border-bottom: 1px solid var(--border);
}

.collection-bar[hidden] {
  display: none;
}

.collection-bar input[type="text"] {
  flex: 1 1 12rem;
  min-width: 0;
  font-size: 0.9rem;
  padding: 0.5rem;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--bg);
  color: var(--fg);
}

/* Locked while an existing collection is picked in #collection-target
   (m9 Фаза 2) — a muted fill makes it visually obvious the name isn't
   editable right now, not just structurally read-only. */
.collection-bar input[type="text"]:read-only {
  background: var(--border);
  color: var(--muted);
}

#collection-target {
  flex: 1 1 10rem;
  min-width: 0;
}

.collection-bar select {
  font-size: 0.9rem;
  padding: 0.5rem;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--bg);
  color: var(--fg);
}

.collection-count {
  font-size: 0.85rem;
  color: var(--muted);
  white-space: nowrap;
}

.collection-bar-close {
  background: none;
  border: none;
  font-size: 1.3rem;
  line-height: 1;
  color: var(--muted);
  cursor: pointer;
  padding: 0 0.25rem;
}

.collection-bar-status {
  flex: 1 1 100%;
  font-size: 0.82rem;
  color: var(--muted);
  overflow-wrap: anywhere;
}

.collection-bar-status.is-warning {
  color: #c0392b;
}

.collection-bar-status a {
  color: var(--accent);
}

.collection-copy-button {
  margin-left: 0.5rem;
}

.search-form {
  display: flex;
  gap: 0.5rem;
  padding: 0.75rem 1rem 0;
}

.search-form input[type="search"] {
  flex: 1;
  min-width: 0;
  font-size: 1rem;
  padding: 0.6rem;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--bg);
  color: var(--fg);
}

.search-form button {
  padding: 0.6rem 1rem;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--bg);
  color: var(--fg);
  cursor: pointer;
}

.filter-bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.75rem;
  padding: 0.5rem 1rem 0;
}

.unread-toggle {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  font-size: 0.9rem;
  color: var(--fg);
  cursor: pointer;
}

.reset-link {
  font-size: 0.85rem;
  color: var(--accent);
}

.topic-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  padding: 0.75rem 1rem;
  overflow-x: auto;
}

.chip {
  display: inline-block;
  padding: 0.3rem 0.7rem;
  border-radius: 999px;
  border: 1px solid var(--border);
  font-size: 0.85rem;
  color: var(--muted);
  white-space: nowrap;
}

.chip-active {
  border-color: var(--accent);
  color: var(--accent);
}

.chip-small {
  font-size: 0.75rem;
  padding: 0.15rem 0.5rem;
  margin-top: 0.4rem;
}

/* Comment-count badge (m15), sits next to the topic chip in the tile
   metadata area and inline in .summary-meta on the detail page. Not the
   .chip pill style on purpose — it's a live number, not a filterable tag.
   Since m19, also holds the tag chips — a sibling of .card-link rather
   than nested inside it (see _summary_card.html), so it needs its own
   horizontal padding to line up with .card-body's. */
.card-tags {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.4rem;
  padding: 0 0.75rem;
}

/* m19: .card-tags now sits between two pieces of what used to be one
   .card-link/.card-body — the top piece (title/meta/dates) drops its own
   bottom padding so .card-tags's chip margin is the only gap, not both
   stacked. */
.card-body-top {
  padding-bottom: 0;
}

.card-tldr-link {
  display: block;
  padding: 0 0.75rem 0.75rem;
}

/* Tag chips (m19): filled + square corners, no border — reads as a
   different affordance from .chip's outlined pill (topic = category, tag =
   label) without needing a caption. Shared between the tile's .card-tags
   row, the detail page's .summary-tags, and the /tags index. */
.chip-tag {
  display: inline-block;
  padding: 0.15rem 0.5rem;
  border-radius: 4px;
  background: var(--border);
  color: var(--fg);
  font-size: 0.75rem;
  text-decoration: none;
  white-space: nowrap;
}

.chip-tag:hover {
  background: var(--accent);
  color: #fff;
}

/* "+N" overflow marker on the tile — plain text, never a link/toggle: see
   _TASK_m19_tags_display.md §1 (embedding interactive content inside the
   whole-tile link breaks the mobile tap target for no benefit — every tag
   is visible on the conspect itself). */
.chip-more {
  font-size: 0.75rem;
  color: var(--muted);
  white-space: nowrap;
}

.summary-tags {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  margin-bottom: 0.75rem;
}

.tags-index-link {
  align-self: center;
  font-size: 0.85rem;
  color: var(--accent);
  white-space: nowrap;
}

/* Tag editing on the conspect card (m20 §1) — detail page only, see
   _summary_tags_section.html. */
.chip-tag-wrap {
  display: inline-flex;
  align-items: center;
  gap: 0.2rem;
  background: var(--border);
  border-radius: 4px;
}

.chip-tag-wrap .chip-tag {
  background: none;
}

.chip-tag-remove {
  border: none;
  background: none;
  color: var(--muted);
  font-size: 0.9rem;
  line-height: 1;
  padding: 0 0.4rem 0 0;
  cursor: pointer;
}

.chip-tag-remove:hover {
  color: #c0392b;
}

.summary-tags-section {
  margin-bottom: 0.75rem;
}

.tag-add-form {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  margin-top: 0.4rem;
}

.tag-input {
  flex: 1;
  min-width: 8rem;
  padding: 0.35rem 0.6rem;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--bg);
  color: var(--fg);
  font-size: 0.85rem;
}

/* Tag merge (m20 §2) — /t/<slug> only. */
.tag-merge {
  padding: 0.75rem 1rem;
  border-bottom: 1px solid var(--border);
}

.tag-merge-form {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
}

.tag-merge-label {
  font-size: 0.85rem;
  color: var(--muted);
}

.tag-merge-summary {
  font-size: 0.9rem;
  margin: 0 0 0.6rem;
}

.tag-merge-confirm-form {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
}

.comment-stat {
  display: inline-block;
  font-size: 0.78rem;
  color: var(--fg);
  margin-top: 0.4rem;
}

.comment-stat-muted {
  color: var(--muted);
}

.empty {
  padding: 2rem 1rem;
  text-align: center;
  color: var(--muted);
}

.card-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 0.75rem;
  padding: 0 1rem 1rem;
}

@media (min-width: 640px) {
  .card-grid { grid-template-columns: repeat(2, 1fr); }
}

@media (min-width: 960px) {
  .card-grid { grid-template-columns: repeat(3, 1fr); }
}

.card {
  position: relative;
  display: flex;
  flex-direction: column;
  border: 1px solid var(--border);
  border-radius: 10px;
  overflow: hidden;
  background: var(--bg);
}

.card.card-state-new { background: var(--state-new-bg); }
.card.card-state-read { background: var(--state-read-bg); }

/* Selection mode (m8 collections, task Фаза 4): tiles get a small inset
   (the task's "плитки чуть уменьшаются") so the checkbox has a frame of
   its own to sit in instead of covering the thumbnail. Grid column count
   is untouched — the m7 "one flat, even grid" fix keeps working. */
.card-select {
  display: none;
}

body.collection-selecting .card {
  padding: 0.35rem;
}

body.collection-selecting .card-link {
  display: block;
  border-radius: 7px;
  overflow: hidden;
}

body.collection-selecting .card-select {
  display: block;
  position: absolute;
  top: 0.55rem;
  left: 0.55rem;
  width: 1.2rem;
  height: 1.2rem;
  z-index: 2;
  accent-color: var(--accent);
  cursor: pointer;
}

.card-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
  padding: 0 0.75rem 0.75rem;
}

.state-badge {
  font-size: 0.75rem;
  font-weight: 600;
  color: var(--muted);
  margin-right: auto;
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
}

/* Marks (m11): compact ★/⚑ next to the read/new badge, so a tile shows
   whether it's favorited/in-work without opening the card. */
.card-mark {
  font-size: 0.85rem;
  color: var(--mark-active-fg);
}

.state-button {
  font-size: 0.78rem;
  padding: 0.3rem 0.6rem;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--bg);
  color: var(--fg);
  cursor: pointer;
}

/* Gated request-comments button (m15): "0 comments" / "comments disabled"
   — pressing it would burn a run for nothing, see task rationale. */
.state-button:disabled {
  opacity: 0.55;
  cursor: default;
}

/* The five-button row (m14 task's own open tail: "мобильный вид ещё не
   проверен") — the request-comments button is the widest of the five and
   m15 makes it wider still by appending a count. Past 400px it competes
   for space with flex-wrap's normal per-item wrapping; below it, force the
   button onto its own row instead so it doesn't crowd its neighbours. */
@media (max-width: 400px) {
  .state-button-comments {
    flex-basis: 100%;
  }
}

/* Marks (m11): active favorite/work button — own background so it's
   recognizable without reading the label, per theme (see :root above). */
.state-button-active {
  background: var(--mark-active-bg);
  color: var(--mark-active-fg);
  border-color: var(--mark-active-bg);
}

.card-thumb {
  width: 100%;
  aspect-ratio: 16 / 9;
  object-fit: cover;
  display: block;
  background: var(--border);
}

.card-thumb-placeholder {
  background: linear-gradient(135deg, var(--border), transparent);
}

.card-body {
  padding: 0.75rem;
  display: flex;
  flex-direction: column;
}

.card-title {
  font-weight: 600;
  margin-bottom: 0.3rem;
  overflow-wrap: anywhere;
}

.card-meta {
  display: flex;
  flex-wrap: wrap;
  gap: 0.3rem;
  font-size: 0.8rem;
  color: var(--muted);
}

/* Publish date row (m16) — own line between .card-meta (channel/duration)
   and .card-tags (topic/comment count), only rendered when video.published_at
   is known. Same visual weight as .card-meta on purpose: it's the same
   kind of metadata, just a second line because the owner's layout puts
   the camera-tagged publish date and the labelled "конспект" date
   together, separate from channel/duration. */
.card-dates {
  display: flex;
  flex-wrap: wrap;
  gap: 0.3rem;
  font-size: 0.8rem;
  color: var(--muted);
}

.card-published {
  color: var(--fg);
}

.card-tldr {
  margin: 0.5rem 0 0;
  font-size: 0.85rem;
  color: var(--muted);
  overflow-wrap: anywhere;
}

.pagination {
  display: flex;
  justify-content: space-between;
  padding: 1.5rem 1rem 3rem;
  color: var(--accent);
}

/* --- public collection page --- */

.collection-page {
  max-width: 1200px;
  margin: 0 auto;
  padding: 1rem 0 3rem;
}

.collection-title {
  font-size: 1.3rem;
  margin: 0 1rem 0.25rem;
  overflow-wrap: anywhere;
}

.collection-sub {
  margin: 0 1rem 1rem;
  font-size: 0.85rem;
  color: var(--muted);
}

/* --- summary card page --- */

.back-link { color: var(--accent); }

.summary-page {
  max-width: 720px;
  margin: 0 auto;
  padding: 1rem 1rem 3rem;
}

.summary-page.card-state-new { background: var(--state-new-bg); }
.summary-page.card-state-read { background: var(--state-read-bg); }

.summary-thumb-link {
  display: block;
  margin: 0 0 1rem;
  border-radius: 10px;
  overflow: hidden;
}

.summary-thumb {
  display: block;
  width: 100%;
  aspect-ratio: 16 / 9;
  object-fit: cover;
  background: var(--border);
}

.summary-title {
  font-size: 1.3rem;
  margin: 0.25rem 0 0.5rem;
  overflow-wrap: anywhere;
}

.summary-meta {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  align-items: center;
  font-size: 0.85rem;
  color: var(--muted);
  margin-bottom: 0.75rem;
}

.summary-meta a { color: var(--accent); }

.youtube-link {
  display: inline-block;
  margin-bottom: 1.25rem;
  color: var(--accent);
  font-size: 0.9rem;
}

.conspect, .comment-body {
  line-height: 1.6;
  overflow-wrap: anywhere;
}

.conspect h1, .conspect h2, .conspect h3 {
  line-height: 1.3;
  margin-top: 1.5rem;
}

.conspect p, .conspect ul, .conspect ol,
.comment-body p, .comment-body ul, .comment-body ol {
  margin: 0.75rem 0;
}

.conspect a, .comment-body a { color: var(--accent); text-decoration: underline; }

.conspect blockquote, .comment-body blockquote {
  margin: 0.75rem 0;
  padding: 0.25rem 0.75rem;
  border-left: 3px solid var(--border);
  color: var(--muted);
}

/* Comments (m12): pasted-in analysis starts with "# Заголовок" (see
   _comment_example.txt) — left at conspect sizing it would render bigger
   than the video title on the same page. Every level, h1..h6, is clamped
   to the same size as bold body text; only indent and weight tell them
   apart (task's own wording), not font-size. */
.comment-body h1, .comment-body h2, .comment-body h3,
.comment-body h4, .comment-body h5, .comment-body h6 {
  font-size: 1em;
  line-height: 1.4;
  margin: 1em 0 0.4em;
}
.comment-body h1 { font-weight: 800; margin-left: 0; }
.comment-body h2 { font-weight: 750; margin-left: 0.4em; }
.comment-body h3 { font-weight: 700; margin-left: 0.8em; }
.comment-body h4 { font-weight: 650; margin-left: 1.2em; }
.comment-body h5 { font-weight: 600; margin-left: 1.6em; }
.comment-body h6 { font-weight: 550; margin-left: 2em; font-style: italic; }

.share-section {
  margin: 1.25rem 0;
  padding: 1rem;
  border: 1px solid var(--border);
  border-radius: 10px;
  max-width: 100%;
  box-sizing: border-box;
}

.share-section h2 {
  font-size: 1rem;
  margin: 0 0 0.5rem;
}

.share-content {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  min-width: 0;
}

.share-status {
  font-size: 0.85rem;
  color: var(--muted);
  overflow-wrap: anywhere;
}

.share-status code {
  font-size: 0.8rem;
  word-break: break-all;
}

.share-duration-buttons {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  max-width: 100%;
}

/* Comment-visibility level on a share link (m14) — the issuing form (new
   link) and the standalone control (already-live link) share this look. */
.share-scope-label {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  flex-wrap: wrap;
  font-size: 0.85rem;
  color: var(--muted);
  max-width: 100%;
}

.share-scope-label select {
  font-size: 0.85rem;
  padding: 0.3rem 0.4rem;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--bg);
  color: var(--fg);
  max-width: 100%;
}

.share-issue-form {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  min-width: 0;
}

.share-hint {
  font-size: 0.78rem;
  color: var(--muted);
  margin: 0.5rem 0 0;
}

.shared-list {
  list-style: none;
  margin: 1rem 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

.shared-item {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
  padding: 0.6rem 0.75rem;
  border: 1px solid var(--border);
  border-radius: 8px;
}

.shared-item a {
  color: var(--accent);
  flex: 1 1 auto;
  min-width: 12rem;
}

.shared-meta {
  font-size: 0.8rem;
  color: var(--muted);
}

/* --- collections list page (m8 Фаза 5) --- */

.collection-item-info {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  flex: 1 1 16rem;
  min-width: 0;
}

.shared-item code {
  font-size: 0.8rem;
  word-break: break-all;
  color: var(--muted);
}

.shared-item-expired {
  opacity: 0.65;
}

.collections-expired-heading {
  font-size: 1rem;
  margin: 1.5rem 1rem 0;
}

/* --- collection detail / composition page (m9 Фаза 3) --- */

.collection-detail-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
  padding: 0 1rem 1rem;
  font-size: 0.85rem;
  color: var(--muted);
}

.collection-detail-meta code {
  font-size: 0.8rem;
  word-break: break-all;
}

.state-button-danger {
  border-color: #c0392b;
  color: #c0392b;
}

.conspect code, .comment-body code {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.9em;
  background: var(--border);
  padding: 0.1em 0.35em;
  border-radius: 4px;
}

.conspect pre, .comment-body pre {
  overflow-x: auto;
  background: var(--border);
  padding: 0.75rem;
  border-radius: 8px;
}

.conspect pre code, .comment-body pre code { background: none; padding: 0; }

/* Таблицы переносят текст и НЕ прокручиваются вбок.

   Было display:block + overflow-x:auto + white-space:nowrap — тройка, которая
   запрещала перенос и оттого требовала прокрутки на каждой второй странице.
   В корпусе 437 таблиц, из них 92% на 2-4 столбца, а ячейки бывают до 488
   символов — целый абзац в клетке. Для такой формы перенос строго лучше
   прокрутки: колонка текста и так узкая, а возить её вбок на телефоне неудобно.

   table-layout оставлен auto намеренно: при fixed два столбца делятся поровну,
   и у частой пары «короткая метка | длинный текст» половина ширины уходит
   метке впустую. Auto раздаёт ширину по содержимому — ровно то, что нужно. */
.conspect table, .comment-body table {
  display: table;
  width: 100%;
  max-width: 100%;
  border-collapse: collapse;
}

.conspect th, .conspect td,
.comment-body th, .comment-body td {
  border: 1px solid var(--border);
  padding: 0.4rem 0.6rem;
  text-align: left;
  /* Ячейки после переноса разной высоты: без этого короткая метка повисает
     посередине высокой клетки и строка читается как разъехавшаяся. */
  vertical-align: top;
  /* Голая длинная ссылка — единственное, что ещё способно распереть колонку.
     Ссылки тайм-кодов дают видимый текст в десять символов, но вставленный
     руками URL не даёт, и тогда рвём по любому месту. */
  overflow-wrap: anywhere;
  /* Страница объявлена lang="ru", поэтому переносы по слогам работают и
     заметно выручают в узкой колонке. */
  hyphens: auto;
}

/* Единственное исключение из «переносим, а не прокручиваем».

   Замер на живой странице при вьюпорте 375px: таблица на 7 столбцов после
   переноса даёт колонки 43/39/55/74/36/50/45 px — в самой узкой помещается
   пять символов, и текст рвётся по слогам в вертикальную ленту. Это хуже
   прокрутки, а не лучше. Таких таблиц в корпусе 8% (36 из 437), и только им
   прокрутка возвращается — и только на узком экране, на десктопе они
   помещаются целиком.

   :has() выбирает таблицы, у которых в строке есть пятая ячейка. Если браузер
   его не знает, правило просто отбрасывается и таблица переносится, как все
   остальные, — деградация безопасная. */
@media (max-width: 640px) {
  /* Запасной вариант, когда tables.js не отработал: таблица сама себе
     контейнер прокрутки. Колонки при этом ужимаются неравномерно, но
     страницу вбок не рвёт — это здесь главное. */
  .conspect table:has(tr > :nth-child(5)),
  .comment-body table:has(tr > :nth-child(5)) {
    display: block;
    overflow-x: auto;
  }

  /* Основной вариант: обёртку ставит tables.js, и тогда пол ширины достаётся
     таблице целиком, а не каждой ячейке. Разница по замеру на семи столбцах:
     с полом на ячейках все колонки выходили ровно по 83px, включая «№» с
     одним символом; с полом на таблице раскладка auto даёт «№» около 40px, а
     освободившееся уходит колонкам с текстом. */
  .table-scroll > table {
    display: table;
    width: auto;
  }
}

/* Ступени пола по числу столбцов. Ниже примерно 95px на колонку текст
   перестаёт читаться, отсюда и величины. */
.table-scroll {
  overflow-x: auto;
  /* Признак, что блок ездит вбок: без него край читается как обрезанный. */
  border-right: 2px solid var(--accent);
}

.table-scroll > table[data-cols="5"] { min-width: 32rem; }
.table-scroll > table[data-cols="6"] { min-width: 38rem; }
.table-scroll > table[data-cols="7"] { min-width: 44rem; }
.table-scroll > table[data-cols="8"] { min-width: 50rem; }
.table-scroll > table[data-cols="9"] { min-width: 56rem; }

/* Развернуть таблицу в модальном окне (static/tables.js).

   Смысл выигрыша разный на разных экранах, и это честно: на десктопе колонка
   конспекта около 690px при окне в полторы тысячи — окно даёт вдвое больше
   места, и семистолбцовая таблица помещается свободно. На телефоне выигрыш
   скромнее: уходят поля страницы и таблица получает свою поверхность
   прокрутки, не утаскивая за собой страницу. */
.table-expand-wrap {
  margin: -0.4rem 0 1rem;
}

.table-expand {
  font: inherit;
  font-size: 0.82rem;
  color: var(--accent);
  background: none;
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 0.25rem 0.6rem;
  cursor: pointer;
}

.table-expand:hover {
  background: var(--accent);
  border-color: var(--accent);
  color: #fff;
}

.table-modal {
  width: 96vw;
  max-width: 96vw;
  height: 92vh;
  max-height: 92vh;
  padding: 0;
  border: 0;
  border-radius: 10px;
  background: var(--bg);
  color: var(--fg);
  overflow: hidden;
}

.table-modal::backdrop {
  background: rgba(0, 0, 0, 0.6);
}

.table-modal-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 0.6rem 0.9rem;
  border-bottom: 1px solid var(--border);
}

.table-modal-hint {
  font-size: 0.82rem;
  color: var(--muted);
}

.table-modal-close {
  font: inherit;
  font-size: 0.85rem;
  padding: 0.3rem 0.8rem;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: none;
  color: var(--fg);
  cursor: pointer;
}

.table-modal-body {
  /* Обе оси: по ширине окна таблица чаще всего помещается целиком, а вот
     длинная по строкам всё равно потребует вертикальной прокрутки. */
  overflow: auto;
  height: calc(92vh - 3rem);
  padding: 0.9rem;
}

.table-modal-table {
  /* Здесь ширину задаёт содержимое, а не колонка текста: места хватает, и
     ужимать колонки до 83px больше незачем. */
  width: auto;
  min-width: 100%;
  border-collapse: collapse;
}

.table-modal-table th,
.table-modal-table td {
  border: 1px solid var(--border);
  padding: 0.4rem 0.6rem;
  text-align: left;
  vertical-align: top;
  overflow-wrap: anywhere;
  hyphens: auto;
}

/* Пол — таблице, не ячейке, по той же причине, что и в тексте. На широком
   экране эти величины меньше доступной ширины и потому ни на что не влияют:
   работает min-width:100% выше, и колонки расходятся по содержимому. */
.table-modal-table[data-cols="5"] { min-width: 38rem; }
.table-modal-table[data-cols="6"] { min-width: 44rem; }
.table-modal-table[data-cols="7"] { min-width: 50rem; }
.table-modal-table[data-cols="8"] { min-width: 56rem; }
.table-modal-table[data-cols="9"] { min-width: 62rem; }

.conspect img, .comment-body img {
  max-width: 100%;
  height: auto;
}

/* --- comments (m12) --- */

.comments-section {
  margin: 1.5rem 0;
}

.comments-heading {
  font-size: 1.1rem;
  margin: 0 0 0.75rem;
}

.comments-empty {
  color: var(--muted);
  font-size: 0.9rem;
}

.comments-list {
  display: flex;
  flex-direction: column;
  gap: 1rem;
  margin-bottom: 1.5rem;
}

.comment {
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 0.85rem 1rem;
  max-width: 100%;
  box-sizing: border-box;
  min-width: 0;
}

.comment-header {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
  font-size: 0.85rem;
  color: var(--muted);
  margin-bottom: 0.5rem;
}

.comment-author { font-weight: 600; color: var(--fg); }

/* Machine digest marker (m13): без него через месяц не отличить свою
   заметку от машинной (owner's own reasoning in the task doc). */
.comment-source-badge {
  font-size: 0.72rem;
  font-weight: 600;
  padding: 0.1rem 0.45rem;
  border-radius: 999px;
  border: 1px solid var(--border);
  color: var(--muted);
}

.comment-edited-mark {
  font-style: italic;
}

/* Folding (m12): pure CSS, no server round trip. The checkbox precedes
   .comment-body-foldable in the DOM so the ":checked ~" sibling selector
   below can reach it. overflow:hidden (not display:none on the rest) is
   deliberate — the clipped text stays in the DOM, so Ctrl+F still finds it
   even while collapsed; a server-side truncate-and-fetch-more approach
   would have removed it from the DOM entirely. */
.comment-toggle-checkbox {
  position: absolute;
  opacity: 0;
  pointer-events: none;
}

.comment-body-wrap {
  position: relative;
}

.comment-body-foldable {
  max-height: 300px;
  overflow: hidden;
  position: relative;
}

.comment-body-foldable::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 3rem;
  background: linear-gradient(to bottom, transparent, var(--bg));
  pointer-events: none;
}

.comment-toggle-checkbox:checked ~ .comment-body-foldable {
  max-height: none;
}

.comment-toggle-checkbox:checked ~ .comment-body-foldable::after {
  display: none;
}

.comment-toggle-link {
  display: inline-block;
  margin-top: 0.4rem;
  color: var(--accent);
  cursor: pointer;
  font-size: 0.85rem;
}

.comment-toggle-less { display: none; }
.comment-toggle-checkbox:checked ~ .comment-toggle-more { display: none; }
.comment-toggle-checkbox:checked ~ .comment-toggle-less { display: inline-block; }

.comment-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin-top: 0.75rem;
}

/* Edit/delete toggles (m12): native <details>/<summary>, no custom JS —
   clicking the summary a second time collapses it back on its own.
   inline-block keeps "Править"/"Удалить" side by side while closed, but it
   also sizes the closed <details> to shrink-fit its content — and an open
   <details> is still that same shrink-fit box, so a width:100% control
   inside it has no real percentage base to resolve against and falls back
   to the browser's default input/textarea size (~20 characters). Forcing
   the OPEN box to a definite block width breaks that chain: the edit form
   then measures against the same width as the add-comment form instead of
   its own shrunk-to-content container. */
.comment-edit-details, .comment-delete-details {
  display: inline-block;
}

.comment-edit-details[open], .comment-delete-details[open] {
  display: block;
  flex-basis: 100%;
  width: 100%;
}

.comment-edit-summary, .comment-delete-summary {
  list-style: none;
  display: inline-block;
}
.comment-edit-summary::-webkit-details-marker,
.comment-delete-summary::-webkit-details-marker { display: none; }

.comment-edit-label-open, .comment-delete-label-open { display: none; }
.comment-edit-details[open] .comment-edit-label-closed,
.comment-delete-details[open] .comment-delete-label-closed { display: none; }
.comment-edit-details[open] .comment-edit-label-open,
.comment-delete-details[open] .comment-delete-label-open { display: inline; }

.comment-edit-details[open] .comment-edit-summary,
.comment-delete-details[open] .comment-delete-summary {
  background: var(--mark-active-bg);
}

.comment-edit-form, .comment-form {
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
  margin-top: 0.6rem;
  max-width: 100%;
}

.comment-form {
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 1rem;
  box-sizing: border-box;
}

.comment-form-heading {
  font-size: 1rem;
  margin: 0;
}

.comment-field {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  font-size: 0.85rem;
  color: var(--muted);
}

.comment-field input[type="text"] {
  padding: 0.5rem 0.6rem;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--bg);
  color: var(--fg);
  font-size: 0.95rem;
  max-width: 100%;
  box-sizing: border-box;
}

.comment-textarea {
  width: 100%;
  max-width: 100%;
  box-sizing: border-box;
  padding: 0.6rem;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--bg);
  color: var(--fg);
  font-size: 0.95rem;
  font-family: inherit;
  resize: vertical;
}

.comment-hint {
  font-size: 0.78rem;
  color: var(--muted);
  margin: 0;
}

.comment-hint code {
  font-size: 0.85em;
  background: var(--border);
  padding: 0.05em 0.3em;
  border-radius: 3px;
}

.comment-delete-confirm {
  display: inline-block;
  margin-left: 0.5rem;
}

.form-error {
  color: #c0392b;
  font-size: 0.85rem;
  margin: 0;
}

/* --- statistics (m10) --- */

.stats-controls {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.75rem 1.5rem;
  padding: 0.75rem 1rem 0;
}

.stats-controls .topic-chips {
  padding: 0;
}

.stats-range-form {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
}

.stats-range-form label {
  display: flex;
  align-items: center;
  gap: 0.35rem;
  font-size: 0.85rem;
  color: var(--muted);
}

.stats-range-form input[type="date"] {
  font-size: 0.85rem;
  padding: 0.35rem 0.5rem;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--bg);
  color: var(--fg);
}

.stats-notes {
  margin: 0.75rem 1rem 0;
  padding: 0.6rem 0.8rem;
  border: 1px solid var(--border);
  border-radius: 8px;
  font-size: 0.85rem;
  color: var(--muted);
}

.stats-notes p { margin: 0.2rem 0; }

.stats-range-label {
  margin: 0.75rem 1rem 0;
  color: var(--muted);
  font-size: 0.9rem;
}

.stats-total-label {
  margin: 0.25rem 1rem 1rem;
  color: var(--muted);
  font-size: 0.9rem;
}

.stat-tiles {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem;
  margin: 0.75rem 1rem;
}

.stat-tile {
  flex: 1 1 12rem;
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 0.75rem 1rem;
}

.stat-tile-value {
  font-size: 1.6rem;
  font-weight: 600;
}

.stat-tile-label {
  color: var(--muted);
  font-size: 0.85rem;
  margin-top: 0.2rem;
}

.chart-card {
  margin: 0 1rem 1.5rem;
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 1rem;
}

.chart-card h2 {
  font-size: 1rem;
  margin: 0 0 0.75rem;
}

.stats-empty {
  color: var(--muted);
  font-size: 0.9rem;
}

.stats-table {
  border-collapse: collapse;
  font-size: 0.85rem;
}

.stats-table th, .stats-table td {
  border: 1px solid var(--border);
  padding: 0.35rem 0.6rem;
  text-align: left;
}

.stats-table th { color: var(--muted); font-weight: 600; }

.stats-hint {
  color: var(--muted);
  font-size: 0.82rem;
  margin: -0.35rem 0 0.75rem;
}

/* The SVG itself carries fixed pixel width/height (not viewBox scaling) so
   bars stay a legible physical size — see app/charts.py. On a narrow phone
   that means the chart can be wider than the viewport; it scrolls inside
   this wrapper instead of dragging the whole page sideways (task's own
   requirement, Фаза 5). */
.chart-scroll {
  overflow-x: auto;
  max-width: 100%;
}

.stat-svg {
  display: block;
}

.stat-svg text {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}

.stat-grid-line {
  stroke: var(--border);
  stroke-width: 1;
}

.stat-baseline {
  stroke: var(--muted);
  stroke-width: 1;
}

.stat-axis-text {
  fill: var(--muted);
  font-size: 10px;
}

.stat-value-text {
  fill: var(--fg);
  font-size: 11px;
}

.stat-category-label {
  fill: var(--fg);
  font-size: 12px;
}

/* Bar color is chosen separately per theme, not derived by inverting the
   light value — see :root[data-theme="dark"] below and ARCHITECTURE.md. */
.stat-bar {
  fill: var(--stat-bar);
}

.stat-bar-hit {
  fill: transparent;
}

.stat-bar-hit:hover {
  fill: var(--stat-hit-hover);
}

.chart-card details {
  margin-top: 0.75rem;
}

.chart-card summary {
  cursor: pointer;
  color: var(--muted);
  font-size: 0.85rem;
}

.chart-card details table {
  margin-top: 0.5rem;
}

/* Tag search (m23, /tags): client-side filter over the chip list below. */
.tags-search-bar {
  padding: 0.75rem 1rem 0;
}

.tags-search {
  display: block;
  width: 100%;
  box-sizing: border-box;
  /* Not smaller than 16px: Safari on iPhone zooms the page on focus of a
     smaller input and doesn't zoom back out. */
  font-size: 16px;
  padding: 0.6rem;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--bg);
  color: var(--fg);
}

.tags-search-count {
  margin: 0.5rem 0 0;
  font-size: 0.82rem;
  color: var(--muted);
}

/* .tags-index-item below is inline-flex, which beats the UA [hidden]
   default the same way .collection-bar[hidden] handles it for .collection-bar
   (flex) elsewhere in this file — tags_index.js sets .hidden on non-matching
   chips and needs this rule to actually hide them. */
.tags-index-item[hidden] {
  display: none;
}

/* Tag dictionary (m19, /tags): a plain wrapping list, not a card-grid —
   there's nothing here but a name and a count. */
.tags-index {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  padding: 1rem;
}

.tags-index-item {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.3rem 0.7rem;
  border-radius: 4px;
  background: var(--border);
  color: var(--fg);
  font-size: 0.85rem;
  text-decoration: none;
}

.tags-index-item:hover {
  background: var(--accent);
  color: #fff;
}

.tags-index-count {
  color: var(--muted);
  font-size: 0.78rem;
}

.tags-index-item:hover .tags-index-count {
  color: inherit;
}

/* Categories as data (m21, /topics). */
.topics-list {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  padding: 1rem;
}

.topic-row {
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 1rem;
  box-sizing: border-box;
}

.topic-edit-form {
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
}

.topic-row-header {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 0.5rem;
}

.topic-slug {
  font-family: monospace;
  font-size: 0.95rem;
}

.topic-count {
  font-size: 0.82rem;
  color: var(--muted);
}

.topic-field {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  font-size: 0.85rem;
  color: var(--muted);
}

.topic-field input[type="text"],
.topic-field input[type="number"],
.topic-field select {
  padding: 0.5rem 0.6rem;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--bg);
  color: var(--fg);
  font-size: 0.95rem;
  max-width: 100%;
  box-sizing: border-box;
}

.topic-field-order input {
  max-width: 8rem;
}

.topic-textarea {
  width: 100%;
  max-width: 100%;
  box-sizing: border-box;
  min-height: 3.5rem;
  padding: 0.6rem;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--bg);
  color: var(--fg);
  font-size: 0.95rem;
  font-family: inherit;
  resize: vertical;
}

.topic-row-actions {
  display: flex;
}

.topic-delete {
  margin-top: 0.6rem;
  padding-top: 0.6rem;
  border-top: 1px solid var(--border);
}

.topic-delete-confirm {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.5rem;
}

.topic-add {
  padding: 0 1rem 1.5rem;
}

.topic-add-hint {
  font-size: 0.85rem;
  color: var(--muted);
  max-width: 48rem;
}

.topic-add-form {
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 1rem;
  box-sizing: border-box;
  max-width: 100%;
}

/* Collection expiry editor (m24 Часть 1) — the select+button pair added to
   each row on /collections and to the meta block on /collections/<id>.
   .shared-item/.collection-detail-meta already flex-wrap (see above), so
   this just needs to not fight that wrap on a narrow screen. */
.collection-expiry-form {
  display: flex;
  align-items: center;
  gap: 0.4rem;
}

.collection-expiry-form select {
  padding: 0.4rem 0.5rem;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--bg);
  color: var(--fg);
  /* Not smaller than 16px: Safari on iPhone zooms the page on focus of a
     smaller form control and doesn't zoom back out (same rule as
     .tags-search, m23). */
  font-size: 16px;
}

/* Quick-add-to-collection widget on the conspect page (m24 Часть 2). */
.summary-collections-section {
  margin: 1.25rem 0;
  padding: 1rem;
  border: 1px solid var(--border);
  border-radius: 10px;
  max-width: 100%;
  box-sizing: border-box;
}

.summary-collections-section h2 {
  font-size: 1rem;
  margin: 0 0 0.5rem;
}

.summary-collections-membership {
  list-style: none;
  margin: 0 0 0.75rem;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  font-size: 0.9rem;
}

.summary-collections-membership a {
  color: var(--accent);
}

.summary-collections-form {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
  margin-top: 0.6rem;
}

.summary-collections-form input[type="text"],
.summary-collections-form select {
  flex: 1 1 10rem;
  min-width: 0;
  padding: 0.5rem;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--bg);
  color: var(--fg);
  /* Same iPhone-zoom rule as .collection-expiry-form select above. */
  font-size: 16px;
  box-sizing: border-box;
}

/* m24: подписи «(истекла)» и «Пока ни в одной коллекции» в виджете коллекций
   на странице конспекта размечены классом .muted, которого в файле не было —
   без правила они рисовались обычным цветом текста. */
.muted {
  color: var(--muted);
}

/* m24b: "убрать" next to each collection in the conspect-page widget. */
.summary-collections-membership li {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.2rem 0.5rem;
}

.summary-collections-remove {
  font: inherit;
  font-size: 0.8rem;
  color: var(--muted);
  background: none;
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 0.15rem 0.5rem;
  /* A comfortable tap target on a phone without making the row look heavy. */
  min-height: 1.9rem;
  cursor: pointer;
}

.summary-collections-remove:hover {
  color: var(--fg);
  border-color: var(--fg);
}
