/* ==================================================================================
   TT-LIVE Recorder - app.css
   The whole design system. There is no second stylesheet and there is no inline
   style attribute anywhere in the templates, because the Content-Security-Policy
   this app sends has no 'unsafe-inline': a style="" attribute simply does not paint.

   Order: tokens and themes, reset, layout shell, typography, components, the table,
   the responsive table-to-cards switch, state helpers, media queries.

   WHAT A THEME MAY AND MAY NOT CHANGE
   Every account picks a palette (spec 9). base.html puts the id on the root element
   as <html data-theme="...">, `crimson` is the default, and an anonymous visitor on
   the login page has no data-theme at all - which is why :root below IS crimson and
   not some neutral base. A theme is one block of custom-property VALUES, nothing
   else:

     may change - the value of any token declared on :root, and color-scheme.
     may NOT    - a selector, a rule, a class name, a length, a radius, a shadow
                  geometry, or what a token MEANS. --surface-2 is still "one step
                  away from the card" in all five, even though on `licht` that step
                  goes down in lightness instead of up, because on a light page a
                  nested surface gets darker, not lighter.

   So do not put a colour literal in a rule. A literal is a sixth theme that nobody
   can switch off, and it is exactly how a light theme ends up with an invisible
   zebra stripe or a blue glow under a crimson dot - both of which this file had
   until the tokens --zebra, --accent-glow, --overlay-soft, --row-archived,
   --row-weg, --field-border-hover, --saved-ring and --select-arrow were pulled out
   of the rules that hard-coded them. If a rule needs a colour, give it a token and
   set that token in every palette. Three literals are left on purpose, listed here
   so nobody "fixes" them by accident:
     - .player background #000 - the letterbox behind a portrait video. Black in
       every theme, because it is the absence of picture, not a surface;
     - @media print - paper is white and ink is black whatever the screen does;
     - `transparent` as the end state of the tt-saved pulse - an alpha, not a hue.

   CONTRAST IS MEASURED, NOT EYEBALLED (spec 9.3). Every text/background pair every
   theme can actually paint was computed against WCAG 2.1: 4.5:1 for body text, 3:1
   for the focus ring, the form-control border and the accent where it is used as
   chrome. Weakest text pair and weakest non-text pair per theme:
     crimson      --on-accent on --accent              5.40  |  accent/surf-3 3.74
     middernacht  --on-danger on --danger-solid-hover  4.94  |  field-border  3.02
     grafiet      --on-accent on --accent              4.68  |  field-border  3.33
     licht        --placeholder on --surface-3         4.92  |  field-border  3.35
     systeem      is crimson in the dark half and licht in the light half.
   In four of the five the weakest non-text pair is --field-border against the field
   it encloses. Crimson is the exception since it was repainted to oxblood: its
   --surface-3 is a lighter, saturated red now, so the tightest non-text pair there is
   the accent used as the inset underline under the current tab, at 3.74:1, with
   --field-border sitting behind it at 3.93:1. Both clear 3:1, but the pair to watch
   when touching crimson is the accent, not the field border.
   Both were recomputed a second time with prefers-contrast:more folded in, because
   that block comes last in the file and therefore wins; neither state fails.
   --border and --border-strong are hairlines between things that are already told
   apart by their fill, so they sit below 3:1 on purpose in all five, exactly as they
   did in the single-palette version. The line a user must be able to find is
   --field-border, which clears 3:1 against the fill it encloses.

   One known limit, unchanged from the single-palette version: .skip-link:focus draws
   its ring with outline-offset:-3px, so that one ring lands ON --accent instead of on
   the page, and no colour clears 3:1 against both a page and the accent sitting on
   it. It was 1.95:1 when the accent was blue; it is 2.48:1 in crimson - down from
   2.85 because the oxblood repaint had to brighten the accent to keep it clear of
   the new ground - 2.83:1 in grafiet and 2.09:1 in licht. Fixing it properly means
   changing that rule, not the palette.
   ================================================================================== */

/* ==================================================================================
   1. Tokens and themes
   ================================================================================== */

/* -- crimson: the default, and the fallback for a page with no data-theme ------
   A deep OXBLOOD page, not a near-black with a hint of warmth in it. The whole
   surface ladder lives in one family - hue 350-355, the fills at saturation 40-68%
   and --field-border lower again at 38% because it has to stay legible - and steps
   only in lightness, so a card is a lighter oxblood panel sitting on a darker
   oxblood ground rather than a grey box with a red button on it.

   What makes that read as a red theme instead of a dark theme with red chrome is the
   EDGE, not the step. The step itself is small on purpose - --surface is 1.14:1
   against --bg - so the work is done by --border and --border-strong. They are much
   LIGHTER than the fills on either side of them (26% and 33% against a card at 14.5%)
   while holding the same hue, which is what makes the card outline read as a red line
   rather than as a shadow: --border-strong is 1.84:1 on the card and 2.09:1 on the
   page. Lightness is the mechanism, not saturation - only --border-strong (67.9%) is
   in fact more saturated than the card it encloses (64.9%); --border sits at 62.1%
   and --field-border at 37.7%, both BELOW their own fills. Lift the fills apart
   instead of the edges and the ladder turns pink before it turns legible.

   That puts three reds on screen at once - the page, the primary button and the
   destructive button - so the separation this palette was built around matters more
   here, not less. The primary button is BRIGHT crimson with near-black ink; every
   destructive control is DARK rust with white ink. That is how "Verwijderen" stays
   distinguishable from "Opslaan" when the brand colour is itself a red, and three
   things separate them at once: lightness (2.02:1 between the two fills), hue (the
   accent is the cool end of red at ~355deg, everything destructive is the warm end at
   ~16deg), and which way round the ink runs. Any one of those alone would be too
   subtle; together they survive greyscale and the common forms of colour blindness.

   Both fills were re-derived against this ground rather than carried over, and it is
   worth being exact about which one moved, because the obvious guess is wrong. The
   destructive fill did NOT get darker. #8e2f1a was hsl(11, 69%, 33%), picked against a
   near-black page; against the accent it replaced it measured 1.99:1, and on an
   oxblood ground it also sits in the page's own hue. It is now hsl(16, 74%, 34%) -
   five degrees WARMER at the same lightness, which is what parts it from a page that
   is itself red: 2.30:1 against a card and 2.62:1 against the page. The lightness axis
   was bought back at the other end, by lifting the accent from 57% to 62%. Darkening
   the destructive fill instead would have restored the 2:1 and sunk the button into
   the card, which is the trade this ground makes easy to get backwards. */
:root,
[data-theme="crimson"],
[data-theme="systeem"] {
  color-scheme: dark;

  /* -- surfaces, darkest first ---------------------------------------------- */
  --bg:           #24070c;   /* the page */
  --surface:      #3d0d13;   /* cards, the top bar */
  --surface-2:    #4b141b;   /* table head, nested panels */
  --surface-3:    #531d24;   /* form fields, ghost buttons */
  --surface-4:    #602931;   /* hover on surface-3 */

  /* -- lines -----------------------------------------------------------------
     Same hue as the fills they separate, but a good deal LIGHTER - that is what
     makes an edge read as a red line rather than as a shadow. (Not saturation: of
     these three only --border-strong is more saturated than the fill it encloses.) */
  --border:        #6b1926;  /* hairline between rows; decorative only */
  --border-strong: #8d1b2a;  /* card and panel edges */
  --field-border:  #c17680;  /* form controls: 3.93:1 against their own fill */

  /* -- text ------------------------------------------------------------------
     Lifted off the old values: a saturated ground this much lighter than a
     near-black eats contrast, and every one of these was remeasured, not nudged. */
  --text:        #f8edee;    /* 16.51:1 on --bg, 11.69:1 on --surface-3 */
  --muted:       #d9bfc3;    /* 10.98:1 on --bg,  7.77:1 on --surface-3 */
  --placeholder: #cfb4b9;    /*  6.94:1 on --surface-3 */

  /* -- accent --------------------------------------------------------------- */
  --accent:       #e85461;   /* fills and chrome */
  --accent-text:  #f98691;   /* accent as TEXT: 6.99:1 on --surface */
  --accent-hover: #f26e79;   /* also a:hover, so it has to work as text too */
  --on-accent:    #1d070a;   /* 5.40:1 on --accent */

  /* -- semantic --------------------------------------------------------------
     Every tint was lifted with the ground. On a near-black page these sat below the
     card and still read; on an oxblood one at that lightness they vanished into it -
     the old --danger-bg on this card measures 1.03:1. They now sit ABOVE the card
     (1.17:1 to 1.34:1) and each keeps a --*-line edge of 1.7:1 to 2.2:1 against its
     own fill. Green and amber are the pair to watch on a red page: they hold at
     9.52:1 and 8.90:1 as ink on a card, and their fills stay 164deg and 48deg off the
     card's hue, so neither collapses into the family the page belongs to. */
  --ok:        #4ade80;  --ok-bg:      #153327;  --ok-line:      #27684a;
  --warn:      #f0b429;  --warn-bg:    #3d2e0f;  --warn-line:    #785c1c;
  --danger:    #ff9c82;  --danger-bg:  #462316;  --danger-line:  #96482c;
  --info:      #7ab7ff;  --info-bg:    #182d49;  --info-line:    #305a91;
  --archive:   #c9b8fd;  --archive-bg: #2d254b;  --archive-line: #534790;
  --neutral:   #cdc0c6;  --neutral-bg: #3f3133;  --neutral-line: #744e54;

  --danger-solid:       #973917;   /* white on this is 7.21:1 */
  --danger-solid-hover: #b1431b;
  --on-danger:          #ffffff;

  /* Amber, not crimson: a focus ring the same colour as the primary button is a
     focus ring nobody can see on the primary button. It matters more on this ground
     than it did on the old one, where only the button was red. */
  --focus: #ffd166;

  /* -- colours the rules used to hard-code ---------------------------------- */
  --accent-glow:        rgba(232, 84, 97, .20);   /* halo under .brand-dot */
  --field-border-hover: #d897a0;                  /* .input:hover, lighter here */
  --overlay-soft:       rgba(255, 255, 255, .08); /* .flash-close:hover */
  --zebra:              rgba(255, 255, 255, .028); /* a hair stronger: the same alpha
                                                      lifts a lighter ground less */
  --row-archived:       rgba(201, 184, 253, .05);
  --row-weg:            rgba(255, 156, 130, .06);
  --saved-ring:         rgba(74, 222, 128, .55);

  /* The <select> arrow. It has to be a whole url() and not merely a colour: a data:
     URI is parsed as its own document and cannot read a custom property out of the
     page, so the fill is baked in and the token carries the entire image. Keep that
     fill equal to --muted. */
  --select-arrow: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath fill='%23d9bfc3' d='M1.4 0 6 4.6 10.6 0 12 1.4 6 7.4 0 1.4z'/%3E%3C/svg%3E");

  /* -- shape and rhythm ----------------------------------------------------- */
  --r-sm:   6px;
  --r-md:  10px;
  --r-lg:  14px;
  --r-pill: 999px;

  --shadow-1: 0 1px 2px rgba(0, 0, 0, .40);
  --shadow-2: 0 1px 2px rgba(0, 0, 0, .40), 0 10px 28px rgba(0, 0, 0, .30);

  --font: -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  --mono: ui-monospace, Consolas, "Courier New", monospace;

  --tap: 44px;        /* the smallest thing a thumb is asked to hit */
  --shell: 1180px;    /* the content column never grows past this */
}

/* `systeem` is crimson in its dark half, so it shares the block above and changes
   exactly one thing here: it tells the UA that a light half exists, so scrollbars,
   form controls and the like flip over with the rest of the page. */
[data-theme="systeem"] { color-scheme: light dark; }

/* -- middernacht: the palette this app shipped with ---------------------------
   Copied across unchanged, down to the glow behind the brand dot. Somebody has been
   looking at this every day; adding a theme picker is not an excuse to redraw it. */
[data-theme="middernacht"] {
  color-scheme: dark;

  --bg:           #0f1115;
  --surface:      #171a21;
  --surface-2:    #1e222b;
  --surface-3:    #262b36;
  --surface-4:    #2f3542;

  --border:        #2b313d;
  --border-strong: #3d4555;
  --field-border:  #6b7488;

  --text:        #e7e9ee;
  --muted:       #9aa3b2;
  --placeholder: #949daf;

  --accent:       #4f9cf9;
  --accent-text:  #6aa9fb;
  --accent-hover: #6fb0ff;
  --on-accent:    #08111e;

  --ok:        #4ade80;  --ok-bg:      #122a1e;  --ok-line:      #1f5a3c;
  --warn:      #f0b429;  --warn-bg:    #2b2110;  --warn-line:    #6b5115;
  --danger:    #ff8a8a;  --danger-bg:  #2e1719;  --danger-line:  #7a3339;
  --info:      #6aa9fb;  --info-bg:    #122336;  --info-line:    #2b4f7d;
  --archive:   #c4b5fd;  --archive-bg: #221f3a;  --archive-line: #443c72;
  --neutral:   #c3cad6;  --neutral-bg: #232833;  --neutral-line: #3d4555;

  --danger-solid:       #b5323a;
  --danger-solid-hover: #cc3a43;
  --on-danger:          #ffffff;

  --focus: #ffd166;

  --accent-glow:        rgba(79, 156, 249, .18);
  --field-border-hover: #808a9e;
  --overlay-soft:       rgba(255, 255, 255, .08);
  --zebra:              rgba(255, 255, 255, .018);
  --row-archived:       rgba(196, 181, 253, .05);
  --row-weg:            rgba(255, 138, 138, .06);
  --saved-ring:         rgba(74, 222, 128, .55);

  --select-arrow: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath fill='%239aa3b2' d='M1.4 0 6 4.6 10.6 0 12 1.4 6 7.4 0 1.4z'/%3E%3C/svg%3E");
}

/* -- grafiet: dark and deliberately almost colourless -------------------------
   For somebody who finds colour distracting. The accent is a pale slate, so the
   primary button is simply the brightest thing on the page rather than the reddest,
   and the semantic tints keep just enough chroma to stay tellable apart - stripping
   that out too would make "klaar" and "vastgelopen" the same badge. */
[data-theme="grafiet"] {
  color-scheme: dark;

  --bg:           #101113;
  --surface:      #191a1d;
  --surface-2:    #202226;
  --surface-3:    #292b30;
  --surface-4:    #34373d;

  --border:        #2d3036;
  --border-strong: #40434b;
  --field-border:  #767b85;

  --text:        #e7e8eb;
  --muted:       #a5a9b1;
  --placeholder: #9ca0a9;

  /* Light enough to take dark ink (4.68:1) and to let --accent-hover double as the
     a:hover colour, dark enough that the amber ring still shows on it (2.83:1). The
     window between those two is narrow; do not "brighten" this without remeasuring. */
  --accent:       #6e7f98;
  --accent-text:  #a3b0c5;
  --accent-hover: #8695ae;
  --on-accent:    #0d1013;

  --ok:        #86c9a1;  --ok-bg:      #17241c;  --ok-line:      #35513f;
  --warn:      #d6b36d;  --warn-bg:    #262117;  --warn-line:    #574a2e;
  --danger:    #e79d9d;  --danger-bg:  #2a1d1e;  --danger-line:  #5e3c3e;
  --info:      #97b6d8;  --info-bg:    #1a222c;  --info-line:    #3a4d62;
  --archive:   #b6b1d4;  --archive-bg: #232233;  --archive-line: #474765;
  --neutral:   #c0c4cd;  --neutral-bg: #232529;  --neutral-line: #40434b;

  --danger-solid:       #93373d;
  --danger-solid-hover: #a74047;
  --on-danger:          #ffffff;

  --focus: #ffd166;

  --accent-glow:        rgba(110, 127, 152, .22);
  --field-border-hover: #9298a3;
  --overlay-soft:       rgba(255, 255, 255, .08);
  --zebra:              rgba(255, 255, 255, .018);
  --row-archived:       rgba(182, 177, 212, .06);
  --row-weg:            rgba(231, 157, 157, .06);
  --saved-ring:         rgba(134, 201, 161, .55);

  --select-arrow: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath fill='%23a5a9b1' d='M1.4 0 6 4.6 10.6 0 12 1.4 6 7.4 0 1.4z'/%3E%3C/svg%3E");
}

/* -- licht: light page, crimson accent ----------------------------------------
   Not the dark palette upside down. Four things had to be rethought rather than
   inverted:
     1. the surface ladder runs the other way. --bg is a warm off-white and the CARD
        is the white one, because on paper a card catches the light; from --surface
        onwards every nested step gets DARKER, so a field inside a card inside the
        page still reads as recessed.
     2. the accent is dark with white ink, the mirror of the dark themes, and hover
        goes DOWN in lightness instead of up.
     3. the shadows are near-black at a quarter of the opacity. A dark theme's shadow
        is a hole; a light theme's is a smudge, and .40 black under a white card just
        looks like dirt.
     4. the semantic tints are far paler and their ink far darker. #4ade80 on white is
        1.74:1 - the same green that carries "klaar" on near-black is unreadable here,
        so every tint pair was recomputed rather than reused.
   The focus ring cannot stay amber: #ffd166 on white is 1.44:1, which is no ring at
   all. It goes deep navy instead of bright: dark is the only direction that works on
   a light page (11.81:1 on a card), and going dark rather than mid-blue is also what
   keeps the one ring that is drawn INSET, on .skip-link, visible over the crimson
   accent - 2.09:1 there against 1.10:1 for a mid blue. Blue and not black so it still
   reads as a focus ring rather than as a border, and so it can never be mistaken for
   the accent or for a danger state.

   The destructive fill is a much darker rust than the accent on purpose. A light
   theme squeezes every dark colour towards the same corner, and #a3311a next to
   #c4283f measured 1.02:1 - two buttons the same weight, one of which deletes a
   recording. At #6b1c0a it is 2.07:1, the same separation the dark themes have. */
[data-theme="licht"] {
  color-scheme: light;

  --bg:           #f6f3f4;   /* the page */
  --surface:      #ffffff;   /* cards, the top bar - lighter than the page */
  --surface-2:    #f0ebed;   /* and from here on, darker as it nests */
  --surface-3:    #e9e3e6;
  --surface-4:    #dbd3d7;

  --border:        #e0d9dc;
  --border-strong: #cabec4;
  --field-border:  #86777e;  /* 3.35:1 against its own fill */

  --text:        #1f171a;    /* 17.56:1 on --surface, 13.88:1 on --surface-3 */
  --muted:       #63565c;    /*  6.96:1 on --surface,  5.50:1 on --surface-3 */
  --placeholder: #6b5d63;    /*  4.92:1 on --surface-3 */

  --accent:       #c4283f;
  --accent-text:  #a81b31;
  --accent-hover: #a01e33;   /* darker on hover: the light-theme direction */
  --on-accent:    #ffffff;

  --ok:        #10653a;  --ok-bg:      #e3f3ea;  --ok-line:      #93c9ad;
  --warn:      #7a5200;  --warn-bg:    #fbf0d9;  --warn-line:    #dcbb6a;
  --danger:    #a13116;  --danger-bg:  #fbe9e3;  --danger-line:  #e2a894;
  --info:      #14548c;  --info-bg:    #e5f0fa;  --info-line:    #9dc0e2;
  --archive:   #4f3fa6;  --archive-bg: #eeecfa;  --archive-line: #b5abe2;
  --neutral:   #544850;  --neutral-bg: #eeeaec;  --neutral-line: #cbc0c6;

  --danger-solid:       #6b1c0a;
  --danger-solid-hover: #571607;
  --on-danger:          #ffffff;

  --focus: #12327d;

  --accent-glow:        rgba(196, 40, 63, .22);
  --field-border-hover: #5f5258;                 /* darker on hover, not lighter */
  --overlay-soft:       rgba(31, 23, 26, .07);
  --zebra:              rgba(31, 23, 26, .028);
  --row-archived:       rgba(79, 63, 166, .07);
  --row-weg:            rgba(161, 49, 22, .07);
  --saved-ring:         rgba(16, 101, 58, .45);

  --select-arrow: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath fill='%2363565c' d='M1.4 0 6 4.6 10.6 0 12 1.4 6 7.4 0 1.4z'/%3E%3C/svg%3E");

  --shadow-1: 0 1px 2px rgba(31, 23, 26, .10);
  --shadow-2: 0 1px 2px rgba(31, 23, 26, .10), 0 10px 28px rgba(31, 23, 26, .10);
}

/* -- systeem, light half -------------------------------------------------------
   The same values as `licht`, written out instead of shared because the two
   selectors cannot be grouped: `licht` applies whatever the device says, this one
   only inside the media query. Wrapping the selector in @media rather than nesting
   @media inside the selector keeps this parsable by anything that predates CSS
   nesting - an old parser that trips over a nested at-rule can swallow the rest of
   the block, and the failure mode of that is a phone in light mode stuck with dark
   text on a dark page. If you change a value in `licht`, change it here too. */
@media (prefers-color-scheme: light) {
  [data-theme="systeem"] {
    --bg:           #f6f3f4;
    --surface:      #ffffff;
    --surface-2:    #f0ebed;
    --surface-3:    #e9e3e6;
    --surface-4:    #dbd3d7;

    --border:        #e0d9dc;
    --border-strong: #cabec4;
    --field-border:  #86777e;

    --text:        #1f171a;
    --muted:       #63565c;
    --placeholder: #6b5d63;

    --accent:       #c4283f;
    --accent-text:  #a81b31;
    --accent-hover: #a01e33;
    --on-accent:    #ffffff;

    --ok:        #10653a;  --ok-bg:      #e3f3ea;  --ok-line:      #93c9ad;
    --warn:      #7a5200;  --warn-bg:    #fbf0d9;  --warn-line:    #dcbb6a;
    --danger:    #a13116;  --danger-bg:  #fbe9e3;  --danger-line:  #e2a894;
    --info:      #14548c;  --info-bg:    #e5f0fa;  --info-line:    #9dc0e2;
    --archive:   #4f3fa6;  --archive-bg: #eeecfa;  --archive-line: #b5abe2;
    --neutral:   #544850;  --neutral-bg: #eeeaec;  --neutral-line: #cbc0c6;

    --danger-solid:       #6b1c0a;
    --danger-solid-hover: #571607;
    --on-danger:          #ffffff;

    --focus: #12327d;

    --accent-glow:        rgba(196, 40, 63, .22);
    --field-border-hover: #5f5258;
    --overlay-soft:       rgba(31, 23, 26, .07);
    --zebra:              rgba(31, 23, 26, .028);
    --row-archived:       rgba(79, 63, 166, .07);
    --row-weg:            rgba(161, 49, 22, .07);
    --saved-ring:         rgba(16, 101, 58, .45);

    --select-arrow: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath fill='%2363565c' d='M1.4 0 6 4.6 10.6 0 12 1.4 6 7.4 0 1.4z'/%3E%3C/svg%3E");

    --shadow-1: 0 1px 2px rgba(31, 23, 26, .10);
    --shadow-2: 0 1px 2px rgba(31, 23, 26, .10), 0 10px 28px rgba(31, 23, 26, .10);
  }
}

/* ==================================================================================
   2. Reset and element defaults
   ================================================================================== */

*,
*::before,
*::after { box-sizing: border-box; }

html {
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
}

body {
  margin: 0;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font);
  font-size: 1rem;
  line-height: 1.5;
}

[hidden] { display: none !important; }

img, video { max-width: 100%; }
img { height: auto; border: 0; }

a {
  color: var(--accent-text);
  text-decoration: underline;
  text-underline-offset: 2px;
}
a:hover { color: var(--accent-hover); }

h1, h2, h3, h4 {
  margin: 0 0 .5rem;
  line-height: 1.25;
  font-weight: 650;
  letter-spacing: -.01em;
}
h1 { font-size: 1.375rem; }
h2 { font-size: 1.125rem; }
h3 { font-size: 1rem; }

p  { margin: 0 0 .75rem; }
ul, ol { margin: 0 0 .75rem; padding-left: 1.25rem; }

hr {
  border: 0;
  border-top: 1px solid var(--border);
  margin: 1.25rem 0;
}

code, kbd, samp, pre { font-family: var(--mono); font-size: .9375em; }
pre {
  margin: 0 0 .75rem;
  padding: .75rem;
  overflow-x: auto;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
}

/* One focus treatment for everything. outline-offset puts the ring on the page
   background instead of on the control's own fill, so it stays visible on the
   blue primary button as well as on a bare link. */
:focus { outline: none; }
:focus-visible {
  outline: 3px solid var(--focus);
  outline-offset: 2px;
}

::selection { background: var(--accent); color: var(--on-accent); }

/* ==================================================================================
   3. The page shell
   ================================================================================== */

.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 100;
  padding: .75rem 1rem;
  background: var(--accent);
  color: var(--on-accent);
  font-weight: 650;
  text-decoration: none;
  border-radius: 0 0 var(--r-md) 0;
}
.skip-link:focus {
  left: 0;
  outline: 3px solid var(--focus);
  outline-offset: -3px;
}

.topbar {
  background: var(--surface);
  border-bottom: 1px solid var(--border-strong);
  box-shadow: var(--shadow-1);
}

.topbar-inner {
  max-width: var(--shell);
  margin: 0 auto;
  padding: .5rem .75rem;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: .375rem .75rem;
}

.brand {
  display: inline-flex;
  align-items: center;
  gap: .5rem;
  min-height: var(--tap);
  padding: 0 .25rem;
  margin-right: auto;
  color: var(--text);
  font-weight: 700;
  font-size: 1rem;
  letter-spacing: -.01em;
  text-decoration: none;
  white-space: nowrap;
}
.brand:hover { color: var(--text); }

.brand-dot {
  width: 10px;
  height: 10px;
  flex: 0 0 auto;
  border-radius: 50%;
  background: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-glow);
}

.topnav {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: .25rem;
}

.topnav-link {
  display: inline-flex;
  align-items: center;
  gap: .375rem;
  min-height: var(--tap);
  padding: 0 .625rem;
  border-radius: var(--r-sm);
  color: var(--muted);
  font-size: .9375rem;
  font-weight: 550;
  text-decoration: none;
  white-space: nowrap;
}
.topnav-link:hover { background: var(--surface-3); color: var(--text); }
.topnav-link[aria-current="page"] {
  background: var(--surface-3);
  color: var(--text);
  box-shadow: inset 0 -2px 0 var(--accent);
}

.topnav-form { margin: 0; display: inline-flex; }

.main {
  flex: 1 1 auto;
  width: 100%;
}
.main:focus { outline: none; }

.page {
  max-width: var(--shell);
  margin: 0 auto;
  padding: 1rem .75rem 2.5rem;
}

.page-head {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  justify-content: space-between;
  gap: .5rem 1rem;
  margin-bottom: 1rem;
}
.page-title { margin: 0; }
.page-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: .5rem;
}

.footer {
  border-top: 1px solid var(--border);
  background: var(--surface);
  color: var(--muted);
  font-size: .8125rem;
}
.footer-inner {
  max-width: var(--shell);
  margin: 0 auto;
  padding: .875rem .75rem;
  display: flex;
  flex-wrap: wrap;
  gap: .25rem .75rem;
  align-items: center;
  justify-content: space-between;
}

/* ==================================================================================
   4. Typography helpers
   ================================================================================== */

.muted { color: var(--muted); }

/* Sizes, durations and timestamps. Tabular figures so the columns line up. */
.mono {
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  font-size: .9375em;
  letter-spacing: -.01em;
}

.lead { font-size: 1.0625rem; color: var(--muted); margin-bottom: 1rem; }
.small { font-size: .8125rem; }
.strong { font-weight: 650; }
.nowrap { white-space: nowrap; }
.truncate {
  display: block;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.break { overflow-wrap: anywhere; word-break: break-word; }

.visually-hidden {
  position: absolute !important;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

.section-title {
  margin: 1.5rem 0 .625rem;
  font-size: .8125rem;
  font-weight: 700;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--muted);
}

/* ==================================================================================
   5. Cards and panels
   ================================================================================== */

.card {
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-lg);
  padding: 1rem;
  margin: 0 0 1rem;
}
.card-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: .5rem .75rem;
  margin: -.25rem 0 .75rem;
}
.card-title { margin: 0; font-size: 1rem; }
.card-body > :last-child { margin-bottom: 0; }
.card-tight { padding: .75rem; }
.card-flush { padding: 0; overflow: hidden; }

.grid   { display: grid; gap: 1rem;    grid-template-columns: 1fr; }
.grid-2 { display: grid; gap: 1rem;    grid-template-columns: 1fr; }
.grid-3 { display: grid; gap: .75rem;  grid-template-columns: 1fr; }

/* Label/value pairs: the detail page and the status panel. */
.kv { margin: 0; }
.kv-row {
  display: flex;
  flex-wrap: wrap;
  gap: .25rem 1rem;
  justify-content: space-between;
  align-items: baseline;
  padding: .5rem 0;
  border-bottom: 1px solid var(--border);
}
.kv-row:last-child { border-bottom: 0; }
.kv-key { color: var(--muted); font-size: .875rem; }
.kv-val { text-align: right; overflow-wrap: anywhere; }

.stat {
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  padding: .625rem .75rem;
}
.stat-label {
  display: block;
  color: var(--muted);
  font-size: .75rem;
  font-weight: 650;
  letter-spacing: .04em;
  text-transform: uppercase;
}
.stat-value {
  display: block;
  margin-top: .125rem;
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  font-size: 1.125rem;
  font-weight: 650;
}

.toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: .5rem;
  margin: 0 0 1rem;
}
.toolbar-spacer { flex: 1 1 auto; }

/* ==================================================================================
   6. Badges
   ================================================================================== */

/* Contract: library.rows_for_user() gives every row a `status` key out of
   klaar | bezig | gearchiveerd | verplaatsen | weg and a ready-made Dutch
   `status_label`, so a template writes exactly:

       <span class="badge badge--{{ row.status }}">{{ row.status_label }}</span>

   status.snapshot() gives recorder.verdict out of loopt | gestopt | vastgelopen |
   onbekend, and those have modifiers of the same shape. */

.badge {
  display: inline-flex;
  align-items: center;
  gap: .375rem;
  padding: .1875rem .5rem;
  border: 1px solid var(--neutral-line);
  border-radius: var(--r-pill);
  background: var(--neutral-bg);
  color: var(--neutral);
  font-size: .8125rem;
  font-weight: 650;
  line-height: 1.4;
  white-space: nowrap;
  vertical-align: baseline;
}
.badge::before {
  content: "";
  width: 7px;
  height: 7px;
  flex: 0 0 auto;
  border-radius: 50%;
  background: currentColor;
}
.badge-plain::before { display: none; }

/* -- recording status ------------------------------------------------------- */
.badge--klaar        { background: var(--ok-bg);      border-color: var(--ok-line);      color: var(--ok); }
.badge--bezig        { background: var(--info-bg);    border-color: var(--info-line);    color: var(--info); }
.badge--gearchiveerd { background: var(--archive-bg); border-color: var(--archive-line); color: var(--archive); }
.badge--verplaatsen  { background: var(--warn-bg);    border-color: var(--warn-line);    color: var(--warn); }
.badge--weg          { background: var(--danger-bg);  border-color: var(--danger-line);  color: var(--danger); }

/* -- recorder verdict ------------------------------------------------------- */
.badge--loopt       { background: var(--ok-bg);      border-color: var(--ok-line);      color: var(--ok); }
.badge--gestopt     { background: var(--neutral-bg); border-color: var(--neutral-line); color: var(--neutral); }
.badge--vastgelopen { background: var(--danger-bg);  border-color: var(--danger-line);  color: var(--danger); }
.badge--onbekend    { background: var(--neutral-bg); border-color: var(--neutral-line); color: var(--neutral); }

/* -- generic tones, for anything that is not one of the two lists above ----- */
.badge--ok      { background: var(--ok-bg);      border-color: var(--ok-line);      color: var(--ok); }
.badge--info    { background: var(--info-bg);    border-color: var(--info-line);    color: var(--info); }
.badge--warn    { background: var(--warn-bg);    border-color: var(--warn-line);    color: var(--warn); }
.badge--danger  { background: var(--danger-bg);  border-color: var(--danger-line);  color: var(--danger); }
.badge--muted   { background: var(--neutral-bg); border-color: var(--neutral-line); color: var(--neutral); }
/* The accent badge (the "jij" / administrator marker) sits on --surface-3 and takes
   its edge from the accent, NOT on the --info tint. It used to borrow the info slab
   while keeping accent ink, which was survivable while the accent was blue and the
   two were near neighbours - with a crimson accent it renders as pink text on a dark
   navy block, two colours that do not belong in the same badge. Accent ink on
   surface-3 is a pair this file already leans on elsewhere and it measures 5.64:1
   (crimson), 5.86 (middernacht), 6.46 (grafiet) and 5.79 (licht). */
.badge--accent  { background: var(--surface-3);  border-color: var(--accent);       color: var(--accent-text); }

.badge-sm { font-size: .75rem; padding: .125rem .4375rem; }
.badge-count {
  margin-left: .25rem;
  padding: .0625rem .375rem;
  border-radius: var(--r-pill);
  background: var(--surface-4);
  color: var(--text);
  font-family: var(--mono);
  font-size: .75rem;
  font-weight: 650;
}

/* ==================================================================================
   7. Buttons
   ================================================================================== */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: .4375rem;
  min-height: var(--tap);
  padding: .5rem .875rem;
  border: 1px solid var(--border-strong);
  border-radius: var(--r-md);
  background: var(--surface-3);
  color: var(--text);
  font-family: inherit;
  font-size: .9375rem;
  font-weight: 600;
  line-height: 1.2;
  text-align: center;
  text-decoration: none;
  white-space: nowrap;
  cursor: pointer;
  -webkit-appearance: none;
  appearance: none;
}
.btn:hover { background: var(--surface-4); color: var(--text); }
.btn:active { transform: translateY(1px); }
.btn[disabled],
.btn[aria-disabled="true"] {
  opacity: .5;
  cursor: not-allowed;
  transform: none;
}

.btn-primary {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--on-accent);
}
.btn-primary:hover {
  background: var(--accent-hover);
  border-color: var(--accent-hover);
  color: var(--on-accent);
}

.btn-danger {
  background: var(--danger-solid);
  border-color: var(--danger-solid);
  color: var(--on-danger);
}
.btn-danger:hover {
  background: var(--danger-solid-hover);
  border-color: var(--danger-solid-hover);
  color: var(--on-danger);
}

.btn-ghost {
  background: transparent;
  border-color: var(--border-strong);
  color: var(--text);
}
.btn-ghost:hover { background: var(--surface-3); }

.btn-link {
  background: none;
  border-color: transparent;
  color: var(--accent-text);
  text-decoration: underline;
  text-underline-offset: 2px;
  padding-left: .25rem;
  padding-right: .25rem;
}
.btn-link:hover { background: none; color: var(--accent-hover); }

/* Dense rows. Still a 44px target wherever the pointer is a finger. */
.btn-sm {
  min-height: 36px;
  padding: .3125rem .625rem;
  font-size: .875rem;
  border-radius: var(--r-sm);
}
.btn-block { display: flex; width: 100%; }

.btn-row {
  display: flex;
  flex-wrap: wrap;
  gap: .5rem;
  align-items: center;
}
.btn-row > form { margin: 0; display: inline-flex; }

/* ==================================================================================
   8. Forms
   ================================================================================== */

form { margin: 0; }

.field { display: block; margin: 0 0 1rem; }
.field-label {
  display: block;
  margin-bottom: .3125rem;
  font-size: .875rem;
  font-weight: 650;
  color: var(--text);
}
.field-hint {
  display: block;
  margin-top: .3125rem;
  font-size: .8125rem;
  color: var(--muted);
}
.field-error {
  display: block;
  margin-top: .3125rem;
  font-size: .8125rem;
  font-weight: 600;
  color: var(--danger);
}
.field-row {
  display: flex;
  flex-wrap: wrap;
  gap: .5rem;
  align-items: flex-end;
}
.field-grow { flex: 1 1 14rem; min-width: 0; }

.input,
.select,
.textarea {
  display: block;
  width: 100%;
  min-height: var(--tap);
  padding: .5625rem .6875rem;
  background: var(--surface-3);
  border: 1px solid var(--field-border);
  border-radius: var(--r-md);
  color: var(--text);
  font-family: inherit;
  font-size: 1rem;              /* 16px: anything smaller makes iOS zoom on focus */
  line-height: 1.4;
  -webkit-appearance: none;
  appearance: none;
}
.textarea {
  min-height: 7rem;
  resize: vertical;
  line-height: 1.5;
}
.input::placeholder,
.textarea::placeholder { color: var(--placeholder); opacity: 1; }

.input:hover,
.select:hover,
.textarea:hover { border-color: var(--field-border-hover); }

.input:disabled,
.select:disabled,
.textarea:disabled { opacity: .55; cursor: not-allowed; }

.input[aria-invalid="true"],
.select[aria-invalid="true"],
.textarea[aria-invalid="true"] { border-color: var(--danger); }

/* The arrow is a data: URI, because CSP img-src allows data: and forbids
   everything else - there is no file to 404 and no request to make. The token
   carries the whole url() and not just the fill, because a data: URI is parsed as
   its own document and cannot read a custom property out of this one. */
.select {
  padding-right: 2.25rem;
  background-image: var(--select-arrow);
  background-repeat: no-repeat;
  background-position: right .75rem center;
  background-size: 12px 8px;
}

.checkbox {
  display: flex;
  align-items: center;
  gap: .625rem;
  min-height: var(--tap);
  cursor: pointer;
  font-size: .9375rem;
}
.checkbox input[type="checkbox"],
.checkbox input[type="radio"] {
  width: 20px;
  height: 20px;
  flex: 0 0 auto;
  margin: 0;
  accent-color: var(--accent);
  cursor: pointer;
}
.checkbox-text { min-width: 0; }

.search {
  display: flex;
  flex: 1 1 15rem;
  min-width: 0;
  gap: .5rem;
}
.search .input { flex: 1 1 auto; }

fieldset {
  margin: 0 0 1rem;
  padding: .875rem 1rem 1rem;
  border: 1px solid var(--border-strong);
  border-radius: var(--r-md);
}
legend {
  padding: 0 .375rem;
  font-size: .8125rem;
  font-weight: 700;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--muted);
}

/* ==================================================================================
   9. Flashes
   ================================================================================== */

.flashes { margin: 0 0 1rem; display: grid; gap: .5rem; }
.flashes:empty { display: none; margin: 0; }

.flash {
  display: flex;
  align-items: flex-start;
  gap: .625rem;
  padding: .6875rem .75rem;
  border: 1px solid var(--neutral-line);
  border-left-width: 4px;
  border-radius: var(--r-md);
  background: var(--neutral-bg);
  color: var(--text);
  font-size: .9375rem;
}
.flash-ok  { background: var(--ok-bg);     border-color: var(--ok-line);     border-left-color: var(--ok); }
.flash-err { background: var(--danger-bg); border-color: var(--danger-line); border-left-color: var(--danger); }

.flash-icon { flex: 0 0 auto; font-weight: 700; line-height: 1.5; }
.flash-ok  .flash-icon { color: var(--ok); }
.flash-err .flash-icon { color: var(--danger); }
.flash-text { flex: 1 1 auto; margin: 0; overflow-wrap: anywhere; }

.flash-close {
  flex: 0 0 auto;
  width: 28px;
  height: 28px;
  margin: -.125rem -.25rem 0 0;
  padding: 0;
  border: 0;
  border-radius: var(--r-sm);
  background: transparent;
  color: inherit;
  font-family: inherit;
  font-size: 1.125rem;
  line-height: 1;
  opacity: .7;
  cursor: pointer;
}
.flash-close:hover { opacity: 1; background: var(--overlay-soft); }

.flash-enter { animation: tt-slide-in .16s ease-out; }
@keyframes tt-slide-in {
  from { opacity: 0; transform: translateY(-4px); }
  to   { opacity: 1; transform: none; }
}

/* ==================================================================================
   10. Empty state
   ================================================================================== */

.empty-state {
  padding: 2rem 1rem;
  border: 1px dashed var(--border-strong);
  border-radius: var(--r-lg);
  background: var(--surface);
  text-align: center;
  color: var(--muted);
}
.empty-state-title {
  margin: 0 0 .375rem;
  color: var(--text);
  font-size: 1rem;
  font-weight: 650;
}
.empty-state p:last-child { margin-bottom: 0; }

/* ==================================================================================
   11. Tabs
   ================================================================================== */

.tabs {
  display: flex;
  flex-wrap: wrap;
  gap: .375rem;
  margin: 0 0 1rem;
  padding: .25rem;
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-md);
}
.tab {
  display: inline-flex;
  align-items: center;
  gap: .375rem;
  min-height: 40px;
  padding: 0 .75rem;
  border-radius: var(--r-sm);
  color: var(--muted);
  font-size: .9375rem;
  font-weight: 600;
  text-decoration: none;
  white-space: nowrap;
}
.tab:hover { background: var(--surface-3); color: var(--text); }
.tab[aria-current="page"],
.tab.is-current {
  background: var(--surface-3);
  color: var(--text);
  box-shadow: inset 0 -2px 0 var(--accent);
}

/* ==================================================================================
   12. The table, and the table-to-cards switch
   ================================================================================== */

/* The mechanism, stated once so every page does the same thing:

   <div class="table-wrap">
     <table class="table">
       <thead><tr><th scope="col">Datum</th>...</tr></thead>
       <tbody>
         <tr data-row data-filter-row data-filter-text="...">
           <td data-label="Datum">...</td>
           <td data-label="" class="actions">...</td>   <- empty label = no label
         </tr>

   At 760px and up this is an ordinary table inside a horizontally scrolling
   wrapper. Below 760px every table part becomes display:block, the <thead> is
   taken out of view, each <tr> turns into a card and each <td> prints its own
   header from td::before { content: attr(data-label) }. That is why data-label on
   every <td> is mandatory: without it the card is a column of naked values. */

.table-wrap {
  margin: 0 0 1rem;
  border-radius: var(--r-lg);
}

.table {
  width: 100%;
  border-collapse: collapse;
  font-size: .9375rem;
}

.table caption {
  padding: 0 0 .5rem;
  color: var(--muted);
  font-size: .8125rem;
  text-align: left;
}

.table th,
.table td {
  padding: .625rem .75rem;
  text-align: left;
  vertical-align: middle;
}

.table thead th {
  position: sticky;
  top: 0;
  z-index: 1;
  background: var(--surface-2);
  border-bottom: 1px solid var(--border-strong);
  color: var(--muted);
  font-size: .75rem;
  font-weight: 700;
  letter-spacing: .05em;
  text-transform: uppercase;
  white-space: nowrap;
}

.table tbody tr { border-bottom: 1px solid var(--border); }
.table tbody tr:last-child { border-bottom: 0; }
.table tbody tr:hover { background: var(--surface-2); }

.table td.num,
.table th.num {
  text-align: right;
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
}
.table td.actions,
.table th.actions { text-align: right; white-space: nowrap; }
.table td.actions .btn-row { justify-content: flex-end; }

.table-zebra tbody tr:nth-child(even) { background: var(--zebra); }

/* A row whose file is gone, or that this user archived, reads differently. */
.table tbody tr[data-archived="1"] { background: var(--row-archived); }
.table tbody tr[data-status="weg"] { background: var(--row-weg); }

@media (min-width: 760px) {
  .table-wrap {
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    background: var(--surface);
    border: 1px solid var(--border-strong);
  }
  .table thead th:first-child { border-top-left-radius: var(--r-lg); }
  .table thead th:last-child  { border-top-right-radius: var(--r-lg); }
}

@media (max-width: 759.98px) {
  .table,
  .table thead,
  .table tbody,
  .table tfoot,
  .table tr,
  .table td { display: block; }

  .table thead {
    position: absolute;
    width: 1px; height: 1px;
    margin: -1px; padding: 0;
    overflow: hidden;
    clip: rect(0 0 0 0);
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
  }

  /* ------------------------------------------------------------------------
     A card, not a stack of nine labelled rows.

     The first version of this gave every <td> its own "Label .......  waarde"
     line with a hairline under it. That is a faithful transposition of the
     table and it is the wrong shape for a phone: nine rows plus a rename field
     plus a note field made one recording about 350px tall, so two of them
     filled a screen and the 10px between cards disappeared between all the
     other horizontal rules. Scrolling it, you could not see where one
     recording ended and the next began - which is exactly what the owner
     reported.

     So the row becomes a flex container and the cells are placed by hand into
     four bands:

         za 13 sep 2026, 22:29                      [Klaar]     <- 1 datum + status
         @langejohan2.10m84                                     <- 2 what you scan for
         1:52:03 · 2,2 GB · 3.106 chat · 412 kijkers            <- 3 the numbers, one line
         [Bekijken] [Afspelen] [Downloaden] ...                 <- 4 actions

     Band 3 is where the height goes: four labelled rows become one muted line,
     because "2,2 GB" and "1:52:03" say what they are without a label in front
     of them, and the two that do not get a word after the number instead.

     Nothing is removed from the phone except the admin rename field - see the
     note at .table td[data-label="Handle / titel"] form below.
     ------------------------------------------------------------------------ */
  .table tbody tr {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    /* The row gap is deliberately smaller than the space BETWEEN cards: bands
       inside one recording belong together, cards do not. */
    gap: .375rem .5rem;
    margin: 0 0 1.125rem;          /* was .625rem - "meer ruimte tussen de blokken" */
    padding: .8125rem .875rem;
    background: var(--surface);
    border: 1px solid var(--border-strong);
    border-radius: var(--r-lg);
  }
  .table tbody tr:last-child {
    border-bottom: 1px solid var(--border-strong);
    margin-bottom: 0;
  }
  .table tbody tr:hover { background: var(--surface); }

  /* Cells are plain blocks now. No hairlines: the card edge is the separator,
     and a rule under every value is what made this look like a form. */
  .table td {
    display: block;
    flex: 0 0 auto;
    padding: 0;
    border-bottom: 0;
    text-align: left;
    white-space: normal;
  }
  /* The desktop .num alignment would push these to the right of a block they
     now fill exactly. */
  .table td.num { text-align: left; }

  /* Labels off by default; the four bands below re-add only what is needed. */
  .table td::before { content: none; }

  /* -- band 1: when it was, and how it ended up ---------------------------- */
  .table td[data-label="Datum"] {
    order: 1;
    font-size: .8125rem;
    color: var(--muted);
  }
  .table td[data-label="Status"] {
    order: 2;
    margin-left: auto;            /* pins the badge to the right edge */
  }
  /* A "Bestand weg" or "Verplaatsen" status carries an explaining paragraph.
     That cannot sit on the date line, so a status cell WITH prose takes a band
     of its own. :has() is already used elsewhere in this file. */
  .table td[data-label="Status"]:has(p) {
    order: 2;
    flex: 1 1 100%;
    margin-left: 0;
  }

  /* -- band 2: the line you actually read ---------------------------------- */
  .table td[data-label="Handle / titel"] {
    order: 3;
    flex: 1 1 100%;
    font-size: 1.0625rem;
  }
  /* The admin rename field is hidden on a phone, and only here. It is a global
     edit of shared data, it is the single tallest thing in the card, and the
     same form sits on the recording's own page one tap away. The note field in
     the actions band deliberately STAYS: that one is personal, short, and the
     kind of thing you do jot down from a phone. display:none, not opacity, so
     it leaves the tab order with it. */
  .table td[data-label="Handle / titel"] form { display: none; }

  /* -- band 3: the numbers, one muted line --------------------------------- */
  .table td[data-label="Duur"],
  .table td[data-label="Grootte"],
  .table td[data-label="Chat"],
  .table td[data-label="Kijkers"] {
    order: 4;
    font-size: .8125rem;
    color: var(--muted);
  }
  /* A middot before every one but the first, so the line reads as one sentence
     rather than four stranded numbers. */
  .table td[data-label="Grootte"]::before,
  .table td[data-label="Chat"]::before,
  .table td[data-label="Kijkers"]::before {
    content: "·";
    margin-right: .4375rem;
    opacity: .55;
  }
  /* Duration and size announce themselves; a count does not. */
  .table td[data-label="Chat"]::after    { content: " chat"; }
  .table td[data-label="Kijkers"]::after { content: " kijkers"; }

  /* -- band 4: what you can do with it ------------------------------------- */
  .table td.actions,
  .table td.full {
    order: 5;
    flex: 1 1 100%;
    margin-top: .1875rem;
  }
  .table td.actions .btn-row { justify-content: flex-start; }
}

/* ==================================================================================
   13. The video player
   ================================================================================== */

/* The source is 1080x1920 - PORTRAIT. Left alone it is 1920 CSS pixels tall and
   the page becomes a scroll tunnel, so the height is capped first and the width
   follows from the aspect ratio. */
.player {
  display: flex;
  align-items: center;
  justify-content: center;
  margin: 0 0 1rem;
  padding: .5rem;
  background: #000;
  border: 1px solid var(--border-strong);
  border-radius: var(--r-lg);
  overflow: hidden;
}
.player video {
  display: block;
  width: auto;
  max-width: 100%;
  height: auto;
  max-height: 70vh;
  background: #000;
  border-radius: var(--r-sm);
}
.player-landscape video { width: 100%; max-height: 70vh; }

.player-meta {
  display: flex;
  flex-wrap: wrap;
  gap: .375rem .875rem;
  margin: -.5rem 0 1rem;
  color: var(--muted);
  font-size: .8125rem;
}

/* The report is shown in an iframe; frame-src 'self' allows exactly that. */
.report-frame {
  display: block;
  width: 100%;
  height: 78vh;
  min-height: 24rem;
  background: var(--surface-2);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-lg);
}

/* ==================================================================================
   14. State helpers driven by app.js
   ================================================================================== */

/* Progressive enhancement in the safe direction only: anything marked
   data-js-only is hidden until app.js has run, so it can appear but never
   disappear. <html> ships with class="no-js" and app.js swaps it for "js".

   WHY html:not(.js) instead of a pair of rules that hide and then unhide: the
   unhide would have to say display:revert, which rolls all the way back to the
   user-agent value and throws away whatever the element's own class asked for -
   a `<div class="search" data-js-only>` would come back as `block` instead of
   `flex`. With this selector there is simply no rule left to override. */
html:not(.js) [data-js-only] { display: none !important; }

/* The client-side filter box hides rows by adding this class. */
.is-filtered-out { display: none !important; }

.is-busy { opacity: .65; pointer-events: none; }

.is-saved { animation: tt-saved 1.1s ease-out; }
@keyframes tt-saved {
  0%   { box-shadow: 0 0 0 0 var(--saved-ring); }
  100% { box-shadow: 0 0 0 10px transparent; }
}

/* The stale indicator. The numbers stay on screen - freezing silently is the
   failure mode this is here to prevent - but they are visibly demoted. */
.status-stale {
  display: inline-flex;
  align-items: center;
  gap: .375rem;
  padding: .1875rem .5rem;
  border: 1px solid var(--warn-line);
  border-radius: var(--r-pill);
  background: var(--warn-bg);
  color: var(--warn);
  font-size: .8125rem;
  font-weight: 650;
  white-space: nowrap;
}
.is-status-stale [data-status-field],
.is-status-stale [data-status-badge] { opacity: .55; }

.spinner {
  width: 14px;
  height: 14px;
  border: 2px solid var(--border-strong);
  border-top-color: var(--accent);
  border-radius: 50%;
  animation: tt-spin .8s linear infinite;
}
@keyframes tt-spin { to { transform: rotate(360deg); } }

/* ==================================================================================
   15. Wider screens
   ================================================================================== */

@media (min-width: 560px) {
  .grid-3 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

@media (min-width: 760px) {
  .page { padding: 1.5rem 1.25rem 3rem; }
  .topbar-inner { padding: .5rem 1.25rem; }
  .footer-inner { padding: 1rem 1.25rem; }
  h1 { font-size: 1.625rem; }
  .card { padding: 1.25rem; }
  .grid-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .grid-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

@media (min-width: 1024px) {
  .grid-sidebar {
    display: grid;
    gap: 1.25rem;
    grid-template-columns: minmax(0, 2fr) minmax(18rem, 1fr);
  }
}

/* Anything that has to stay a 44px target even when it is a "small" button. */
@media (pointer: coarse) {
  .btn-sm { min-height: var(--tap); padding-left: .75rem; padding-right: .75rem; }
  .tab { min-height: var(--tap); }
}

/* ==================================================================================
   16. Motion, contrast, print
   ================================================================================== */

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: .001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .001ms !important;
    scroll-behavior: auto !important;
  }
  .btn:active { transform: none; }
}

/* Forced high contrast. This block sits after the palettes on purpose: it has the
   same specificity as a [data-theme] block and only wins by coming later. It has to
   name every theme one by one - a lone :root override would hand `licht` the pale rose
   --muted meant for an oxblood page and undo the whole point of the setting. */
@media (prefers-contrast: more) {
  :root,
  [data-theme="crimson"],
  [data-theme="systeem"] {
    --muted: #ecd9dd;          /* 9.90:1 on --surface-3 */
    --border: #9b2234;
    --border-strong: #c2616e;  /* 4.13:1 on --surface: the card edge, stated */
  }
  [data-theme="middernacht"] {
    --muted: #c3cad6;
    --border: #4a5265;
    --border-strong: #6b7488;
  }
  [data-theme="grafiet"] {
    --muted: #cfd3da;
    --border: #4a4e57;
    --border-strong: #767b85;
  }
  [data-theme="licht"] {
    --muted: #3d3238;
    --border: #b0a3a9;
    --border-strong: #86777e;
  }
}

/* The light half of `systeem` once more: the block above lists it with the dark
   palettes, so without this it would win here on source order. */
@media (prefers-contrast: more) and (prefers-color-scheme: light) {
  [data-theme="systeem"] {
    --muted: #3d3238;
    --border: #b0a3a9;
    --border-strong: #86777e;
  }
}

@media print {
  .topbar, .footer, .page-actions, .toolbar, .tabs, .btn, .flash-close {
    display: none !important;
  }
  body { background: #fff; color: #000; }
  .card, .table-wrap { border-color: #999; }
}

/* ==================================================================================
   17. The login page

   The only page with no navigation, no data and exactly one job. It borrows the
   whole vocabulary above and changes exactly one thing, keyed on
   <body data-page="login"> because base.html documents that attribute as the hook
   for page-specific CSS: the column is narrow. .page is 1180px wide everywhere
   else because it holds a table; here it holds two form fields, and a login box
   the width of a desk is neither calm nor obviously a login.

   The footer is NOT hidden here. login.html overrides base.html's {% block footer %}
   with an empty one instead, because a version string that is merely invisible is
   still in the HTML that anybody can read.

   Nothing else on the page is special: the card, the fields and the button are the
   same components every other page uses, and login.html adds no class of its own.
   ================================================================================== */

[data-page="login"] .page {
  max-width: 24rem;
  padding-top: 1.5rem;
}

@media (min-width: 760px) {
  [data-page="login"] .page { padding-top: 3.5rem; }
}


/* ==================================================================================
   19. The theme picker on the account page
   ==================================================================================
   The swatch is the whole trick here, and it is worth a paragraph. Each swatch span
   carries data-theme="<id>" ITSELF, which means the [data-theme] token blocks near
   the top of this file re-declare --bg, --surface and --accent for that one span and
   nothing else. So a swatch is painted by the very palette it advertises: three
   var() reads, no per-theme rule, and a sixth theme added to themes.py shows up here
   correctly without a line of CSS being written.

   The markup deliberately carries .checkbox AND .theme-option, in that order, so the
   picker is a usable 44px form row even with this whole section deleted. Everything
   below is the good-looking layer on top of that floor, so nothing here may be the
   only thing making the control operable.
   ================================================================================== */

.theme-form { display: grid; gap: 1rem; }

/* A fieldset has a border, padding and a min-width of its own that no other form
   control in this stylesheet has. Strip all three; the legend is visually hidden and
   the card already draws the box. */
.theme-fieldset {
  border: 0;
  margin: 0;
  padding: 0;
  min-width: 0;
}

.theme-grid {
  display: grid;
  gap: .5rem;
  grid-template-columns: 1fr;
}

@media (min-width: 640px) {
  .theme-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/* One object, repeated: same edges, same inner padding, the radio always first and
   the swatch always in the same place. Alignment is start, not center, because the
   description under the name makes these rows two lines tall on a narrow phone. */
.theme-option {
  align-items: flex-start;
  gap: .625rem;
  padding: .625rem .75rem;
  border: 1px solid var(--border-strong);
  border-radius: var(--r-md);
  background: var(--surface-2);
  cursor: pointer;
  transition: border-color .15s ease, background .15s ease;
}

.theme-option:hover { background: var(--surface-3); }

/* :has() carries the checked state to the whole row. Every browser that can run this
   app has supported it since 2023; where it is missing the row simply stays in its
   resting style and the radio itself still shows which one is picked, which is the
   accessible answer anyway. */
.theme-option:has(.theme-radio:checked) {
  border-color: var(--accent);
  background: var(--surface-3);
}

/* The focus ring goes on the ROW, not the radio: the radio is 18px in the corner of a
   two-line card, and a ring drawn around it is easy to miss when you are tabbing. */
.theme-option:has(.theme-radio:focus-visible) {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

.theme-radio { margin-top: .125rem; flex: none; }

/* -- the swatch ------------------------------------------------------------------ */

.theme-swatch {
  flex: none;
  width: 2.25rem;
  height: 2.25rem;
  border-radius: var(--r-sm);
  border: 1px solid var(--border-strong);
  background: var(--bg);              /* re-declared by this span's own data-theme */
  position: relative;
  overflow: hidden;
  display: block;
}

/* The bar is the top bar of the site and the dot is the accent: a recognisable
   miniature of the page rather than an abstract colour chip. */
.theme-swatch-bar {
  position: absolute;
  inset: 0 0 auto 0;
  height: .6875rem;
  background: var(--surface);
  border-bottom: 1px solid var(--border);
  display: block;
}

.theme-swatch-dot {
  position: absolute;
  left: .375rem;
  bottom: .375rem;
  width: .75rem;
  height: .75rem;
  border-radius: 50%;
  background: var(--accent);
  display: block;
}

/* "Volg mijn apparaat" is the one theme whose answer is not fixed: it resolves to
   dark or light depending on the reader's own device. Its swatch says exactly that -
   a diagonal split between the darkest and the lightest token in the palette - and
   drops the miniature top bar, because there is no one page colour to draw.

   --bg and --text, not two literals: the first version of this rule painted the light
   half as #f6f3f4 and was caught by
   test_no_rule_outside_a_palette_hard_codes_a_colour, which is precisely what that
   test exists for. In every dark palette --text is a near-white and --bg a
   near-black, so the split reads as "dark or light" while still being made of this
   theme's own colours. */
.theme-swatch[data-theme="systeem"] {
  background: linear-gradient(135deg,
              var(--bg) 0 48%, var(--border-strong) 48% 52%, var(--text) 52% 100%);
}

.theme-swatch[data-theme="systeem"] .theme-swatch-bar { display: none; }

/* With the bar gone the dot would float; centre it so the chip stays composed. */
.theme-swatch[data-theme="systeem"] .theme-swatch-dot {
  left: 50%;
  bottom: 50%;
  transform: translate(-50%, 50%);
}

/* -- the words ------------------------------------------------------------------- */

.theme-option-text { display: grid; gap: .0625rem; min-width: 0; }

.theme-option-name {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: .375rem;
  font-weight: 600;
  color: var(--text);
}

.theme-option-desc {
  font-size: .8125rem;
  line-height: 1.35;
  color: var(--muted);
}
