/* ---------------------------------------------------------------------------
   app.css — one stylesheet, no build step, no framework.

   Light and dark both come from the same tokens, redefined once under
   `prefers-color-scheme`. There is no theme switch: the operating system already
   has one, and adding a second control that disagrees with it is a setting
   nobody asked for (the same argument that kept UI zoom out of Tetralix).
   --------------------------------------------------------------------------- */

:root {
    --page: #f6f7f9;
    --panel: #ffffff;
    --ink: #1b1f24;
    --ink-quiet: #59636e;
    --line: #d8dee4;
    --accent: #0b5fff;
    --accent-ink: #ffffff;
    --good: #0a6b3d;
    --good-bg: #e8f6ee;
    --bad: #9a1c1c;
    --bad-bg: #fdeaea;
    --radius: 10px;
    --gutter: 16px;
}

@media (prefers-color-scheme: dark) {
    :root {
        --page: #14171a;
        --panel: #1d2125;
        --ink: #e8eaed;
        --ink-quiet: #a6b0ba;
        --line: #333a41;
        --accent: #5b9dff;
        --accent-ink: #0b1220;
        --good: #7fd6a6;
        --good-bg: #16301f;
        --bad: #ff9c9c;
        --bad-bg: #351a1a;
    }
}

* { box-sizing: border-box; }

/* ## ⚠ `hidden` has to be made to win, and this is not pedantry
   The browser hides `[hidden]` with `display: none` from its own stylesheet, and
   **any** author rule that sets `display` beats it. `.site-account` below is a
   flex row, so `account.hidden = true` did exactly nothing and a Sign out button
   sat on the sign-in page — found in the browser on 2026-09-20, after jsdom had
   happily reported `hidden === true`, because jsdom has no cascade.
   One rule, once, rather than remembering it at every element. */
[hidden] { display: none !important; }

body {
    margin: 0;
    background: var(--page);
    color: var(--ink);
    font: 16px/1.5 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
}

/* ## Focus is visible everywhere, and it is not the browser's faint default.
   The standing accessibility rule: a keyboard user must always be able to see
   where they are. `:focus-visible` rather than `:focus` so a mouse click does
   not leave a ring behind. */
:focus-visible {
    outline: 3px solid var(--accent);
    outline-offset: 2px;
    border-radius: 4px;
}

a { color: var(--accent); }

/* The first thing Tab reaches. Off-screen rather than `display: none`, because a
   hidden element cannot be focused and this one has to be. */
.skip-link {
    position: absolute;
    left: -9999px;
    top: 0;
    padding: 10px 16px;
    background: var(--accent);
    color: var(--accent-ink);
    z-index: 10;
}

.skip-link:focus {
    left: 0;
}

.site-header {
    display: flex;
    flex-wrap: wrap;
    gap: 12px;
    align-items: center;
    justify-content: space-between;
    padding: 14px var(--gutter);
    background: var(--panel);
    border-bottom: 1px solid var(--line);
}

.site-title {
    margin: 0;
    font-size: 1.15rem;
    letter-spacing: 0.01em;
}

.site-account {
    display: flex;
    gap: 10px;
    align-items: center;
}

.site-who {
    color: var(--ink-quiet);
    font-size: 0.9rem;
}

/* The live region is for screen readers, not for the screen. Not `display: none`
   and not `hidden`, either of which would stop it being announced at all. */
.announcer {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
}

.main {
    max-width: 880px;
    margin: 0 auto;
    padding: 24px var(--gutter) 48px;
}

.main:focus { outline: none; }

.site-footer {
    max-width: 880px;
    margin: 0 auto;
    padding: 0 var(--gutter) 32px;
    color: var(--ink-quiet);
    font-size: 0.85rem;
}

.panel {
    background: var(--panel);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    padding: 24px;
}

.panel--narrow {
    max-width: 420px;
    margin: 32px auto;
}

.page__title {
    font-size: 1.05rem;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--ink-quiet);
    margin: 32px 0 12px;
}

/* --------------------------------------------------------------- forms --- */

.form { display: grid; gap: 16px; }
.field { display: grid; gap: 6px; }

label {
    font-weight: 600;
    font-size: 0.9rem;
}

input[type="text"],
input[type="email"],
input[type="password"] {
    font: inherit;
    padding: 10px 12px;
    color: var(--ink);
    background: var(--page);
    border: 1px solid var(--line);
    border-radius: 8px;
    width: 100%;
}

.button {
    font: inherit;
    font-weight: 600;
    padding: 10px 16px;
    border-radius: 8px;
    border: 1px solid transparent;
    background: var(--accent);
    color: var(--accent-ink);
    cursor: pointer;
    text-decoration: none;
    display: inline-block;
    white-space: nowrap;
}

.button--quiet {
    background: transparent;
    color: var(--ink);
    border-color: var(--line);
}

/* A disabled control stays visible and stays readable — it is never removed.
   `not-allowed` and the dimming say it cannot be used; its `title` says how to
   change that. */
.button:disabled {
    opacity: 0.55;
    cursor: not-allowed;
}

/* ------------------------------------------------------- the paste boxes --- */

.pastes {
    display: grid;
    gap: 16px;
    grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
}

.paste {
    background: var(--panel);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    padding: 16px;
    display: grid;
    gap: 8px;
    align-content: start;
}

.paste__row {
    display: flex;
    gap: 8px;
}

.hint {
    margin: 0;
    color: var(--ink-quiet);
    font-size: 0.85rem;
}

.note {
    color: var(--ink-quiet);
    font-size: 0.85rem;
    line-height: 1.5;
}

.message {
    margin: 0;
    padding: 8px 10px;
    border-radius: 8px;
    font-size: 0.9rem;
}

.message--good { background: var(--good-bg); color: var(--good); }
.message--bad { background: var(--bad-bg); color: var(--bad); }

.error {
    margin: 0;
    padding: 10px 12px;
    background: var(--bad-bg);
    color: var(--bad);
    border-radius: 8px;
}

.loading { color: var(--ink-quiet); }

/* ----------------------------------------------------------- the groups --- */

.groups { display: grid; gap: 28px; }

.group__title {
    font-size: 0.85rem;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--ink-quiet);
    margin: 0 0 10px;
    padding-bottom: 6px;
    border-bottom: 1px solid var(--line);
}

.group__count { font-weight: 400; }

.channels {
    list-style: none;
    margin: 0;
    padding: 0;
    display: grid;
    gap: 10px;
}

.channel {
    display: flex;
    gap: 14px;
    align-items: center;
    background: var(--panel);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    padding: 12px 14px;
}

.channel__body { flex: 1 1 auto; min-width: 0; }

.channel__title { margin: 0; font-size: 1rem; }

/* The channel name is a button because it does something rather than going
   somewhere, but it should read as a title. The underline on hover is what says
   it is clickable at all. */
.channel__open {
    font: inherit;
    font-weight: 600;
    color: var(--ink);
    background: none;
    border: 0;
    padding: 0;
    cursor: pointer;
    text-align: left;
}

.channel__open:hover { text-decoration: underline; }

.channel__counts {
    margin: 2px 0 0;
    color: var(--ink-quiet);
    font-size: 0.85rem;
}

/* The handle and the group badge on one line, wrapping to two on a phone. */
.channel__meta {
    display: flex;
    align-items: center;
    gap: 8px;
    flex-wrap: wrap;
    margin-top: 2px;
}

.channel__handle {
    color: var(--ink-quiet);
    font-size: 0.85rem;
}

/* Which group a row is in, shown only on "Everything" — the section headings
   this page used to have, reduced to a word that can travel with the row now
   that the list pages.

   ⚠ The colour is not the only thing saying which group it is: the word is
   there too. A badge that means something only if you can tell green from grey
   is not readable by everybody, and the greens here are also close enough that
   the difference would be lost on a bright screen. */
.channel__group {
    display: inline-block;
    padding: 1px 8px;
    font-size: 0.72rem;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    border-radius: 999px;
    border: 1px solid var(--line);
    color: var(--ink-quiet);
    background: var(--page);
}

.channel__group--following {
    border-color: var(--good);
    color: var(--good);
}

/* How many channels this group and this search come to, above the list. It says
   the same numbers as the pager below, which is deliberate: on a page of a
   hundred rows the pager is off the bottom of the screen. */
.page__summary {
    margin: 0 0 12px;
    color: var(--ink-quiet);
    font-size: 0.9rem;
}

.channel__actions {
    display: flex;
    gap: 8px;
    flex-wrap: wrap;
    justify-content: flex-end;
}

.thumb {
    border-radius: 8px;
    background: var(--page);
    object-fit: cover;
    flex: 0 0 auto;
}

.empty {
    background: var(--panel);
    border: 1px dashed var(--line);
    border-radius: var(--radius);
    padding: 28px;
    text-align: center;
    color: var(--ink-quiet);
}

.empty h3 { margin-top: 0; color: var(--ink); }

/* ------------------------------------------------------------- narrow --- */

@media (max-width: 560px) {
    .channel {
        flex-wrap: wrap;
    }

    .channel__actions {
        width: 100%;
        justify-content: flex-start;
    }
}

/* ------------------------------------------------------- the quota gauge --- */

.gauge {
    background: var(--panel);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    padding: 14px 16px;
    margin-top: 16px;
    display: grid;
    gap: 8px;
}

.gauge__track {
    position: relative;
    height: 10px;
    border-radius: 999px;
    background: var(--page);
    border: 1px solid var(--line);
    overflow: hidden;
}

.gauge__fill {
    height: 100%;
    width: 0;
    background: var(--accent);

    /* Not `transition: all`. The width is the only thing that should move, and
       animating a colour change would make "importing has stopped" look like a
       slow fade rather than a state. */
    transition: width 200ms ease-out;
}

/* Where importing stops — 8,000 of YouTube's 10,000. The bar runs to the full
   allowance so the last fifth does not look like a bar that is simply full. */
.gauge__limit {
    position: absolute;
    top: -2px;
    bottom: -2px;
    width: 2px;
    background: var(--ink-quiet);
    opacity: 0.85;
}

.gauge__words,
.gauge__resets {
    margin: 0;
    font-size: 0.85rem;
    color: var(--ink-quiet);
}

.gauge__words { color: var(--ink); }

.gauge--warn .gauge__fill { background: #b8730b; }
.gauge--warn .gauge__words { color: #8a5600; }

.gauge--stopped .gauge__fill { background: var(--bad); }
.gauge--stopped .gauge__words { color: var(--bad); font-weight: 600; }

@media (prefers-color-scheme: dark) {
    .gauge--warn .gauge__fill { background: #e3a13c; }
    .gauge--warn .gauge__words { color: #e3a13c; }
}

/* ------------------------------------------ the thirty-day warning (stage 5) --- */

/* Only drawn when something is past YouTube's limit. The words carry the
   message; the colour and the bar down the side only repeat it for the eye. */
.freshness__warning {
    margin: 0 0 16px;
    padding: 12px 16px;
    color: #8a5600;
    background: var(--panel);
    border: 1px solid var(--line);
    border-left: 4px solid #b8730b;
    border-radius: var(--radius);
}

@media (prefers-color-scheme: dark) {
    .freshness__warning { color: #e3a13c; border-left-color: #e3a13c; }
}

/* --------------------------------------------------- one channel's videos --- */

.channel-head {
    display: flex;
    gap: 14px;
    align-items: flex-start;
    margin-bottom: 20px;
}

.channel-head__body { flex: 1 1 auto; min-width: 0; }

.channel-head__title {
    margin: 0;
    font-size: 1.3rem;
}

.channel-head__counts {
    margin: 4px 0 0;
    color: var(--ink-quiet);
}

.controls {
    display: flex;
    flex-wrap: wrap;
    gap: 12px;
    align-items: flex-end;
    padding: 14px 16px;
    background: var(--panel);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    margin-bottom: 16px;
}

.control { display: grid; gap: 6px; }
.control--search { flex: 1 1 240px; }
.control__row { display: flex; gap: 8px; }

/* The Refresh button sits level with the boxes rather than with their labels,
   with what it found beside it. */
.control--action { align-self: flex-end; display: flex; gap: 10px; align-items: center; }

.refresh__said { color: var(--ink-quiet); font-size: 0.9rem; }

.select,
.search {
    font: inherit;
    padding: 9px 10px;
    color: var(--ink);
    background: var(--page);
    border: 1px solid var(--line);
    border-radius: 8px;
}

.search { width: 100%; }

.videos {
    list-style: none;
    margin: 0;
    padding: 0;
    display: grid;
    gap: 8px;
}

.video {
    display: flex;
    gap: 12px;
    align-items: center;
    background: var(--panel);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    padding: 10px 14px;
}

/* A real checkbox, at a size a finger can hit. 24px because that is WCAG 2.2's
   minimum target size — the label beside it is a much larger target and would
   satisfy the rule on its own, but the box itself costing nothing to get right
   means there is nothing to argue about. */
.video__tick {
    width: 24px;
    height: 24px;
    flex: 0 0 auto;
    accent-color: var(--accent);
    cursor: pointer;
}

/* The whole title and thumbnail are the label, so clicking anywhere along the
   row ticks it — which is what a list of forty needs. */
.video__label {
    flex: 1 1 auto;
    min-width: 0;
    display: flex;
    gap: 12px;
    align-items: center;
    cursor: pointer;
    font-weight: 400;
}

.video__body { min-width: 0; display: grid; gap: 2px; }

.video__title {
    font-weight: 600;
    overflow-wrap: anywhere;
}

.video__facts {
    color: var(--ink-quiet);
    font-size: 0.85rem;
}

/* ⚠ Watched rows are dimmed, and the facts line still says so in words. The
   colour is the convenience; the words are the accessibility. */
.video--watched { opacity: 0.62; }

.video--watched .video__title { font-weight: 400; }

/* Ticked while the Unwatched filter is on: it no longer belongs here, and it
   says so by fading rather than by disappearing under the cursor. */
.video--leaving {
    opacity: 0.35;
    border-style: dashed;
}

.video__link { flex: 0 0 auto; }

.pager {
    display: flex;
    gap: 12px;
    align-items: center;
    justify-content: center;
    margin-top: 18px;

    /* ⚠ Wraps, since the jump box joined it. Without this the row squeezed the
       "1–100 of 7,362" down to nothing on a narrow window rather than moving
       the box to a second line. */
    flex-wrap: wrap;
}

.pager__where {
    color: var(--ink-quiet);
    font-size: 0.9rem;
}

/* ------------------------------------------------------------ the prose --- */

.prose { line-height: 1.65; }
.prose__lead { margin-top: 0; font-size: 1.05rem; }
.prose__section h3 { margin-bottom: 6px; }
.prose__links { padding-left: 20px; }

/* --------------------------------------------------------- the YouTube mark --- */

.site-footer__youtube { margin: 0 0 8px; }

.yt-mark {
    height: 20px;
    width: auto;
    color: var(--ink);
    display: block;
}

@media (max-width: 560px) {
    .video {
        flex-wrap: wrap;
    }

    .video__link {
        margin-left: auto;
    }

    .controls {
        flex-direction: column;
        align-items: stretch;
    }

    /* ⚠ Without this the search box grew a tall empty gap under its label.
       `.control--search` is `flex: 1 1 240px`, which in a row means "take the
       spare width"; once the controls stack it means "take the spare *height*",
       and `.control` is a grid, so the spare height went between the label and
       the box. Seen at 390px on the channels page, and it was there on the
       videos page too — same two rules. */
    .control--search { flex: 0 0 auto; }
}

/* ## Movement is a preference, and some people have turned it off
   Every transition above is a nicety. Somebody who has asked their system for
   less motion — often because it makes them ill — should get none of it. */
@media (prefers-reduced-motion: reduce) {
    *, *::before, *::after {
        transition-duration: 0.01ms !important;
        animation-duration: 0.01ms !important;
    }
}

/* --------------------------------------------------------------- notes --- */

.video-note {
    flex: 1 1 100%;
    display: grid;
    gap: 8px;
    padding-top: 8px;
    border-top: 1px dashed var(--line);
}

/* The note itself, always visible when there is one. `pre-wrap` so the line
   breaks somebody typed are the line breaks they see back. */
.video-note__body {
    margin: 0;
    white-space: pre-wrap;
    overflow-wrap: anywhere;
    color: var(--ink);
    font-size: 0.92rem;
}

.video-note__open { justify-self: start; }
.video-note__label { font-size: 0.85rem; }

/* No maxlength anywhere, and the box grows rather than scrolling at three
   lines — Clive asked for no character limit and a box that fights you at the
   fourth line is a character limit with better manners. */
.video-note__box {
    font: inherit;
    width: 100%;
    min-height: 72px;
    padding: 10px 12px;
    color: var(--ink);
    background: var(--page);
    border: 1px solid var(--line);
    border-radius: 8px;
    resize: vertical;
}

.video-note__actions {
    display: flex;
    gap: 8px;
    align-items: center;
}

.video-note__said {
    color: var(--ink-quiet);
    font-size: 0.85rem;
}

/* --------------------------------------------------- the Takeout import --- */

.page__heading {
    display: flex;
    gap: 12px;
    align-items: baseline;
    justify-content: space-between;
    flex-wrap: wrap;
}

.steps { padding-left: 22px; }
.steps li { margin-bottom: 6px; }

.file {
    font: inherit;
    color: var(--ink);
}

.import__progress {
    margin: 12px 0 0;
    color: var(--ink-quiet);
    font-size: 0.9rem;
    min-height: 1.4em;
}

.import__outcome { display: grid; gap: 8px; margin-top: 8px; }

.report { margin-top: 12px; }

/* ------------------------------------------------- following what you watch --- */

.page__heading-actions {
    display: flex;
    gap: 8px;
    flex-wrap: wrap;
}

.panel__title { margin: 0 0 4px; font-size: 1.05rem; }

.panel__lead {
    margin: 0 0 14px;
    color: var(--ink-quiet);
    max-width: 62ch;
}

/* The shape of the choice: how many channels each threshold would mean. ⚠ Not
   decoration — the difference between 25 and 50 is 68 channels and only 2,352
   videos, and no number box can show that. */
.thresholds { margin-top: 14px; }

.thresholds__lead {
    margin: 0 0 8px;
    font-size: 0.85rem;
    color: var(--ink-quiet);
}

.thresholds__list {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    gap: 8px;
    flex-wrap: wrap;
}

/* Each one reads "100" over "28" — the threshold and what it would follow. */
.thresholds__list .button {
    display: grid;
    gap: 2px;
    justify-items: center;
    min-width: 66px;
    padding: 6px 10px;
}

.thresholds__n { font-weight: 600; }

.thresholds__c {
    font-size: 0.72rem;
    color: var(--ink-quiet);
}

/* The one that is chosen. `aria-pressed` carries the same fact for a screen
   reader, so this is not the only thing saying which it is. */
.button--on {
    border-color: var(--accent);
    box-shadow: inset 0 0 0 1px var(--accent);
}

.button--on .thresholds__c { color: var(--ink); }

/* -------------------------------------------------------------- the queue --- */

.queue { margin-top: 22px; }

.queue__bar {
    width: 100%;
    height: 10px;
    margin: 4px 0 8px;
}

.queue__words { margin: 0 0 4px; }

.queue__trouble {
    margin: 0 0 10px;
    color: var(--ink-quiet);
    font-size: 0.9rem;
}

.queue__actions { margin-bottom: 14px; }

.queue__rows {
    list-style: none;
    margin: 0;
    padding: 0;
    display: grid;
    gap: 8px;
}

.queue__row {
    background: var(--panel);
    border: 1px solid var(--line);
    border-left-width: 4px;
    border-radius: var(--radius);
    padding: 10px 12px;
}

.queue__row--done { border-left-color: var(--good); }
.queue__row--failed { border-left-color: var(--bad); }
.queue__row--working { border-left-color: var(--accent); }

/* ⚠ Skipped keeps the ordinary line colour on purpose: it is not a failure.
   A channel over YouTube's ceiling was never going to be imported, and
   colouring it like a fault would have somebody trying to fix it. */
.queue__title { font-weight: 600; }

.queue__state {
    margin-left: 8px;
    font-size: 0.75rem;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--ink-quiet);
}

.queue__reason,
.queue__imported {
    margin: 4px 0 0;
    color: var(--ink-quiet);
    font-size: 0.85rem;
}

/* ------------------------------------------------------------- the Shorts --- */

/* "plus 1,204 Shorts hidden. Show them" — a button rather than a note, because
   the first thing anybody wants after reading it is to see them. It reads as
   part of the sentence it finishes. */
.counts__shorts {
    font: inherit;
    color: var(--ink-quiet);
    background: none;
    border: 0;
    padding: 0;
    cursor: pointer;
    text-align: left;
    text-decoration: underline;
    text-decoration-style: dotted;
    text-underline-offset: 2px;
}

.counts__shorts:hover { color: var(--ink); text-decoration-style: solid; }

/* ------------------------------------------------------ the page links --- */

/* Added 2026-09-21, because until then the only way to reach #/follow was to
   type it. Real links, so they can be middle-clicked and bookmarked. */
.site-nav {
    display: flex;
    gap: 4px;
    flex-wrap: wrap;
    margin-right: auto;
    margin-left: 8px;
}

.site-nav a {
    padding: 6px 12px;
    border-radius: 999px;
    color: var(--ink-quiet);
    text-decoration: none;
    font-size: 0.92rem;
    font-weight: 600;
}

.site-nav a:hover {
    color: var(--ink);
    background: var(--page);
}

/* ⚠ The page you are on. `aria-current="page"` carries the same fact for a
   screen reader, so this is not the only thing saying it — and the weight
   changes as well as the colour, for anybody who cannot tell the two apart. */
.site-nav__link--on {
    color: var(--accent);
    background: var(--page);
}

/* ------------------------------------------------------- jumping a page --- */

/* Clive, 2026-09-21: *"It will take far too long to go through over 7,000 of
   them 100 at a time"*. The number typed is a row, not a page — his own
   example was 360 landing on 301–400. */
.pager__goto {
    display: flex;
    align-items: center;
    gap: 8px;
    margin-left: auto;
}

.pager__goto label {
    font-size: 0.9rem;
    font-weight: 600;
    white-space: nowrap;
}

.pager__jump {
    font: inherit;
    width: 6.5em;
    padding: 8px 10px;
    color: var(--ink);
    background: var(--page);
    border: 1px solid var(--line);
    border-radius: 8px;
}

.pager__hint {
    color: var(--ink-quiet);
    font-size: 0.85rem;
    white-space: nowrap;
}

@media (max-width: 560px) {
    .pager__goto {
        margin-left: 0;
        width: 100%;
    }

    .site-nav { margin-left: 0; }
}
