/* Channel Scanner — hand-written CSS, no build step, no external fonts.
 *
 * Everything is driven by the token block below, which is redefined once for
 * dark mode. Dark is not an inversion: surfaces get their own values so the
 * sidebar stays the darkest layer and cards still read as lifted.
 */

:root {
    color-scheme: light dark;

    --bg: #f4f5f7;            /* content pane */
    --bg-veil: rgba(244, 245, 247, 0.86);
    --surface: #ffffff;       /* cards, inputs */
    --surface-2: #eceef2;     /* sidebar */
    --surface-3: #e6e9ee;     /* chips, media backdrop */
    --hover: rgba(0, 0, 0, 0.05);

    --text: #14161a;
    --text-2: #43474e;
    /* Dark enough to clear 4.5:1 on the sidebar, the lightest surface it lands on. */
    --muted: #626873;

    --separator: rgba(0, 0, 0, 0.09);
    --separator-strong: rgba(0, 0, 0, 0.16);

    --accent: #0071e3;        /* links and accented text */
    --accent-solid: #0071e3;  /* filled controls */
    --on-accent: #ffffff;
    --accent-tint: rgba(0, 113, 227, 0.10);
    --ring: rgba(0, 113, 227, 0.35);

    --danger: #d1002a;
    --danger-tint: rgba(209, 0, 42, 0.08);
    --ok: #1a7f37;
    --ok-tint: rgba(26, 127, 55, 0.07);

    --shadow: 0 1px 2px rgba(0, 0, 0, 0.05), 0 10px 30px rgba(0, 0, 0, 0.07);

    --r-sm: 8px;
    --r-md: 12px;
    --r-lg: 14px;
    --r-pill: 980px;

    --pane-x: clamp(16px, 3vw, 36px);

    --font: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", Roboto,
            "Helvetica Neue", Helvetica, Arial, sans-serif;
}

@media (prefers-color-scheme: dark) {
    :root {
        --bg: #0f1216;
        --bg-veil: rgba(15, 18, 22, 0.86);
        --surface: #181c22;
        --surface-2: #0a0c0f;
        --surface-3: #232830;
        --hover: rgba(255, 255, 255, 0.06);

        --text: #e9ecf1;
        --text-2: #b8bfc9;
        --muted: #8b93a0;

        --separator: rgba(255, 255, 255, 0.09);
        --separator-strong: rgba(255, 255, 255, 0.18);

        --accent: #6ab0ff;
        --accent-solid: #2f6fd0;
        --on-accent: #ffffff;
        --accent-tint: rgba(106, 176, 255, 0.14);
        --ring: rgba(106, 176, 255, 0.45);

        --danger: #ff7b7b;
        --danger-tint: rgba(255, 123, 123, 0.12);
        --ok: #4ade80;
        --ok-tint: rgba(74, 222, 128, 0.10);

        --shadow: 0 1px 2px rgba(0, 0, 0, 0.4), 0 10px 30px rgba(0, 0, 0, 0.5);
    }
}

/* ---------- Base ---------- */
* { box-sizing: border-box; }
html { overflow-x: clip; -webkit-text-size-adjust: 100%; }
html, body { height: 100%; }

body {
    margin: 0;
    font-family: var(--font);
    font-size: 14px;
    line-height: 1.5;
    color: var(--text);
    background: var(--bg);
    -webkit-font-smoothing: antialiased;
}

img, svg { max-inline-size: 100%; }
a { color: var(--accent); text-decoration: none; }
h1, h2, h3 { letter-spacing: -0.022em; font-weight: 600; }
p { margin: 0; }

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

::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-thumb {
    background: var(--separator-strong);
    border-radius: var(--r-pill);
    border: 3px solid transparent;
    background-clip: content-box;
}

/* ---------- Utilities ---------- */
.sr-only {
    position: absolute;
    width: 1px; height: 1px;
    padding: 0; margin: -1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
}
.skip-link {
    position: fixed;
    top: 8px; left: 8px;
    z-index: 200;
    padding: 10px 16px;
    border-radius: var(--r-md);
    background: var(--surface);
    box-shadow: var(--shadow);
    font-weight: 600;
    transform: translateY(-200%);
}
.skip-link:focus { transform: none; }
.sep { margin: 0 6px; opacity: 0.55; }
.dim { color: var(--muted); }

/* ---------- Toasts ---------- */
/* Bottom-right, not top-right: the top-right corner is where the channel's
   action buttons live, and a toast that covers "Удалить" is a trap. */
.messages {
    position: fixed;
    bottom: 16px; right: 16px;
    z-index: 100;
    display: flex;
    flex-direction: column;
    gap: 8px;
    max-width: min(360px, calc(100vw - 32px));
    margin: 0; padding: 0;
    list-style: none;
}
.msg {
    padding: 11px 15px;
    border-radius: var(--r-md);
    background: var(--surface);
    border: 1px solid var(--separator-strong);
    box-shadow: var(--shadow);
    font-size: 13.5px;
    /* Self-dismissing without JS: a toast that never leaves covers the feed. */
    animation: toast-in 0.22s ease both, toast-out 0.4s ease 7s forwards;
}
.msg.success { color: var(--ok); }
.msg.error { color: var(--danger); }
@keyframes toast-in { from { opacity: 0; transform: translateY(8px); } }
@keyframes toast-out { to { opacity: 0; visibility: hidden; } }

/* ---------- Buttons ---------- */
.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 6px;
    padding: 7px 15px;
    border: 1px solid var(--separator-strong);
    border-radius: var(--r-pill);
    background: var(--surface);
    color: var(--text);
    cursor: pointer;
    font: inherit;
    font-size: 13px;
    font-weight: 500;
    white-space: nowrap;
    transition: background-color 0.15s ease, border-color 0.15s ease;
}
.btn.primary { background: var(--accent-solid); color: var(--on-accent); border-color: transparent; }
.btn.danger { color: var(--danger); border-color: color-mix(in srgb, var(--danger) 35%, transparent); }
.btn.ghost { background: transparent; border-color: transparent; color: var(--text-2); }
.btn.sm { padding: 5px 12px; font-size: 12.5px; }
.btn[disabled] { opacity: 0.5; cursor: default; }

/* ---------- Two-pane layout ---------- */
.layout {
    display: grid;
    grid-template-columns: clamp(248px, 22vw, 320px) 1fr;
    height: 100vh;
    height: 100dvh;
}
/* Grid tracks are min-content-sized by default; without this a wide <select>
   inside the pane pushes the whole page sideways on a 320px phone. */
.layout > * { min-width: 0; }

/* ---------- Sidebar ---------- */
.sidebar {
    display: flex;
    flex-direction: column;
    min-height: 0;
    /* Not decoration: it makes this pane the containing block for the absolutely
       positioned .sr-only labels inside it. Without a positioned ancestor they
       resolve against the document, and an absolute box counts towards the
       document's scrollable overflow — so <body> grows to reach the last one and
       the whole fixed-height layout can be scrolled off screen. */
    position: relative;
    background: var(--surface-2);
    border-right: 1px solid var(--separator);
}
.sidebar-head { padding: 14px 12px 10px; }
.brand { display: flex; align-items: center; gap: 9px; padding: 0 4px 12px; }
.brand-mark {
    width: 24px; height: 24px;
    flex: none;
    border-radius: 7px;
    background: var(--accent-solid) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Cpath fill='%23fff' d='M7 15.6l17-6.4-3 15.2-5.4-3.6-2.6 2.8-.5-4.6z'/%3E%3C/svg%3E") center / 17px no-repeat;
}
.brand-mark.lg { width: 34px; height: 34px; border-radius: 10px; background-size: 24px; }
.brand-name { font-weight: 600; font-size: 15px; letter-spacing: -0.01em; }

.add-form { display: flex; gap: 8px; }
.add-form input {
    flex: 1;
    min-width: 0;
    padding: 8px 13px;
    border: 1px solid var(--separator-strong);
    border-radius: var(--r-pill);
    background: var(--surface);
    color: var(--text);
    font: inherit;
    font-size: 13px;
}
.channel-nav { flex: 1 1 auto; min-height: 0; overflow-y: auto; padding: 4px 8px 8px; }
.channel-list { margin: 0; padding: 0; list-style: none; }
.channel-item a {
    display: flex;
    align-items: center;
    gap: 11px;
    padding: 8px 10px;
    margin: 2px 0;
    border-radius: var(--r-md);
    color: var(--text);
}
.channel-item.active a { background: var(--accent-solid); color: var(--on-accent); }
.channel-item.active .ch-sub { color: rgba(255, 255, 255, 0.82); }
.channel-empty { color: var(--muted); padding: 24px 12px; font-size: 13px; text-align: center; line-height: 1.6; }

.avatar {
    width: 40px; height: 40px;
    flex: none;
    border-radius: 50%;
    object-fit: cover;
    background: var(--surface-3);
    box-shadow: inset 0 0 0 1px var(--separator);
}
.avatar.lg { width: 56px; height: 56px; }
.avatar.placeholder {
    display: flex;
    align-items: center;
    justify-content: center;
    background: linear-gradient(140deg, #7d8794, #59616d);
    color: #fff;
    font-weight: 600;
    font-size: 16px;
}
.avatar.lg.placeholder { font-size: 22px; }
.channel-item.active .avatar.placeholder { background: rgba(255, 255, 255, 0.22); }
.avatar.sm { width: 26px; height: 26px; font-size: 11px; }
/* A section, not a channel: a glyph in the same circle rather than an initial,
   so the pinned row reads as a different kind of thing at a glance. */
.avatar.icon {
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--accent-tint);
}
.avatar.icon svg { width: 18px; height: 18px; fill: var(--accent); }
.avatar.lg.icon svg { width: 26px; height: 26px; }
.channel-item.active .avatar.icon { background: rgba(255, 255, 255, 0.22); }
.channel-item.active .avatar.icon svg { fill: #fff; }

/* The pinned section sits above the channels and is separated from them, so a
   long channel list never makes it look like the first channel. */
.channel-list.pinned {
    margin-bottom: 6px;
    padding-bottom: 6px;
    border-bottom: 1px solid var(--separator);
}

.ch-meta { display: flex; flex-direction: column; min-width: 0; }
.ch-title { font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.ch-sub { display: flex; align-items: center; gap: 5px; font-size: 12px; color: var(--muted); }
.ch-sub--parsing { color: var(--accent); }
.ch-sub--error { color: var(--danger); }
.channel-item.active .ch-sub--error { color: #ffd7d7; }
.dot {
    width: 6px; height: 6px;
    flex: none;
    border-radius: 50%;
    background: currentColor;
    animation: pulse 1.4s ease-in-out infinite;
}
@keyframes pulse { 50% { opacity: 0.25; } }

.sidebar-foot {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
    padding: 8px 12px;
    border-top: 1px solid var(--separator);
}
.account {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-size: 12.5px;
    color: var(--muted);
}

/* ---------- Content pane ---------- */
/* `position: relative` for the same reason as .sidebar: the feed's .sr-only
   labels are absolute, and without it the deepest one — the last post card,
   thousands of pixels down an unscrolled feed — sets the document's height. */
.content { position: relative; overflow-y: auto; padding: 0 var(--pane-x) 56px; }
/* One reading column for the whole pane, centred in it. Wider than this and
   post text runs past the ~90 characters a line can be scanned comfortably.

   Centred rather than left-aligned so the slack sits on both sides instead of
   piling up on the right. The column is the same width in .sticky-head as it is
   here: that bar bleeds out by --pane-x and pads back by the same, so its
   content box matches this one and the header cannot drift from the feed under
   it. Below 920px + padding the auto margins collapse to nothing on their own,
   which is what leaves the phone layout alone. */
.channel-header, .posts, .notice, .empty-pane, .pagination, .toolbar {
    max-width: 920px;
    margin-inline: auto;
}

.empty-pane { padding: clamp(48px, 14vh, 120px) 8px; text-align: center; }
.empty-title { margin: 0; font-size: 16px; font-weight: 600; letter-spacing: normal; }
.empty-hint { margin-top: 6px; color: var(--muted); max-width: 42ch; margin-inline: auto; }

/* Sticky top: identity + filters travel together while the feed scrolls. */
.sticky-head {
    position: sticky;
    top: 0;
    z-index: 10;
    margin: 0 calc(var(--pane-x) * -1) 16px;
    padding: 16px var(--pane-x) 12px;
    border-bottom: 1px solid var(--separator);
    background: var(--bg-veil);
    backdrop-filter: saturate(180%) blur(14px);
    -webkit-backdrop-filter: saturate(180%) blur(14px);
}
@supports not (backdrop-filter: blur(1px)) {
    .sticky-head { background: var(--bg); }
}

.channel-header {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 14px;
}
.ch-head-meta { flex: 1 1 220px; min-width: 0; }
.ch-head-meta h1 { margin: 0 0 2px; font-size: clamp(1.15rem, 1rem + 0.8vw, 1.4rem); }
.ch-head-sub { color: var(--muted); font-size: 13px; }
.ch-head-actions { display: flex; gap: 8px; flex-wrap: wrap; }

/* Two rows by construction: counts + filters, then the top-% chips. Letting
   the chips wrap wherever they fit made the toolbar read as one long sentence. */
.toolbar {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 10px 20px;
    margin-top: 14px;
}
/* The stats block grows, so the filters sit hard right when they share the row
   and fall back to the left edge when they wrap onto their own. */
.feed-stats { flex: 1 1 auto; font-size: 13px; color: var(--text-2); }
.pct-row { flex: 1 0 100%; }
.feed-stats b { color: var(--text); font-weight: 600; }

.controls { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.ctl {
    display: flex;
    align-items: center;
    gap: 6px;
    font-size: 12.5px;
    color: var(--muted);
    font-weight: 500;
}
.controls select, .controls input {
    padding: 6px 10px;
    border: 1px solid var(--separator-strong);
    border-radius: var(--r-sm);
    background: var(--surface);
    color: var(--text);
    font: inherit;
    font-size: 13px;
}
/* The flagged filter is a checkbox among text inputs: the rule above would give
   it a 10px padding box and a border of its own. */
.controls input[type="checkbox"] {
    width: 15px; height: 15px;
    padding: 0;
    accent-color: var(--accent-solid);
}
.ctl.check { cursor: pointer; }

.controls select {
    appearance: none;
    padding-right: 26px;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath fill='%23888f9a' d='M5 6 0 1 1 0l4 4 4-4 1 1z'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: right 9px center;
}
.controls input[type=number] { width: 78px; }
.controls select { max-width: 100%; }
.ctl { min-width: 0; }

/* Inputs share one focus treatment; the ring is the only affordance that moves. */
.add-form input:focus, .controls select:focus, .controls input:focus, .login-card input:focus {
    outline: none;
    border-color: var(--accent);
    box-shadow: 0 0 0 3px var(--ring);
}

.pct-row { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; }
.pct-label { color: var(--muted); font-size: 12.5px; font-weight: 500; }
.chips { display: flex; flex-wrap: wrap; gap: 6px; }
.chip {
    padding: 4px 11px;
    border: 1px solid var(--separator-strong);
    border-radius: var(--r-pill);
    background: var(--surface);
    color: var(--text-2);
    font-size: 12.5px;
    font-weight: 500;
    font-variant-numeric: tabular-nums;
}
.chip.active { background: var(--accent-solid); color: var(--on-accent); border-color: transparent; }
/* The count is the point of a channel chip — it says whether the filter is
   worth pressing — but it must not compete with the name for the row. */
.chip-count {
    margin-left: 6px;
    padding: 1px 5px;
    border-radius: var(--r-pill);
    background: var(--surface-3);
    color: var(--muted);
    font-size: 11px;
}
.chip.active .chip-count { background: rgba(255, 255, 255, 0.22); color: inherit; }

/* A channel filter of two dozen chips is several lines of a toolbar meant to be
   glanced past on the way to the feed. app.js folds it to one line and writes
   the count into this button; text rather than another chip, so the way out of
   the row does not read as one more thing in it. */
.chip.folded { display: none; }
.chips-more {
    padding: 4px 2px;
    border: 0;
    background: none;
    font: inherit;
    font-size: 12.5px;
    font-weight: 500;
    color: var(--accent);
    white-space: nowrap;
    cursor: pointer;
}
.chips-more:hover { text-decoration: underline; }

/* ---------- Notices ---------- */
.notice {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 8px;
    padding: 13px 16px;
    border-radius: var(--r-md);
    background: var(--accent-tint);
    color: var(--accent);
    font-size: 13.5px;
}
.notice .notice-hint { color: var(--muted); }
.notice.error { background: var(--danger-tint); color: var(--danger); }
.notice.error .notice-hint { color: var(--text-2); }

/* ---------- Post cards ---------- */
.posts { display: flex; flex-direction: column; gap: 14px; }
.post-card {
    padding: 16px 18px;
    border: 1px solid var(--separator);
    border-radius: var(--r-lg);
    background: var(--surface);
}

/* Flagged: recognisable while scrolling, without shouting.
   The accent border does the work at a distance; the tint keeps it readable
   when several cards in a row are flagged, where a border alone stops
   registering. No coloured bar down the left edge — the gallery is flush with
   the card's rounded corner and a bar would cut into it. */
.post-card.flagged {
    border-color: var(--accent);
    background: linear-gradient(var(--accent-tint), var(--accent-tint)), var(--surface);
}

/* Posted: spent, not selected. It reads as receding rather than accented —
   this is the one state where the answer is "already dealt with", and a card
   that shouts it would compete with the ones still worth reading. Listed after
   .flagged so a post that is both looks used rather than pending. */
.post-card.posted {
    border-color: var(--separator);
    background: linear-gradient(var(--ok-tint), var(--ok-tint)), var(--surface);
}

/* On a page that lists one mark, every card carries it by definition, so the
   accent border marks nothing and only makes the page shout. The footer button
   still carries the state, and the one card worth marking here is the opposite
   case: one just unmarked.

   Dimmed rather than removed — deleting the card would take the undo with it.
   It says "gone on the next reload" without lying about what is still stored. */
.posts[data-drop-unmarked="flagged"] .post-card.flagged,
.posts[data-drop-unmarked="posted"] .post-card.posted {
    border-color: var(--separator);
    background: var(--surface);
}
/* The *other* mark still says something here, though, and on the favourites
   page it says the most useful thing on it: this one has already been used.
   Restated at the same specificity as the rule above, which would otherwise
   flatten it along with the mark the page is made of. */
.posts[data-drop-unmarked="flagged"] .post-card.posted {
    background: linear-gradient(var(--ok-tint), var(--ok-tint)), var(--surface);
}
.posts[data-drop-unmarked] .post-card.dropped { opacity: 0.42; }

/* Which channel a post came from. Only rendered where cards mix channels, and a
   link rather than a label: naming the channel invites opening it. */
.post-channel {
    display: flex;
    align-items: center;
    gap: 8px;
    margin-bottom: 12px;
    color: var(--text-2);
    font-size: 13px;
    min-width: 0;
}
.post-channel-name { font-weight: 600; color: var(--text); }
/* The username is the part that may be long; it truncates, the title does not. */
.post-channel .dim { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* The two mark buttons: outlined and muted until they mean something, filled
   and coloured once they do. Same 15px box as the footer's stat icons, so the
   row keeps its rhythm. */
.mark-form { display: inline-flex; }
.mark-btn {
    display: inline-flex;
    align-items: center;
    padding: 2px;
    border: 0;
    background: none;
    color: var(--muted);
    cursor: pointer;
    transition: color 0.15s ease;
}
.mark-btn .ic { width: 15px; height: 15px; fill: none; stroke: currentColor; stroke-width: 1.8; }
.post-card.flagged .mark-btn--flagged { color: var(--accent); }
.post-card.posted .mark-btn--posted { color: var(--ok); }
.post-card.flagged .mark-btn--flagged .ic { fill: currentColor; stroke: currentColor; }
/* Fill only, no stroke: the tick is knocked out of the disc, and stroking the
   path draws that knockout closed again — leaving a plain green dot. */
.post-card.posted .mark-btn--posted .ic { fill: currentColor; stroke: none; }

/* Fixed aspect boxes: images arrive lazily, and without a reserved box every
   arrival would shove the rest of the feed down under the reader's cursor. */
.post-gallery {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 4px;
    max-width: 420px;
    margin-bottom: 12px;
    border-radius: var(--r-md);
    overflow: hidden;
}
.post-gallery .media {
    position: relative;
    display: block;
    aspect-ratio: 1 / 1;
    background: var(--surface-3);
}
.post-gallery.single { grid-template-columns: 1fr; }
.post-gallery.single .media { aspect-ratio: 16 / 10; }
/* A lone trailing image spans the row instead of leaving a hole. */
.post-gallery:not(.single) .media:last-child:nth-child(odd) { grid-column: span 2; aspect-ratio: 16 / 9; }
.post-gallery .media img { width: 100%; height: 100%; object-fit: cover; display: block; }
.post-gallery.single .media img { object-fit: contain; }

.play {
    position: absolute;
    top: 50%; left: 50%;
    transform: translate(-50%, -50%);
    width: 46px; height: 46px;
    border-radius: 50%;
    background: rgba(0, 0, 0, 0.55);
    display: flex;
    align-items: center;
    justify-content: center;
}
.play::after {
    content: "";
    border-style: solid;
    border-width: 8px 0 8px 14px;
    border-color: transparent transparent transparent #fff;
    margin-left: 3px;
}

/* A video whose cover thumbnail could not be downloaded: say what it is instead
   of rendering a text-only card for something that is really a video. */
.video-placeholder {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 14px 16px;
    border: 1px solid var(--separator);
    border-radius: var(--r-md);
    background: var(--surface-3);
    color: var(--text-2);
    text-decoration: none;
    transition: background var(--dur, 120ms) ease, border-color var(--dur, 120ms) ease;
}
.video-placeholder:hover { background: var(--hover); border-color: var(--separator-strong); }
.video-placeholder .play {
    position: static;
    transform: none;
    flex: none;
    width: 34px;
    height: 34px;
}

.post-text { max-width: 72ch; line-height: 1.55; color: var(--text-2); white-space: pre-wrap; overflow-wrap: anywhere; }

.reactions { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 13px; }
.reaction {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    padding: 3px 10px;
    border-radius: var(--r-pill);
    background: var(--surface-3);
    font-size: 12.5px;
    font-weight: 600;
    color: var(--text);
    font-variant-numeric: tabular-nums;
}
.reaction .r-emoji {
    font-size: 14px;
    line-height: 1;
    font-family: "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", sans-serif;
    font-variant-emoji: emoji;
}

.post-footer {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 14px;
    margin-top: 14px;
    padding-top: 12px;
    border-top: 1px solid var(--separator);
    font-size: 12.5px;
    color: var(--muted);
    font-variant-numeric: tabular-nums;
}
.stat { display: inline-flex; align-items: center; gap: 5px; }
.stat .ic { width: 15px; height: 15px; fill: currentColor; opacity: 0.75; }
.stat.er {
    padding: 1px 8px;
    border: 1px solid var(--separator);
    border-radius: var(--r-pill);
    color: var(--text-2);
}
.post-date { margin-left: auto; }
.tg-open { font-weight: 500; white-space: nowrap; }

/* ---------- Pagination ---------- */
.pagination {
    display: flex;
    align-items: center;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: 10px;
    margin-top: 20px;
}
.page-range { color: var(--muted); font-size: 12.5px; font-variant-numeric: tabular-nums; }
.page-btns { display: flex; align-items: center; gap: 6px; }
.page-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 34px;
    height: 34px;
    padding: 0 9px;
    border: 1px solid var(--separator-strong);
    border-radius: var(--r-sm);
    background: var(--surface);
    color: var(--text);
    font-size: 13px;
    font-weight: 500;
    font-variant-numeric: tabular-nums;
}
.page-btn.active { background: var(--accent-solid); color: var(--on-accent); border-color: transparent; }
.page-btn.disabled { color: var(--muted); opacity: 0.45; }
.page-status { padding: 0 8px; color: var(--muted); font-size: 13px; font-variant-numeric: tabular-nums; }

/* Mobile-only "back to the channel list" link. */
.back-link { display: none; align-items: center; gap: 6px; padding: 12px 0 2px; font-weight: 500; }
.back-link span { font-size: 19px; line-height: 1; }

/* ---------- Login ---------- */
.login-wrap {
    display: flex;
    justify-content: center;
    align-items: flex-start;
    padding: clamp(48px, 12vh, 140px) 16px 32px;
    min-height: 100vh;
    min-height: 100dvh;
}
.card { background: var(--surface); border: 1px solid var(--separator); border-radius: var(--r-lg); box-shadow: var(--shadow); }
.login-card {
    width: 100%;
    max-width: 340px;
    padding: clamp(24px, 5vw, 32px);
    display: flex;
    flex-direction: column;
    gap: 15px;
}
.login-brand { display: flex; flex-direction: column; align-items: center; gap: 10px; margin-bottom: 4px; }
.login-brand h1 { margin: 0; font-size: 1.4rem; }
.login-card label { display: flex; flex-direction: column; gap: 6px; font-size: 13px; color: var(--text-2); font-weight: 500; }
.login-card input {
    padding: 10px 13px;
    border: 1px solid var(--separator-strong);
    border-radius: var(--r-sm);
    background: var(--surface);
    color: var(--text);
    font: inherit;
    font-size: 14px;
}
.login-card .msg { animation: none; box-shadow: none; }
.login-card .btn { margin-top: 4px; padding: 11px; font-size: 15px; }

/* ---------- Hover, only where a real pointer exists ---------- */
@media (hover: hover) {
    .btn:hover:not([disabled]) { background: var(--hover); }
    .btn.primary:hover { background: var(--accent-solid); filter: brightness(1.08); }
    .btn.danger:hover { background: var(--danger-tint); }
    .btn.ghost:hover { background: var(--hover); color: var(--text); }
    .channel-item a:hover { background: var(--hover); }
    .channel-item.active a:hover { background: var(--accent-solid); filter: brightness(1.06); }
    .chip:hover { background: var(--hover); color: var(--text); }
    .chip.active:hover { background: var(--accent-solid); filter: brightness(1.08); }
    .page-btn:hover:not(.disabled):not(.active) { background: var(--hover); }
    .tg-open:hover, .ch-head-sub a:hover { text-decoration: underline; }
    .post-gallery .media:hover img { filter: brightness(1.05); }
}

/* ---------- Tablet: give the feed the room the sidebar was hoarding ---------- */
@media (max-width: 1100px) {
    .layout { grid-template-columns: 250px 1fr; }
    .ch-head-actions { gap: 6px; }
}

/* ---------- Mobile: one pane at a time ---------- */
@media (max-width: 860px) {
    .layout { grid-template-columns: 1fr; height: auto; min-height: 100dvh; }
    .sidebar, .content { overflow: visible; }
    .sidebar { border-right: none; }
    .channel-nav { overflow: visible; }
    /* Only one pane is ever on screen: the list, or the channel you opened. */
    .layout.has-selection .sidebar { display: none; }
    .layout:not(.has-selection) .content { display: none; }
    .back-link { display: inline-flex; }

    /* Not sticky here: pinned, this block would eat most of a phone screen. */
    .sticky-head { position: static; margin-bottom: 14px; padding-top: 10px; }
    .sidebar-foot { position: sticky; bottom: 0; background: var(--surface-2); }
    /* Clear the pinned account row — a toast landing on "Выйти" is a mis-tap waiting to happen. */
    .messages { bottom: 72px; }

    /* Touch targets: 44px is the smallest reliably tappable box. */
    .btn { min-height: 44px; }
    .btn.sm { min-height: 38px; }
    .page-btn { min-width: 44px; height: 44px; }
    .add-form input, .controls select, .controls input, .login-card input {
        min-height: 44px;
        font-size: 16px; /* below 16px iOS Safari zooms the whole page on focus */
    }
}

/* ---------- Small phones: stack everything that was a row ---------- */
@media (max-width: 560px) {
    .channel-header { gap: 12px; }
    .avatar.lg { width: 44px; height: 44px; }
    .ch-head-actions { width: 100%; }
    .ch-head-actions form, .ch-head-actions .btn { flex: 1 1 auto; }
    .toolbar { flex-direction: column; align-items: stretch; gap: 12px; }
    .controls { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
    .controls .ctl { flex-direction: column; align-items: flex-start; gap: 3px; }
    /* :first-of-type, not :first-child — the form's first child is a hidden input. */
    .controls .ctl:first-of-type { grid-column: 1 / -1; }
    .controls select, .controls input[type=number] { width: 100%; }
    .controls .btn { grid-column: 1 / -1; }
    .post-card { padding: 14px; }
    .post-footer { gap: 10px 14px; }
    .post-date { margin-left: 0; }
    .pagination { justify-content: center; }
    .page-range { width: 100%; text-align: center; }
    .messages { left: 12px; right: 12px; max-width: none; }
}

/* ---------- Motion is optional ---------- */
@media (prefers-reduced-motion: reduce) {
    *, *::before, *::after {
        animation-duration: 0.001ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.001ms !important;
    }
    /* Durations collapse but delays do not, so the toast still clears itself
       after its 7s — it just stops sliding and fading to do it. */
}

/* ==========================================================================
   Settings (apps.appsettings) — appended section, self-contained.
   Everything here is prefixed `app-nav`/`settings-`/`set-` so it cannot reach
   into the dashboard, and every colour comes from the tokens at the top of the
   file, so both themes follow for free.
   ========================================================================== */

:root { --app-nav-h: 38px; }

/* ---------- Staff-only global nav ---------- */
/* A bar above <main>, not a floating pill: the free corners of the dashboard are
   the channel actions (top-right) and the toasts (bottom-right), and a link that
   lands on "Удалить" is a trap. The sibling rules hand the full-height grid back
   the height the bar takes — without them the app grows a second scrollbar. */
.app-nav {
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: 8px;
    height: var(--app-nav-h);
    padding: 0 var(--pane-x);
    border-bottom: 1px solid var(--separator);
    background: var(--surface-2);
}
.app-nav-link {
    padding: 4px 12px;
    border-radius: var(--r-pill);
    color: var(--text-2);
    font-size: 12.5px;
    font-weight: 500;
    transition: background-color 0.15s ease, color 0.15s ease;
}
.app-nav-link.current { background: var(--accent-tint); color: var(--accent); }
.app-nav ~ #main .layout {
    height: calc(100vh - var(--app-nav-h));
    height: calc(100dvh - var(--app-nav-h));
}

/* ---------- Settings page shell ---------- */
.settings-wrap {
    display: flex;
    flex-direction: column;
    gap: 18px;
    max-width: 760px;
    margin: 0 auto;
    padding: clamp(18px, 4vw, 36px) var(--pane-x) 72px;
}
.settings-back { display: inline-flex; align-items: center; gap: 6px; font-size: 13px; font-weight: 500; }
.settings-back span { font-size: 19px; line-height: 1; }
.settings-head h1 { margin: 8px 0 0; font-size: clamp(1.3rem, 1.1rem + 1vw, 1.65rem); }
.settings-sub { margin-top: 8px; max-width: 62ch; color: var(--muted); }
/* Scoped to this page rather than a bare `code` rule: the dashboard has no
   monospace anywhere, and a global element rule would be a surprise waiting. */
.settings-wrap code {
    padding: 1px 5px;
    border-radius: 5px;
    background: var(--surface-3);
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-size: 0.92em;
}

/* ---------- Found-channels table ---------- */
/* Its own scroller: the numbers must not be able to widen the reading column,
   and a narrow window should scroll the table rather than the page. */
.table-scroll { overflow-x: auto; }
.found { width: 100%; border-collapse: collapse; font-size: 13px; }
/* Only the numbers refuse to wrap. The channel column used to as well, and a
   long title then pushed the "Добавить" button out of the scroller — the one
   control the table exists for. */
.found th, .found td { padding: 8px 10px; text-align: left; }
.found td.found-channel { width: 100%; }
.found th { color: var(--muted); font-weight: 500; font-size: 12px; }
.found th a { color: inherit; }
.found th a[aria-current] { color: var(--accent); font-weight: 600; }
.found tbody tr { border-top: 1px solid var(--separator); }
.found .num { font-variant-numeric: tabular-nums; text-align: right; white-space: nowrap; }
.found-title { margin-left: 8px; }
.found-action { text-align: right; white-space: nowrap; }

/* ---------- Cards ---------- */
.set-card { display: flex; flex-direction: column; gap: 16px; padding: clamp(18px, 3.4vw, 26px); }
.set-card-head { display: flex; align-items: center; flex-wrap: wrap; gap: 10px; }
.set-card-head h2 { margin: 0; font-size: 1.05rem; }

/* The badge is the state machine made visible: a half-finished login that looks
   like a fresh form is how a second code gets requested and the first dies. */
.set-badge {
    padding: 3px 10px;
    border-radius: var(--r-pill);
    background: var(--surface-3);
    color: var(--text-2);
    font-size: 12px;
    font-weight: 600;
}
.set-badge--on { background: color-mix(in srgb, var(--ok) 15%, transparent); color: var(--ok); }
.set-badge--wait { background: var(--accent-tint); color: var(--accent); }

.set-note, .set-error {
    padding: 11px 14px;
    border-radius: var(--r-md);
    font-size: 13px;
    line-height: 1.5;
}
.set-note { background: var(--accent-tint); color: var(--accent); }
.set-note code { background: color-mix(in srgb, var(--accent) 12%, transparent); }
.set-error { background: var(--danger-tint); color: var(--danger); }

/* ---------- Fields ---------- */
.set-form { display: flex; flex-direction: column; gap: 18px; }
.set-field { display: flex; flex-direction: column; gap: 5px; min-width: 0; }
.set-label { font-size: 13px; font-weight: 600; color: var(--text); }
.set-field input, .set-field select {
    width: 100%;
    padding: 9px 12px;
    border: 1px solid var(--separator-strong);
    border-radius: var(--r-sm);
    background: var(--surface);
    color: var(--text);
    font: inherit;
    font-size: 14px;
}
/* Same focus treatment as every other input in the app: border plus ring. */
.set-field input:focus, .set-field select:focus {
    outline: none;
    border-color: var(--accent);
    box-shadow: 0 0 0 3px var(--ring);
}
.set-field input[aria-invalid="true"] { border-color: var(--danger); }
.set-help { max-width: 62ch; color: var(--muted); font-size: 12.5px; line-height: 1.5; }
.set-help b { color: var(--text-2); font-weight: 600; }
.set-default { color: var(--text-2); font-size: 12.5px; font-variant-numeric: tabular-nums; }
.set-err { color: var(--danger); font-size: 12.5px; font-weight: 500; }

/* ---------- A secret that is shown as absent, not as blank ---------- */
/* Baseline, not centre: opening <details> makes the right-hand column two rows
   tall, and centring would leave the dots floating beside the new input. */
.set-masked { display: flex; align-items: baseline; flex-wrap: wrap; gap: 8px 12px; }
.set-dots { letter-spacing: 0.16em; color: var(--text-2); font-size: 15px; }
/* <details> so "заменить" needs no JavaScript; the input inside submits either
   way, and an empty one means "keep what is stored". */
.set-replace { flex: 1 1 200px; min-width: 0; }
.set-replace summary { list-style: none; }
.set-replace summary::-webkit-details-marker { display: none; }
.set-replace summary::marker { content: ""; }
.set-replace input { margin-top: 8px; }

/* ---------- Connected account ---------- */
.set-facts {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
    gap: 14px 18px;
    margin: 0;
}
.set-facts > div { min-width: 0; }
.set-facts dt { color: var(--muted); font-size: 12px; font-weight: 500; }
.set-facts dd { margin: 2px 0 0; font-size: 14px; font-weight: 600; overflow-wrap: anywhere; }

.set-actions { display: flex; align-items: center; flex-wrap: wrap; gap: 10px; }
/* Sibling of the login form, never nested inside it: forms do not nest. */
.set-cancel { margin-top: -6px; }

/* ---------- Config groups ---------- */
.set-group { margin: 0; padding: 0; border: 0; min-width: 0; }
.set-group + .set-group { padding-top: 18px; border-top: 1px solid var(--separator); }
.set-legend {
    padding: 0 0 14px;
    color: var(--muted);
    font-size: 11.5px;
    font-weight: 700;
    letter-spacing: 0.07em;
    text-transform: uppercase;
}
.set-grid { display: grid; gap: 20px; }
.set-submit { display: flex; align-items: center; flex-wrap: wrap; gap: 14px; }
.set-submit .set-help { flex: 1 1 260px; }

/* ---------- Hover, only where a real pointer exists ---------- */
@media (hover: hover) {
    .app-nav-link:hover { background: var(--hover); color: var(--text); }
    /* No underline for .settings-back: it is a .btn now, so it already answers
       the pointer with a background, and the underline read as a second,
       conflicting affordance. The rule was left over from when it was a link. */
    .set-help a:hover, .set-note a:hover { text-decoration: underline; }
}

/* ---------- Mobile ---------- */
@media (max-width: 860px) {
    /* Restores the mobile layout rule above, minus the bar's height. */
    .app-nav ~ #main .layout { height: auto; min-height: calc(100dvh - var(--app-nav-h)); }
    .app-nav { padding: 0 16px; }
    /* 44px targets, and 16px so iOS Safari does not zoom the page on focus. */
    .set-field input, .set-field select { min-height: 44px; font-size: 16px; }
}

@media (max-width: 560px) {
    .settings-wrap { gap: 14px; padding-bottom: 48px; }
    .set-dots { flex: 1 1 100%; }
    .set-replace { flex: 1 1 100%; }
    .set-actions .btn, .set-submit .btn { width: 100%; }
    .set-submit { flex-direction: column; align-items: stretch; gap: 10px; }
}

/* ---------- Sidebar: blocked add form, footer actions ---------- */
/* Shown when Telegram is not connected. Disabling the button without saying
   why is the kind of dead end that makes a new install look broken. */
.add-blocked {
    margin-top: 10px;
    padding: 11px 13px;
    border: 1px solid var(--separator);
    border-radius: var(--r-md);
    background: var(--surface);
    font-size: 12.5px;
    color: var(--text-2);
}
.add-blocked p { margin: 0 0 8px; line-height: 1.45; }
.add-blocked p:last-child { margin-bottom: 0; }

.foot-actions { display: flex; align-items: center; gap: 4px; }

/* Icon-only control: the same visual weight as .btn.ghost, but square, so the
   cog does not read as a second "Выйти". */
.icon-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 30px; height: 30px;
    border-radius: var(--r-sm);
    color: var(--muted);
    transition: background-color 0.15s ease, color 0.15s ease;
}
.icon-btn:hover { background: var(--hover); color: var(--text); }
.icon-btn svg { width: 17px; height: 17px; fill: currentColor; }

/* "A newer version is waiting" — the cog is the only control that leads to the
   page which installs it, so the badge belongs on the cog rather than in a
   banner competing with the feed. The ring is the sidebar's own background, so
   the dot reads as separate from the icon at any size. */
.icon-btn.has-update { position: relative; }
.update-dot {
    position: absolute;
    top: 3px; right: 3px;
    width: 7px; height: 7px;
    border-radius: 50%;
    background: var(--accent-solid);
    box-shadow: 0 0 0 2px var(--surface-2);
}

/* The settings page's way back, now a real control rather than a bare link. */
.settings-back { align-self: flex-start; margin-bottom: 14px; }
