/* Relay browser UI — stylesheet.
 *
 * Extracted from index.html (stage 1 of splitting that file up): it was 925 of
 * its 2,564 lines. Served from /static/app.css; the markup keeps only the
 * before-paint theme script inline, which has to run before the first paint to
 * avoid a flash and so cannot move here.
 *
 * Section markers below are the original ones — they are also the seams the
 * stylesheet would split along if it ever needs to.
 */

*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
/* Form controls take `buttontext`/`fieldtext` from the UA, which `color-scheme`
   resolves to white on a dark theme and black on a light one — a colour owned by
   no theme. Harmless while every control that renders text sets its own colour
   (`.menu-btn` draws bars, not glyphs), but it leaks a foreign value into the
   page and it is what the per-theme colour audit trips over. `hr` is here for
   the same reason: it renders as a `border-top`, so its `color` is never
   painted, but the UA still hands it a grey belonging to no theme. The audit
   found it the moment a fixture first contained a horizontal rule. */
button, input, select, textarea, hr { color: inherit; }
button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
a:focus-visible      { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 2px; }

:root, [data-theme="dark"] {
  color-scheme: dark;
  --bg:          #090a0d;
  --surface:     #0f1116;
  --border:      #1d2230;
  --text:        #d8dde8;
  --muted:       #4a5568;
  --accent:      #f59e0b;
  --accent-dim:  #d97706;
  --accent-glow: rgba(245, 158, 11, 0.10);
  --tag-bg:      #141208;
  --tag-active:  #231a05;
  --green:       #10b981;
  --red:         #f87171;
  --body:        #b8bfcc;
  --body-strong: #c4cad8;
  --wash-raise:  rgba(255, 255, 255, 0.02);

  /* Semantic tokens. Components reference these, never a literal — a theme
     redefines this block and nothing else. Values derivable from --accent or
     --red are not tokens at all: they are color-mix() at the point of use, so
     they follow the accent into every theme for free. */
  /* Secondary and tertiary accents, for chips that are decorative rather than
     semantic — the id pill, the tag count, the uppercase section labels, the
     `via` source. Without them every such chip is grey in every theme, because
     the contract had one accent and nothing else.

     Violet and cyan against relay's amber: far enough round the wheel to read
     as separate information, and clear of --green and --red, which are
     semantic and must not be borrowed for decoration. They sit at the same
     lightness step as the tokens already here — the bright step on dark, the
     deep step on light — so they read as part of the same set rather than as
     stickers on it. */
  --accent-2:      #a78bfa;            /* violet */
  --accent-3:      #22d3ee;            /* cyan */
  --on-accent:     #000;               /* foreground on an --accent fill */
  --on-scrim:      #eee;               /* foreground on a dark scrim */
  --border-hover:  #2d3348;
  --scrim-rgb:     0, 0, 0;            /* channels, so call sites pick the alpha */
  --shadow-card:   0 4px 24px rgba(0, 0, 0, 0.35);
  --shadow-pill:   0 2px 10px rgba(0, 0, 0, 0.35);
  --shadow-modal:  0 24px 64px rgba(0, 0, 0, 0.6);
  --shadow-drawer: 8px 0 40px rgba(0, 0, 0, 0.6);
  font-family: 'IBM Plex Mono', monospace;
  font-size: 13px;
}

:root[data-theme="light"], [data-theme="light"] {
  color-scheme: light;
  --bg:          #edeae1;
  --surface:     #ffffff;
  --border:      #d2ccbb;
  --text:        #1c1e24;
  --muted:       #736e5d;
  --accent:      #b96809;
  --accent-dim:  #914f06;
  --accent-glow: rgba(185, 104, 9, 0.12);
  --tag-bg:      #f7ecd6;
  --tag-active:  #efe0c0;
  --green:       #047857;
  --red:         #c81e1e;
  --body:        #33373f;
  --body-strong: #22252c;
  --wash-raise:  rgba(0, 0, 0, 0.03);

  --accent-2:      #6d28d9;            /* violet, deep step to match this theme */
  --accent-3:      #0e7490;            /* cyan, deep step */
  --on-accent:     #000;               /* 5.05:1 on #b96809 — white would be 4.16:1 */
  --on-scrim:      #eee;
  --border-hover:  #b8b09a;
  --scrim-rgb:     0, 0, 0;
  --shadow-card:   0 4px 16px rgba(0, 0, 0, 0.10);
  --shadow-pill:   0 2px 10px rgba(0, 0, 0, 0.12);
  --shadow-modal:  0 24px 64px rgba(0, 0, 0, 0.18);
  --shadow-drawer: 8px 0 40px rgba(0, 0, 0, 0.16);
}

/* Gruvbox classic dark (morhetz). Not invented here — these are the values from
   the corellia desktop identity, and `relay_tui/palettes/gruvbox.toml` already
   anchors on the same three (#fabd2f, #504945, #1d2021), so the browser UI, the
   TUI, the terminal and the desktop agree.

   This is the theme that justified two of the semantic tokens. `--on-accent` is
   #1d2021 rather than black — gruvbox puts dark *brown* on its yellows, which is
   what the ricing does everywhere else — and `--border-hover` maps onto bg3, a
   colour the palette already defines for exactly that job. A theme that only
   ever restated #000 would not have proven either token was needed. */
:root[data-theme="gruvbox"], [data-theme="gruvbox"] {
  color-scheme: dark;
  --bg:          #1d2021;   /* bg0_h (hard) */
  --surface:     #282828;   /* bg0 */
  --border:      #504945;   /* bg2 */
  --text:        #fbf1c7;   /* fg0 — the brightest fg, not fg1 */
  --muted:       #928374;   /* gray */
  --accent:      #fabd2f;   /* yellow */
  --accent-dim:  #d79921;   /* neutral yellow */
  --accent-glow: rgba(250, 189, 47, 0.10);
  --tag-bg:      #32302f;   /* bg0_s — a warm step off the surface */
  --tag-active:  #3c3836;   /* bg1 */
  --green:       #b8bb26;
  --red:         #fb4934;
  --body:        #ebdbb2;   /* fg1 — fg2 left prose at 8.6:1 */
  --body-strong: #fbf1c7;   /* fg0 */
  --wash-raise:  rgba(235, 219, 178, 0.04);   /* lifts with fg, not pure white */

  --accent-2:      #d3869b;   /* purple */
  --accent-3:      #8ec07c;   /* aqua */
  --on-accent:     #1d2021;
  --on-scrim:      #ebdbb2;
  --border-hover:  #665c54;   /* bg3 */
  --scrim-rgb:     18, 20, 21;   /* a hair warmer than pure black, like the bg */
  --shadow-card:   0 4px 24px rgba(0, 0, 0, 0.40);
  --shadow-pill:   0 2px 10px rgba(0, 0, 0, 0.40);
  --shadow-modal:  0 24px 64px rgba(0, 0, 0, 0.62);
  --shadow-drawer: 8px 0 40px rgba(0, 0, 0, 0.62);
}

/* Tokyo Night (folke/tokyonight.nvim, "night" variant). Every value below is
   from the reference palette — the elevated surface is storm's `bg`, which is
   what the theme itself uses for floats and panels, and the two border steps
   are `fg_gutter` and `dark3` rather than anything mixed by hand.

   This is the first *cool* theme, and that was the point of adding it: the
   literal check cannot see a token used for the wrong role, only for the wrong
   value. Everything shipped before it was amber or yellow, so a warm assumption
   baked into a token's meaning would have gone unnoticed. Nothing outside this
   block changed to accommodate it.

   Two role corrections after seeing it rendered. `--body` is post prose — the
   main reading text — so it takes `fg`; `fg_dark` is the palette's *secondary*
   UI colour and using it here was a mis-mapping that cost 3.7 points of
   contrast. And the canvas drops to `bg_dark` so cards sit on the theme's
   signature `bg` rather than on storm's lighter panel tone. Together those took
   prose from 6.9:1 to 10.6:1 — see the contrast rule in tests/test_css_tokens.py.

   `--accent` was `blue` (#7aa2f7) — 89% saturation, but at 72% lightness, which
   reads as pastel next to this surface (6.8:1) no matter how saturated the hue
   is. `blue2` keeps the same 89% saturation at 45% lightness instead: still
   inside the reference palette, same family as `blue`/`blue0`, but it actually
   registers as accent rather than tint (7.3:1). `--accent-dim` follows it to
   `green2`, the palette's own hue-neighbour of `blue2` (188° vs 189°) rather
   than staying on `blue0`, whose 223° no longer sits next to the new accent. */
:root[data-theme="tokyo-night"], [data-theme="tokyo-night"] {
  color-scheme: dark;
  --bg:          #16161e;   /* bg_dark — the palette's darker canvas */
  --surface:     #1a1b26;   /* night bg — cards sit on the theme's signature tone */
  --border:      #3b4261;   /* fg_gutter */
  --text:        #c0caf5;   /* fg */
  --muted:       #565f89;   /* comment */
  --accent:      #0db9d7;   /* blue2 — see block note on why not the pastel `blue` */
  --accent-dim:  #41a6b5;   /* green2 — blue2's hue-neighbour, not blue0 */
  --accent-glow: rgba(13, 185, 215, 0.10);
  --tag-bg:      #24283b;   /* storm bg */
  --tag-active:  #414868;   /* terminal_black */
  --green:       #9ece6a;
  --red:         #f7768e;
  --body:        #c0caf5;   /* fg — see note: fg_dark is secondary UI, not prose */
  --body-strong: #c0caf5;   /* fg */
  --wash-raise:  rgba(192, 202, 245, 0.04);   /* lifts with fg, not pure white */

  --accent-2:      #bb9af7;   /* magenta */
  --accent-3:      #7dcfff;   /* cyan */
  --on-accent:     #1a1b26;   /* 7.3:1 on #0db9d7 */
  --on-scrim:      #c0caf5;
  --border-hover:  #545c7e;   /* dark3 */
  --scrim-rgb:     22, 22, 30;   /* bg_dark #16161e */
  --shadow-card:   0 4px 24px rgba(0, 0, 0, 0.42);
  --shadow-pill:   0 2px 10px rgba(0, 0, 0, 0.42);
  --shadow-modal:  0 24px 64px rgba(0, 0, 0, 0.64);
  --shadow-drawer: 8px 0 40px rgba(0, 0, 0, 0.64);
}

/* ANSI Dark / ANSI Light — the IBM VGA 16-colour set, which is what a terminal
   actually shows and where the vintage feel comes from. (The other common
   "VGA" table is Microsoft's #800000/#c0c0c0 variant; this is the #aa/#55 one.)
   Sourced from cli.r-lib.org's `vga` palette:

     black   #000000   br_black   #555555      blue    #0000aa   br_blue    #5555ff
     red     #aa0000   br_red     #ff5555      magenta #aa00aa   br_magenta #ff55ff
     green   #00aa00   br_green   #55ff55      cyan    #00aaaa   br_cyan    #55ffff
     yellow  #aa5500   br_yellow  #ffff55      white   #aaaaaa   br_white   #ffffff

   Two things follow from a 16-colour palette with nothing between #aaaaaa and
   #ffffff. **Cards are drawn, not filled**: `--surface` equals `--bg` and the
   card is defined by its border, which is how a terminal frames anything — the
   alternative was inventing an off-black, and no such colour exists here.
   **There is no text hierarchy by lightness**: prose and headings are both pure
   white (21:1), and emphasis comes from colour, again as it does in a terminal.
   #aaaaaa would have put prose at 9.0:1, under the floor.

   `--accent` is bright cyan rather than green: green is already `--green`, and
   an accent indistinguishable from the "connected" dot is a bad trade for
   phosphor nostalgia. The light theme takes blue so the pair reads as one
   family, the way Relay Dark and Relay Light share amber. */
:root[data-theme="ansi-dark"], [data-theme="ansi-dark"] {
  color-scheme: dark;
  --bg:          #000000;   /* black */
  --surface:     #000000;   /* black — cards are framed, not filled */
  --border:      #555555;   /* br_black */
  --text:        #ffffff;   /* br_white */
  --muted:       #aaaaaa;   /* white */
  --accent:      #55ffff;   /* br_cyan */
  --accent-dim:  #00aaaa;   /* cyan */
  --accent-glow: rgba(85, 255, 255, 0.10);
  --tag-bg:      #000000;   /* black — the pill is its accent-tinted outline */
  --tag-active:  #555555;   /* br_black */
  --green:       #55ff55;   /* br_green */
  --red:         #ff5555;   /* br_red */
  --body:        #ffffff;   /* br_white */
  --body-strong: #ffffff;   /* br_white */
  --wash-raise:  rgba(255, 255, 255, 0.06);

  --accent-2:      #ff55ff;   /* br_magenta */
  --accent-3:      #ffff55;   /* br_yellow */
  --on-accent:     #000000;
  --on-scrim:      #ffffff;
  --border-hover:  #aaaaaa;   /* white */
  --scrim-rgb:     0, 0, 0;
  --shadow-card:   0 4px 24px rgba(0, 0, 0, 0.6);
  --shadow-pill:   0 2px 10px rgba(0, 0, 0, 0.6);
  --shadow-modal:  0 24px 64px rgba(0, 0, 0, 0.8);
  --shadow-drawer: 8px 0 40px rgba(0, 0, 0, 0.8);
}

:root[data-theme="ansi-light"], [data-theme="ansi-light"] {
  color-scheme: light;
  --bg:          #ffffff;   /* br_white */
  --surface:     #ffffff;   /* br_white — cards are framed, not filled */
  --border:      #aaaaaa;   /* white */
  --text:        #000000;   /* black */
  --muted:       #555555;   /* br_black */
  --accent:      #0000aa;   /* blue */
  --accent-dim:  #5555ff;   /* br_blue — the palette has nothing darker, so the
                               hover lifts instead of deepening */
  --accent-glow: rgba(0, 0, 170, 0.10);
  --tag-bg:      #ffffff;   /* br_white */
  --tag-active:  #aaaaaa;   /* white */
  --green:       #00aa00;   /* green */
  --red:         #aa0000;   /* red */
  --body:        #000000;   /* black */
  --body-strong: #000000;   /* black */
  --wash-raise:  rgba(0, 0, 0, 0.06);

  --accent-2:      #aa00aa;   /* magenta */
  --accent-3:      #aa5500;   /* yellow/brown */
  --on-accent:     #ffffff;
  --on-scrim:      #ffffff;   /* scrims stay dark even under a light theme */
  --border-hover:  #555555;   /* br_black */
  --scrim-rgb:     0, 0, 0;
  --shadow-card:   0 4px 16px rgba(0, 0, 0, 0.14);
  --shadow-pill:   0 2px 10px rgba(0, 0, 0, 0.16);
  --shadow-modal:  0 24px 64px rgba(0, 0, 0, 0.22);
  --shadow-drawer: 8px 0 40px rgba(0, 0, 0, 0.20);
}

/* Catppuccin — four flavours, from catppuccin.com/palette. Role mapping follows
   the project's own guidance: `mantle` is the canvas and `base` the raised
   surface, `surface0/1/2` step up through borders, `overlay1` is muted.

   **The three dark flavours carry different accents on purpose.** Built to the
   letter they were near-indistinguishable: Catppuccin varies its backgrounds
   between flavours but barely its accents, so mauve everywhere gave #ca9ee6,
   #c6a0f6 and #cba6f7 — the same light purple three times — over backgrounds
   only 2.5x apart in luminance, which at that darkness reads as one theme. So
   Frappé takes **peach** (warm), Macchiato **blue** (cool), Mocha keeps the
   canonical **mauve**, and Mocha drops to `crust`/`mantle` so it is also
   unmistakably the deepest — widening the spread to 3.6x and *raising* its
   contrast from 11.3:1 to 12.1:1. Choosing an accent is what Catppuccin expects
   a port to do; mauve is its default, not a requirement.

   Frappé cannot be pushed the other way to widen the spread further: on
   `surface0` its text falls to 6.2:1, under the accessibility floor.

   **The decorative chips had the same problem one layer down.** `--accent-2`/
   `--accent-3` leaned on the same three hues as the accents themselves — peach
   appeared in all three flavours, mauve and teal in two — so a flavour's chips
   often just echoed a sibling's identity colour instead of having their own.
   Every dark flavour's full accent-eligible set (all fourteen members, `--red`/
   `--green` excluded as semantic) was ranked by contrast against its own
   surface: the lower the number, the less pastel it reads at that flavour's
   darkness. Frappé's own ceiling *is* peach/mauve/teal — its next-best options
   score worse — so only the mauve echo was worth removing there, to sapphire,
   at essentially no cost (5.60:1 → 6.25:1). Macchiato and Mocha both had real
   headroom: peach was replacing genuinely punchier maroon in both (7.62:1 →
   6.80:1; 9.92:1 → 8.49:1), and Mocha's teal gave way to sky for the same
   reason (11.79:1 → 11.29:1). Every replacement still clears the 4.5:1 chip
   floor with margin — the tightest is Frappé's new sapphire at 5.13:1.

   Latte is the exception, and the reason is worth recording because it is a
   fact about the palette rather than a choice. Measured as chip text on Latte's
   own base, **every one of its fourteen accent members lands under AA** — best
   is mauve at 4.18:1, and the ceiling with no tint at all is 4.80:1. Its warm
   members are far worse (peach 2.38:1, yellow 2.13:1) simply because orange and
   yellow on a near-white ground cannot do better. Dropping the chip tint to 4%
   would have rescued mauve at 4.54:1, but a 4% tint is not a chip — that trades
   the design in nine themes to accommodate one. So Latte's decorative chips are
   `text`: legible, and honestly uncoloured. Its colour shows in the accent,
   green and red instead.

   Its prose also tops out at 7.06:1 — exactly WCAG AAA, and the palette has no
   darker text than #4c4f69. See the two-tier floor in tests/test_css_tokens.py:
   a faithful reproduction is held to the accessibility floor, not to the house
   target relay's own themes are designed against. */

:root[data-theme="catppuccin-latte"], [data-theme="catppuccin-latte"] {
  color-scheme: light;
  --bg:          #e6e9ef;     /* mantle */
  --surface:     #eff1f5;     /* base */
  --border:      #ccd0da;     /* surface0 */
  --text:        #4c4f69;     /* text */
  --muted:       #8c8fa1;     /* overlay1 */
  --accent:      #8839ef;     /* mauve — Catppuccin's own default accent */
  --accent-dim:  #7287fd;     /* lavender */
  --accent-glow: rgba(136, 57, 239, 0.10);
  --tag-bg:      #ccd0da;     /* surface0 */
  --tag-active:  #bcc0cc;     /* surface1 */
  --green:       #40a02b;     /* green */
  --red:         #d20f39;     /* red */
  --body:        #4c4f69;     /* text */
  --body-strong: #4c4f69;     /* text */
  --wash-raise:  rgba(0, 0, 0, 0.05);

  --accent-2:    #4c4f69;     /* text — no member of Latte clears AA as a chip */
  --accent-3:    #4c4f69;     /* text — see the Latte note above */
  --on-accent:     #eff1f5;
  --on-scrim:      #4c4f69;
  --border-hover:  #acb0be;     /* surface2 */
  --scrim-rgb:     220, 224, 232;   /* crust */
  --shadow-card:   0 4px 24px rgba(0, 0, 0, 0.12);
  --shadow-pill:   0 2px 10px rgba(0, 0, 0, 0.14);
  --shadow-modal:  0 24px 64px rgba(0, 0, 0, 0.2);
  --shadow-drawer: 8px 0 40px rgba(0, 0, 0, 0.18);
}

:root[data-theme="catppuccin-frappe"], [data-theme="catppuccin-frappe"] {
  color-scheme: dark;
  --bg:          #292c3c;     /* mantle */
  --surface:     #303446;     /* base */
  --border:      #414559;     /* surface0 */
  --text:        #c6d0f5;     /* text */
  --muted:       #838ba7;     /* overlay1 */
  --accent:      #ef9f76;     /* peach — Frappé's signature here, see note */
  --accent-dim:  #e5c890;     /* yellow */
  --accent-glow: rgba(239, 159, 118, 0.10);
  --tag-bg:      #414559;     /* surface0 */
  --tag-active:  #51576d;     /* surface1 */
  --green:       #a6d189;     /* green */
  --red:         #e78284;     /* red */
  --body:        #c6d0f5;     /* text */
  --body-strong: #c6d0f5;     /* text */
  --wash-raise:  rgba(198, 208, 245, 0.05);

  --accent-2:    #85c1dc;     /* sapphire — was mauve, which just echoed Mocha's accent */
  --accent-3:    #81c8be;     /* teal */
  --on-accent:     #232634;
  --on-scrim:      #c6d0f5;
  --border-hover:  #626880;     /* surface2 */
  --scrim-rgb:     35, 38, 52;   /* crust */
  --shadow-card:   0 4px 24px rgba(0, 0, 0, 0.42);
  --shadow-pill:   0 2px 10px rgba(0, 0, 0, 0.42);
  --shadow-modal:  0 24px 64px rgba(0, 0, 0, 0.62);
  --shadow-drawer: 8px 0 40px rgba(0, 0, 0, 0.62);
}

:root[data-theme="catppuccin-macchiato"], [data-theme="catppuccin-macchiato"] {
  color-scheme: dark;
  --bg:          #1e2030;     /* mantle */
  --surface:     #24273a;     /* base */
  --border:      #363a4f;     /* surface0 */
  --text:        #cad3f5;     /* text */
  --muted:       #8087a2;     /* overlay1 */
  --accent:      #8aadf4;     /* blue — Macchiato's signature here */
  --accent-dim:  #7dc4e4;     /* sapphire */
  --accent-glow: rgba(138, 173, 244, 0.10);
  --tag-bg:      #363a4f;     /* surface0 */
  --tag-active:  #494d64;     /* surface1 */
  --green:       #a6da95;     /* green */
  --red:         #ed8796;     /* red */
  --body:        #cad3f5;     /* text */
  --body-strong: #cad3f5;     /* text */
  --wash-raise:  rgba(202, 211, 245, 0.05);

  --accent-2:    #ee99a0;     /* maroon — was peach, which just echoed Frappé's accent */
  --accent-3:    #c6a0f6;     /* mauve */
  --on-accent:     #181926;
  --on-scrim:      #cad3f5;
  --border-hover:  #5b6078;     /* surface2 */
  --scrim-rgb:     24, 25, 38;   /* crust */
  --shadow-card:   0 4px 24px rgba(0, 0, 0, 0.42);
  --shadow-pill:   0 2px 10px rgba(0, 0, 0, 0.42);
  --shadow-modal:  0 24px 64px rgba(0, 0, 0, 0.62);
  --shadow-drawer: 8px 0 40px rgba(0, 0, 0, 0.62);
}

:root[data-theme="catppuccin-mocha"], [data-theme="catppuccin-mocha"] {
  color-scheme: dark;
  --bg:          #11111b;     /* crust — Mocha takes the palette's darkest step */
  --surface:     #181825;     /* mantle */
  --border:      #313244;     /* surface0 */
  --text:        #cdd6f4;     /* text */
  --muted:       #7f849c;     /* overlay1 */
  --accent:      #cba6f7;     /* mauve — the canonical default, kept for Mocha */
  --accent-dim:  #b4befe;     /* lavender */
  --accent-glow: rgba(203, 166, 247, 0.10);
  --tag-bg:      #313244;     /* surface0 */
  --tag-active:  #45475a;     /* surface1 */
  --green:       #a6e3a1;     /* green */
  --red:         #f38ba8;     /* red */
  --body:        #cdd6f4;     /* text */
  --body-strong: #cdd6f4;     /* text */
  --wash-raise:  rgba(205, 214, 244, 0.05);

  --accent-2:    #eba0ac;     /* maroon — was peach, which just echoed Frappé's accent */
  --accent-3:    #89dceb;     /* sky — was teal, which just echoed Frappé's accent-3 */
  --on-accent:     #11111b;
  --on-scrim:      #cdd6f4;
  --border-hover:  #585b70;     /* surface2 */
  --scrim-rgb:     17, 17, 27;   /* crust */
  --shadow-card:   0 4px 24px rgba(0, 0, 0, 0.42);
  --shadow-pill:   0 2px 10px rgba(0, 0, 0, 0.42);
  --shadow-modal:  0 24px 64px rgba(0, 0, 0, 0.62);
  --shadow-drawer: 8px 0 40px rgba(0, 0, 0, 0.62);
}

/* Dracula.
   Dracula publishes four neutrals — Background, Current Line/Selection, Comment,
   Foreground — and no darker shade, so cards are framed rather than filled, the
   same answer the ANSI pair reached for the same reason. `Current Line` is
   Dracula's own divider colour, which is exactly what a card border is. */
:root[data-theme="dracula"], [data-theme="dracula"] {
  color-scheme: dark;
  --bg:          #282a36;
  --surface:     #282a36;
  --border:      #44475a;
  --text:        #f8f8f2;
  --muted:       #6272a4;
  --accent:      #bd93f9;
  --accent-dim:  #ff79c6;
  --accent-glow: rgba(189, 147, 249, 0.10);
  --tag-bg:      #44475a;
  --tag-active:  #6272a4;
  --green:       #50fa7b;
  --red:         #ff5555;
  --body:        #f8f8f2;
  --body-strong: #f8f8f2;
  --wash-raise:  rgba(248, 248, 242, 0.05);

  --accent-2:    #ffb86c;
  --accent-3:    #8be9fd;
  --on-accent:     #282a36;
  --on-scrim:      #f8f8f2;
  --border-hover:  #6272a4;
  --scrim-rgb:     25, 26, 33;
  --shadow-card:   0 4px 24px rgba(0, 0, 0, 0.42);
  --shadow-pill:   0 2px 10px rgba(0, 0, 0, 0.42);
  --shadow-modal:  0 24px 64px rgba(0, 0, 0, 0.62);
  --shadow-drawer: 8px 0 40px rgba(0, 0, 0, 0.62);
}

/* Everforest Dark.
   `--green` takes **aqua** rather than green: Everforest's green is its accent,
   and a connected-dot indistinguishable from the accent is the trade the ANSI
   themes already refused. Aqua still reads as green, so the signal survives.
   Cards are framed on bg0 rather than filled with bg1 — on bg1 its own
   foreground falls to 6.40:1, under the accessibility floor. */
:root[data-theme="everforest-dark"], [data-theme="everforest-dark"] {
  color-scheme: dark;
  --bg:          #2d353b;
  --surface:     #2d353b;
  --border:      #475258;
  --text:        #d3c6aa;
  --muted:       #859289;
  --accent:      #a7c080;
  --accent-dim:  #dbbc7f;
  --accent-glow: rgba(167, 192, 128, 0.10);
  --tag-bg:      #343f44;
  --tag-active:  #3d484d;
  --green:       #83c092;
  --red:         #e67e80;
  --body:        #d3c6aa;
  --body-strong: #d3c6aa;
  --wash-raise:  rgba(211, 198, 170, 0.05);

  --accent-2:    #d699b6;
  --accent-3:    #7fbbb3;
  --on-accent:     #2d353b;
  --on-scrim:      #d3c6aa;
  --border-hover:  #56635f;
  --scrim-rgb:     35, 42, 46;
  --shadow-card:   0 4px 24px rgba(0, 0, 0, 0.42);
  --shadow-pill:   0 2px 10px rgba(0, 0, 0, 0.42);
  --shadow-modal:  0 24px 64px rgba(0, 0, 0, 0.62);
  --shadow-drawer: 8px 0 40px rgba(0, 0, 0, 0.62);
}

/* Gruvbox Light.
   The mirror of the Gruvbox we already ship: same roles, the light half of the
   same palette, in the faded variants gruvbox uses on a light ground. The
   accent is **faded_orange**, not the yellow that mirrors Gruvbox Dark's:
   `--on-accent` on faded_yellow reaches only 3.91:1 whichever way it goes,
   because a mid-tone yellow suits neither dark nor light text. faded_blue
   replaces faded_aqua as the second chip for the same reason (3.89 -> 5.03). */
:root[data-theme="gruvbox-light"], [data-theme="gruvbox-light"] {
  color-scheme: light;
  --bg:          #f2e5bc;
  --surface:     #fbf1c7;
  --border:      #d5c4a1;
  --text:        #282828;
  --muted:       #7c6f64;
  --accent:      #af3a03;
  --accent-dim:  #b57614;
  --accent-glow: rgba(175, 58, 3, 0.10);
  --tag-bg:      #ebdbb2;
  --tag-active:  #d5c4a1;
  --green:       #79740e;
  --red:         #9d0006;
  --body:        #3c3836;
  --body-strong: #282828;
  --wash-raise:  rgba(0, 0, 0, 0.05);

  --accent-2:    #8f3f71;
  --accent-3:    #076678;
  --on-accent:     #fbf1c7;
  --on-scrim:      #282828;
  --border-hover:  #bdae93;
  --scrim-rgb:     40, 40, 40;
  --shadow-card:   0 4px 24px rgba(0, 0, 0, 0.12);
  --shadow-pill:   0 2px 10px rgba(0, 0, 0, 0.14);
  --shadow-modal:  0 24px 64px rgba(0, 0, 0, 0.2);
  --shadow-drawer: 8px 0 40px rgba(0, 0, 0, 0.18);
}

/* Molokai.
   The accent is **cyan**, not the green the TUI palette uses as its primary:
   Molokai has exactly one green and one red, and both are needed for `--green`
   and `--red`. Cyan is Molokai's Type colour and collides with nothing, so all
   six of its signature colours stay distinct on screen.

   Both `--accent` and `--accent-dim` shipped at the reference hex — #66d9ef and
   #ae81ff — which read as pastel for the same reason `blue` did in Tokyo Night:
   81%/100% saturation, but 67%/75% lightness, so the hue never gets to register
   before the colour reads as light. Same fix, same hue, lower lightness: cyan
   drops to 50% (7.4:1 against the surface, still 8.1:1 for `--on-accent` sitting
   on it) and purple to 71% — pushed less far, since it is a *button-hover fill*
   with `--on-accent` text on top, and violet's sRGB gamut is dark enough that
   71% is already the last stop with that text still clearing AA (4.85:1; 70%
   drops to 4.64, 65% to 3.71). */
:root[data-theme="molokai"], [data-theme="molokai"] {
  color-scheme: dark;
  --bg:          #1b1d1e;
  --surface:     #232526;
  --border:      #465457;
  --text:        #f8f8f2;
  --muted:       #7e8e91;
  --accent:      #18c4e7;   /* cyan, darkened — see block note */
  --accent-dim:  #9f6bff;   /* purple, darkened — see block note */
  --accent-glow: rgba(24, 196, 231, 0.10);
  --tag-bg:      #293739;
  --tag-active:  #403d3d;
  --green:       #a6e22e;
  --red:         #f92672;
  --body:        #f8f8f2;
  --body-strong: #f8f8f2;
  --wash-raise:  rgba(248, 248, 242, 0.05);

  --accent-2:    #fd971f;
  --accent-3:    #e6db74;
  --on-accent:     #1b1d1e;
  --on-scrim:      #f8f8f2;
  --border-hover:  #7e8e91;
  --scrim-rgb:     0, 0, 0;
  --shadow-card:   0 4px 24px rgba(0, 0, 0, 0.42);
  --shadow-pill:   0 2px 10px rgba(0, 0, 0, 0.42);
  --shadow-modal:  0 24px 64px rgba(0, 0, 0, 0.62);
  --shadow-drawer: 8px 0 40px rgba(0, 0, 0, 0.62);
}

/* Nord.
   `--muted` is **nord9**, not nord3. Nord's comment tone sits at 1.36:1 against
   nord1 — invisible rather than recessive — so the muted role moves up into the
   Frost range. `--text` takes nord6 rather than nord4 for the same reason: the
   contrast the palette can reach, not the first value that looks right. Cards
   are framed on nord0; nord1 was light enough to flatten every chip. And the
   decorative pair is nord13 + nord7, not the Aurora purple and orange that look
   the part — on nord0 those reach 3.79 and 3.81, under AA. */
:root[data-theme="nord"], [data-theme="nord"] {
  color-scheme: dark;
  --bg:          #2e3440;
  --surface:     #2e3440;
  --border:      #434c5e;
  --text:        #eceff4;
  --muted:       #81a1c1;
  --accent:      #88c0d0;
  --accent-dim:  #5e81ac;
  --accent-glow: rgba(136, 192, 208, 0.10);
  --tag-bg:      #3b4252;
  --tag-active:  #434c5e;
  --green:       #a3be8c;
  --red:         #bf616a;
  --body:        #e5e9f0;
  --body-strong: #eceff4;
  --wash-raise:  rgba(236, 239, 244, 0.05);

  --accent-2:    #ebcb8b;
  --accent-3:    #8fbcbb;
  --on-accent:     #2e3440;
  --on-scrim:      #eceff4;
  --border-hover:  #4c566a;
  --scrim-rgb:     36, 41, 51;
  --shadow-card:   0 4px 24px rgba(0, 0, 0, 0.42);
  --shadow-pill:   0 2px 10px rgba(0, 0, 0, 0.42);
  --shadow-modal:  0 24px 64px rgba(0, 0, 0, 0.62);
  --shadow-drawer: 8px 0 40px rgba(0, 0, 0, 0.62);
}

/* Solarized Dark / Solarized Light (Ethan Schoonover). Both draw only from the
   official 8-tone base scale (base03…base3) plus the 8 accent hues — nothing
   mixed in — but the scale is *reassigned*, not read off in its canonical
   roles.

   Solarized's own prose colour is base0 on dark / base00 on light, and both
   sit below this project's 7:1 accessibility floor (4.75:1 and 4.13:1 against
   the surfaces below) — that low contrast is the palette's whole design
   thesis, not an oversight, but it fails the floor here regardless. So `--body`
   and `--text` are promoted to the scale's brighter step — base2/base3 on
   dark, base02/base03 on light — the "background highlight" tones in the
   official spec, repurposed as foreground. Still nothing outside the 8 tones;
   it clears the house target outright (10.6:1 / 12.1:1 body), so unlike the
   Catppuccin flavours this needed no entry in REPRODUCTIONS.

   `--accent` is the signature blue. `--on-accent` is plain black rather than
   base03 — base03-on-blue is 4.08:1, under the floor; black reaches 5.71:1,
   which also holds for the light block below since the accent is unchanged.
   `--accent-2`/`--accent-3` fall back to `--text`, the same move Catppuccin
   Latte makes: every Solarized accent lands under AA as chip text on its own
   10% tint (orange, the best, reaches 3.75), so two illegible colourful chips
   are not worth shipping over one legible flat one. */
:root[data-theme="solarized-dark"], [data-theme="solarized-dark"] {
  color-scheme: dark;
  --bg:          #002b36;   /* base03 */
  --surface:     #073642;   /* base02 — background highlights, raised over base03 */
  --border:      #586e75;   /* base01 — "comments", the palette's only mid-grey */
  --text:        #fdf6e3;   /* base3 — promoted; see block note */
  --muted:       #657b83;   /* base00 */
  --accent:      #268bd2;   /* blue */
  --accent-dim:  #1b6497;   /* blue, darkened — Solarized has one step per hue */
  --accent-glow: rgba(38, 139, 210, 0.10);
  --tag-bg:      #093d4e;
  --tag-active:  #0d455c;
  --green:       #859900;
  --red:         #dc322f;
  --body:        #eee8d5;   /* base2 — promoted; see block note */
  --body-strong: #fdf6e3;   /* base3 */
  --wash-raise:  rgba(253, 246, 227, 0.03);

  --accent-2:      #fdf6e3;   /* text — no Solarized accent clears AA as a chip */
  --accent-3:      #fdf6e3;   /* text — see block note */
  --on-accent:     #000;      /* 5.71:1 on #268bd2 — base03 only reaches 4.08:1 */
  --on-scrim:      #fdf6e3;
  --border-hover:  #819090;
  --scrim-rgb:     0, 43, 54;   /* base03 */
  --shadow-card:   0 4px 24px rgba(0, 0, 0, 0.40);
  --shadow-pill:   0 2px 10px rgba(0, 0, 0, 0.40);
  --shadow-modal:  0 24px 64px rgba(0, 0, 0, 0.62);
  --shadow-drawer: 8px 0 40px rgba(0, 0, 0, 0.62);
}

:root[data-theme="solarized-light"], [data-theme="solarized-light"] {
  color-scheme: light;
  --bg:          #eee8d5;   /* base2 */
  --surface:     #fdf6e3;   /* base3 — the lightest tone, cards at the ceiling */
  --border:      #586e75;   /* base01 — same value as Dark; Solarized reuses it as-is */
  --text:        #002b36;   /* base03 — promoted; see block note */
  --muted:       #657b83;   /* base00 — the palette's own "body text on light" role */
  --accent:      #268bd2;   /* blue */
  --accent-dim:  #1b6497;   /* blue, darkened */
  --accent-glow: rgba(38, 139, 210, 0.10);
  --tag-bg:      #ecede2;
  --tag-active:  #d6e3e0;
  --green:       #859900;
  --red:         #dc322f;
  --body:        #073642;   /* base02 — promoted; see block note */
  --body-strong: #002b36;   /* base03 */
  --wash-raise:  rgba(0, 43, 54, 0.03);

  --accent-2:      #002b36;   /* text — no Solarized accent clears AA as a chip */
  --accent-3:      #002b36;   /* text — see block note */
  --on-accent:     #000;      /* 5.71:1 on #268bd2, same accent as Dark */
  --on-scrim:      #fdf6e3;
  --border-hover:  #466168;
  --scrim-rgb:     0, 43, 54;   /* base03, matching --text like Gruvbox Light does */
  --shadow-card:   0 4px 16px rgba(0, 0, 0, 0.12);
  --shadow-pill:   0 2px 10px rgba(0, 0, 0, 0.14);
  --shadow-modal:  0 24px 64px rgba(0, 0, 0, 0.18);
  --shadow-drawer: 8px 0 40px rgba(0, 0, 0, 0.16);
}

html { color: var(--text); }   /* see note in tests/test_css_tokens.py */

body {
  background: var(--bg); color: var(--text);
  height: 100vh; display: flex; flex-direction: column; overflow: hidden;
}

/* ── Header ─────────────────────────────────────────── */
header {
  display: flex; align-items: center; gap: 14px;
  padding: 0 20px; height: 48px;
  border-bottom: 1px solid var(--border);
  background: var(--surface); flex-shrink: 0;
}
.brand-mark { height: 20px; width: auto; display: block; flex-shrink: 0; }
.brand-name {
  font-size: 11px; font-weight: 600; color: var(--accent);
  letter-spacing: 0.3em; text-transform: uppercase;
}
.brand-sep { color: var(--border); font-weight: 300; font-size: 18px; line-height: 1; }

.live-indicator { display: flex; align-items: center; gap: 5px; flex-shrink: 0; }
.live-dot {
  width: 6px; height: 6px; border-radius: 50%;
  background: var(--muted); flex-shrink: 0; transition: background 0.3s, box-shadow 0.3s;
}
.live-dot.connected { background: var(--green); box-shadow: 0 0 6px var(--green); }
.live-dot.error     { background: var(--red); }
.live-label {
  font-size: 10px; color: var(--muted); letter-spacing: 0.1em;
  text-transform: uppercase; transition: color 0.3s;
}
.live-dot.connected ~ .live-label { color: var(--green); }
.live-dot.error     ~ .live-label { color: var(--red); }

.header-right { display: flex; align-items: center; gap: 10px; margin-left: auto; }

/* One shape for every icon control in the header. They used to be sized by
   their contents — a text "i" and an emoji ☀ in the same padded box render at
   different optical weights and widths, so the row read as three unrelated
   controls. A fixed square with a drawn 15px icon makes them a set. */
.icon-btn {
  width: 28px; height: 28px; flex-shrink: 0; padding: 0;
  display: inline-flex; align-items: center; justify-content: center;
  background: none; border: 1px solid var(--border); border-radius: 4px;
  color: var(--muted); cursor: pointer;
  transition: color 0.15s, border-color 0.15s, background 0.15s;
}
.icon-btn svg { display: block; }
.icon-btn:hover { color: var(--accent); border-color: var(--accent); }
.icon-btn:focus-visible { color: var(--accent); border-color: var(--accent); }
/* Open state, not hover: the picker stays lit while its menu is up. */
.icon-btn[aria-expanded="true"] {
  color: var(--accent); border-color: var(--accent); background: var(--wash-raise);
}
/* Ending the session is the one header action you cannot undo with another
   click, so it answers in red rather than accent — the same language .btn-delete
   already speaks. */
.icon-btn-danger:hover, .icon-btn-danger:focus-visible {
  color: var(--red); border-color: var(--red);
}

/* ── Theme picker ────────────────────────────────────────── */
.theme-picker { position: relative; display: flex; }
.theme-menu {
  display: none; position: absolute; top: calc(100% + 6px); right: 0; z-index: 60;
  /* Wide enough that the longest label — "Catppuccin Macchiato" — sits on one
     line. It wrapped to two at 168px, which made the row taller than its
     neighbours and the list read as broken rather than long. */
  min-width: 236px; padding: 4px;
  /* Fifteen themes is taller than a short window. Scroll the menu rather than
     letting it run off the bottom of the screen. */
  max-height: min(70vh, 560px); overflow-y: auto; overscroll-behavior: contain;
  background: var(--surface); border: 1px solid var(--border); border-radius: 6px;
  box-shadow: var(--shadow-modal);
  animation: modalIn 0.14s ease backwards;
}
.theme-menu.open { display: block; }
.theme-opt {
  display: flex; align-items: center; gap: 9px; width: 100%;
  background: none; border: none; border-radius: 4px; cursor: pointer;
  padding: 7px 9px; font-family: inherit; font-size: 12px; color: var(--text);
  text-align: left; transition: background 0.12s;
}
.theme-opt:hover { background: var(--wash-raise); }
.theme-opt.active { color: var(--accent); }
.theme-sep {
  height: 1px; margin: 4px 9px; background: var(--border);
}
/* Family group label above each cluster of related themes in the picker. */
.theme-group-label {
  padding: 6px 9px 2px;
  font-size: 10px; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--muted); pointer-events: none;
}
/* The swatch carries its own `data-theme`, so the theme's token block paints it
   — a live sample rather than a hex value copied into a second place. */
.theme-swatch {
  width: 15px; height: 15px; border-radius: 4px; flex-shrink: 0;
  background: var(--bg); border: 2px solid var(--accent);
}
.theme-label {
  flex: 1; min-width: 0;
  /* Never wrap: a two-line row in a list of one-line rows looks like a bug. */
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* The tick is its own element, not `::after` on the label — appended to the
   text it joined the wrap and landed on a line of its own. Reserving the column
   in every row also stops the labels shifting as the selection moves. */
.theme-check {
  flex-shrink: 0; width: 12px; text-align: center;
  color: var(--accent); visibility: hidden;
}
.theme-opt.active .theme-check { visibility: visible; }

@media (max-width: 768px) {
  /* Same reasoning as the sheets' 44px floor, capped by a 48px header bar.
     `.btn-new-post` cannot be sized here — its base rule is declared further
     down the file and would win on source order. It is set in the mobile header
     block instead. */
  .icon-btn { width: 40px; height: 40px; }

  /* The sidebar is a drawer on a phone, so its controls are thumb targets like
     any other. Measured before this: the tab strip 41x22, the tag row's rename
     and expiry icons 17x17, the add button 22x18 — all well under the floor the
     sheets and the header already hold. The tag row itself is 29px of text; the
     padding lifts the whole row rather than just the icons, so the tap area
     matches what looks tappable. */
  .sb-tab { min-width: 44px; min-height: 44px; display: inline-flex;
            align-items: center; justify-content: center; }
  .sidebar-add-btn { min-width: 44px; min-height: 44px; }
  .tag-item { min-height: 44px; }
  .tag-rename, .tag-config-btn {
    min-width: 44px; min-height: 44px;
    display: inline-flex; align-items: center; justify-content: center;
    margin: -12px -10px -12px 0;   /* grow the hit area, not the row */
  }
  .theme-opt { min-height: 44px; }
  .theme-menu { min-width: 190px; }
}

/* Visually hidden but available to screen readers. */
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0;
}

/* Sidebar collapse toggle (desktop) */
.collapse-btn {
  background: none; border: none; color: var(--muted);
  cursor: pointer; padding: 4px; margin-left: -2px;
  display: flex; align-items: center; flex-shrink: 0;
  transition: color 0.15s;
}
.collapse-btn:hover { color: var(--accent); }
.collapse-btn svg { display: block; transition: transform 0.24s cubic-bezier(0.4,0,0.2,1); }
.collapse-btn.collapsed svg { transform: rotate(180deg); }

.key-wrap { display: flex; align-items: center; gap: 6px; }
.key-wrap input {
  background: var(--bg); border: 1px solid var(--border); color: var(--text);
  padding: 5px 10px; border-radius: 4px; font-size: 12px; width: 220px;
  font-family: inherit; letter-spacing: 0.04em; transition: border-color 0.15s;
}
.key-wrap input:focus  { outline: none; border-color: var(--accent); }
.key-wrap input::placeholder { color: var(--muted); }

.btn-connect {
  background: var(--accent); border: none; color: var(--on-accent);
  padding: 5px 12px; border-radius: 4px; cursor: pointer;
  font-family: inherit; font-size: 11px; font-weight: 600;
  letter-spacing: 0.1em; text-transform: uppercase;
  transition: background 0.15s;
}
.btn-connect:hover { background: var(--accent-dim); }

.oidc-login { display: flex; align-items: center; gap: 10px; }
.link-btn {
  background: none; border: none; color: var(--muted); cursor: pointer;
  font-family: inherit; font-size: 11px; letter-spacing: 0.04em;
  padding: 3px 0; border-bottom: 1px solid transparent;
  transition: color 0.15s, border-color 0.15s;
}
.link-btn:hover { color: var(--text); border-color: var(--border); }

.connected-bar { display: flex; align-items: center; }

.btn-new-post {
  height: 28px; display: inline-flex; align-items: center; flex-shrink: 0;
  background: var(--accent); border: none; color: var(--on-accent);
  padding: 0 12px; border-radius: 4px; cursor: pointer;
  font-family: inherit; font-size: 11px; font-weight: 600;
  letter-spacing: 0.1em; text-transform: uppercase;
  transition: background 0.15s;
}
.btn-new-post:hover { background: var(--accent-dim); }

/* ── Login view (logged-out) ─────────────────────────── */
.login-view { flex: 1; display: flex; align-items: center; justify-content: center; padding: 24px; }
.login-card {
  display: flex; flex-direction: column; align-items: center; gap: 12px;
  text-align: center; width: 100%; max-width: 300px;
}
.login-mark { height: 56px; width: auto; margin-bottom: 2px; }
.login-title { font-size: 20px; font-weight: 700; letter-spacing: 0.14em; color: var(--accent); margin: 0; }
.login-tag { font-size: 12px; color: var(--muted); margin: 0 0 10px; }
.login-card .oidc-login { flex-direction: column; gap: 12px; width: 100%; }
.btn-login-primary {
  width: 100%; background: var(--accent); border: none; color: var(--on-accent);
  padding: 12px 16px; border-radius: 6px; cursor: pointer;
  font-family: inherit; font-size: 13px; font-weight: 700;
  letter-spacing: 0.08em; text-transform: uppercase; transition: background 0.15s;
}
.btn-login-primary:hover { background: var(--accent-dim); }
.login-card .key-wrap { flex-direction: column; gap: 10px; width: 100%; }
.login-card .key-wrap input[type=password] { width: 100%; padding: 11px 12px; font-size: 13px; }
.login-card .btn-connect { width: 100%; padding: 11px 16px; font-size: 12px; }

/* ── Layout ──────────────────────────────────────────── */
.layout { display: flex; flex: 1; overflow: hidden; }

/* ── Sidebar ─────────────────────────────────────────── */
aside {
  width: 220px; flex-shrink: 0;
  border-right: 1px solid var(--border);
  display: flex; flex-direction: column; overflow: hidden;
  background: var(--bg);
  transition: width 0.24s cubic-bezier(0.4,0,0.2,1),
              border-right-color 0.24s ease,
              opacity 0.18s ease;
}
/* Desktop collapse — width animates to 0, content clipped by overflow:hidden */
aside.collapsed {
  width: 0; opacity: 0; border-right-color: transparent;
  pointer-events: none;
}
aside.collapsed > * { min-width: 220px; }
.sidebar-header {
  padding: 14px 16px 10px;
  font-size: 9px; font-weight: 600; color: var(--muted);
  text-transform: uppercase; letter-spacing: 0.18em;
  display: flex; align-items: center; justify-content: space-between;
}
/* Three tabs and the `+` button fit a 220px sidebar with room. A fourth did
   not — `tests/ui/test_sidebar_tabs.py` measures the strip against the header's
   content box, so the next attempt to add one fails loudly instead of pushing
   the add button off the edge. */
.sidebar-tabs { display: flex; gap: 3px; min-width: 0; }
.sb-tab {
  background: none; border: none; color: var(--muted); cursor: pointer;
  font: inherit; font-size: 9px; letter-spacing: 0.14em; text-transform: uppercase;
  padding: 3px 8px; border-radius: 4px; white-space: nowrap;
}
.sb-tab.active { color: var(--accent); background: var(--tag-active); }
.sb-tab:hover:not(.active) { color: var(--text); }
/* A fixed gutter, so the "all" row (which carries an empty one) lines its label
   up with the folder rows instead of hanging left of them. The icon is drawn
   (`ICON_FOLDER`) and strokes in `currentColor`, so `--accent` reaches it —
   which the U+1F4C1 emoji that shipped here first could never do. */
.folder-item .folder-ico {
  display: inline-flex; align-items: center; justify-content: flex-start;
  width: 1.25em; margin-right: 4px; color: var(--accent);
}
.folder-item .folder-ico svg { display: block; }
.sidebar-add-btn {
  background: none; border: 1px solid var(--border); color: var(--muted);
  cursor: pointer; font-size: 14px; line-height: 1; padding: 1px 6px;
  border-radius: 3px; font-family: inherit;
  transition: border-color 0.15s, color 0.15s;
}
.sidebar-add-btn:hover { border-color: var(--accent); color: var(--accent); }

.tag-new { padding: 0 12px 10px; }
.tag-new-input {
  background: var(--surface); border: 1px solid var(--accent); color: var(--text);
  border-radius: 3px; font-size: 12px; padding: 4px 8px; width: 100%;
  outline: none; font-family: inherit;
}

.tag-list { overflow-y: auto; flex: 1; padding-bottom: 8px; }
.tag-list::-webkit-scrollbar { width: 3px; }
.tag-list::-webkit-scrollbar-track { background: transparent; }
.tag-list::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }

.tag-item {
  display: flex; align-items: center; gap: 8px;
  padding: 7px 16px; cursor: pointer;
  border-left: 2px solid transparent;
  transition: background 0.12s;
}
.tag-item:hover  { background: var(--wash-raise); }
.tag-item.active { background: var(--tag-active); border-left-color: var(--accent); }

.tag-name  { font-size: 12px; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tag-count {
  font-size: 9px; color: var(--accent-3);
  background: color-mix(in srgb, var(--accent-3) 10%, transparent);
  padding: 1px 5px; border-radius: 8px;
  flex-shrink: 0; font-variant-numeric: tabular-nums;
  transition: background 0.12s, color 0.12s;
}
.tag-item.active .tag-count { background: var(--accent); color: var(--on-accent); }

/* Both row controls share a shape: a comfortable hit area around a 13px icon,
   revealed on hover. They carry SVGs now, so size comes from the icon rather
   than a font's dingbat metrics. */
.tag-rename, .tag-config-btn {
  background: none; border: none; cursor: pointer;
  padding: 2px; opacity: 0; flex-shrink: 0; line-height: 0;
  color: var(--muted); border-radius: 3px;
  display: inline-flex; align-items: center; justify-content: center;
  transition: opacity 0.12s, color 0.12s;
}
.tag-item:hover .tag-rename,
.tag-item:hover .tag-config-btn { opacity: 0.75; }
.tag-rename:hover, .tag-config-btn:hover { opacity: 1 !important; color: var(--accent); }
.tag-rename:focus-visible, .tag-config-btn:focus-visible { opacity: 1; color: var(--accent); }
/* Touch devices have no hover, so the controls would never appear at all. */
@media (hover: none) {
  .tag-rename, .tag-config-btn { opacity: 0.55; }
}

.tag-rename-input {
  background: var(--bg); border: 1px solid var(--accent); color: var(--text);
  border-radius: 3px; font-size: 12px; padding: 1px 5px;
  width: 100%; outline: none; font-family: inherit;
}

/* ── Feed ────────────────────────────────────────────── */
main { flex: 1; display: flex; flex-direction: column; overflow: hidden; }

.compose-panel {
  overflow: hidden; max-height: 0; opacity: 0;
  transition: max-height 0.28s ease, opacity 0.2s ease, padding 0.28s ease;
  padding: 0 20px;
}
.compose-panel.open { max-height: 660px; opacity: 1; padding: 16px 20px 0; }
.compose-panel .post { border-style: dashed; border-color: color-mix(in srgb, var(--accent) 40%, transparent); }

.feed {
  flex: 1; overflow-y: auto; overflow-x: hidden;
  padding: 16px 20px; display: flex; flex-direction: column; gap: 10px;
}
.feed::-webkit-scrollbar { width: 4px; }
.feed::-webkit-scrollbar-track { background: transparent; }
.feed::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }

/* Grid view: cards flow into a responsive column grid instead of a stack. */
.feed.grid {
  display: grid; align-content: start;
  grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
  gap: 12px;
}
.feed.grid > .post:not(.editing) { align-self: start; }
.feed.grid > .post.pinned,
.feed.grid > .empty,
.feed.grid > .auth-prompt { grid-column: 1 / -1; }   /* master doc / states span full width */

/* ── Post card ───────────────────────────────────────── */
.post {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: 6px; padding: 16px; min-width: 0;
  transition: border-color 0.2s, box-shadow 0.2s;
  animation: cardIn 0.3s ease both;
}
@keyframes cardIn {
  from { opacity: 0; transform: translateY(6px); }
  to   { opacity: 1; transform: translateY(0); }
}
.post:hover { border-color: var(--border-hover); box-shadow: var(--shadow-card); }
.post.card-focused { border-color: var(--accent); box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 20%, transparent); outline: none; }
.post.new { animation: cardIn 0.3s ease, flash 1.4s ease; }
@keyframes flash {
  0%   { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent), 0 0 24px var(--accent-glow); }
  100% { border-color: var(--border); box-shadow: none; }
}

/* Feed cards lay their parts out on a grid so the media thumbnail can sit
   beside the text in list mode and jump on top as a hero in grid mode —
   one markup, pure-CSS switch. Editing cards fall back to block flow
   (their children become form fields with no grid areas). */
.feed > .post:not(.editing) {
  display: grid; column-gap: 14px; align-items: start;
  grid-template-columns: 1fr auto;
  grid-template-areas: "badge  badge" "header header" "body media" "footer footer";
}
/* Every area needs min-width:0: a `1fr` track's automatic minimum is the
   item's min-content width, so one non-shrinkable child (a nowrap source,
   a nowrap table) would otherwise widen the track past the card border and
   everything inside it would paint outside the frame. */
.feed > .post:not(.editing) > * { min-width: 0; }
.feed > .post .master-badge   { grid-area: badge; }
.feed > .post .post-header    { grid-area: header; }
.feed > .post .post-body-wrap { grid-area: body; }
.feed > .post .post-media     { grid-area: media; }
.feed > .post .post-footer    { grid-area: footer; }
.feed.grid > .post:not(.editing) {
  grid-template-columns: 1fr;
  grid-template-areas: "media" "badge" "header" "body" "footer";
}
/* Uniform tile height so rows never go ragged (CSS grid sizes a row to its
   tallest cell — one hero-image tile would otherwise blow out the whole row).
   The body row flex-fills and clips with the fade; the footer pins to the
   bottom. Image tiles show a short excerpt, text-only tiles use the freed
   space for more text. The full-width master banner keeps its own height. */
.feed.grid > .post:not(.editing):not(.pinned) {
  height: 340px;
  grid-template-rows: auto auto auto minmax(0, 1fr) auto;
}
.feed.grid > .post:not(.pinned) .post-body-wrap {
  max-height: none; height: 100%; min-height: 0;
}

.post-media { align-self: start; line-height: 0; }
.post-media img {
  width: 108px; height: 78px; object-fit: cover; display: block;
  border-radius: 5px; border: 1px solid var(--border);
}
.feed.grid .post-media { margin-bottom: 10px; }
.feed.grid .post-media img { width: 100%; height: 100px; }
.post-media-count { font-size: 10px; color: var(--muted); }

/* Denser previews in feed cards: tighter leading + no leading margin fit
   ~a full extra line inside the same clamped height. The modal (.pm-body)
   keeps the airy 1.72 for comfortable reading. */
.feed .post-body { line-height: 1.55; }
.feed .post-body > :first-child { margin-top: 0; }

/* Narrow grid tiles: stack the header (title on its own line, tags below) so
   tags don't crush the title into a hard ellipsis, and let long titles wrap
   to two lines. The footer meta wraps instead of overflowing. */
/* `stretch` (not flex-start) so the tag row inherits the header's definite
   width — that's what makes the source's max-width:100% ellipsis resolve. */
.feed.grid .post-header { flex-direction: column; align-items: stretch; gap: 6px; }
.feed.grid .post-title {
  white-space: normal; overflow: hidden; text-overflow: clip;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
}
.feed.grid .post-footer-left { flex-wrap: wrap; row-gap: 3px; }
/* A tile footer can't fit pill + both stamps + two labelled buttons on one
   line, so grid mode drops the two most redundant parts: the created stamp
   when an edit stamp is already there (same information, usually the same
   value) and the button captions, leaving the icons. List mode keeps both. */
.feed.grid .t-created:not(:only-child) { display: none; }
.feed:not(.grid) .t-created + .t-edited::before { content: " · "; }
.feed.grid .btn-label { display: none; }
.feed.grid .post-actions button { padding-left: 7px; padding-right: 7px; }
/* The note's own "Last updated:" line is already in the body, which the
   detail modal shows in full — in a tile it only buys a second footer row. */
.feed.grid .t-doc-updated { display: none; }
.feed:not(.grid) .t-doc-updated::before { content: "· "; }

.post-header { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 8px; margin-bottom: 10px; }
.post-title  { font-size: 13px; font-weight: 600; color: var(--text); flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

.post-tags { display: flex; gap: 5px; flex-wrap: wrap; align-items: center; flex-shrink: 0; min-width: 0; max-width: 100%; }
.tag-pill {
  font-size: 10px; padding: 2px 7px; border-radius: 3px;
  background: var(--tag-bg); color: var(--accent); cursor: pointer;
  border: 1px solid color-mix(in srgb, var(--accent) 20%, transparent); letter-spacing: 0.02em;
  transition: background 0.12s;
}
.tag-pill:hover { background: var(--tag-active); }
/* A long "via …" is one unbreakable run: cap + ellipsis it so it can never
   push the header (and with it the card's 1fr track) wider than the card. */
.post-source {
  font-size: 10px; color: var(--accent-2);
  display: inline-block; min-width: 0; max-width: 100%; vertical-align: bottom;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

.post-body { color: var(--body); line-height: 1.72; font-size: 13px; overflow-wrap: anywhere; }
.post-body h1, .post-body h2, .post-body h3 { color: var(--text); margin: 16px 0 6px; font-weight: 700; font-family: system-ui, -apple-system, sans-serif; }
.post-body h1 { font-size: 16px; }
.post-body h2 { font-size: 14px; }
.post-body h3 { font-size: 13px; color: var(--body-strong); }
.post-body hr { border: none; border-top: 1px solid var(--border); margin: 14px 0; }
.post-body p         { margin: 6px 0; }
.post-body ul, .post-body ol { padding-left: 18px; margin: 6px 0; }
.post-body li        { margin: 3px 0; }
.post-body a         { color: var(--accent); text-decoration: underline; text-decoration-style: dotted; text-underline-offset: 2px; }
.post-body a:hover   { text-decoration-style: solid; }
.post-body a.wikilink { color: var(--accent); text-decoration: none; border-bottom: 1px solid color-mix(in srgb, var(--accent) 40%, transparent); cursor: pointer; }
.post-body a.wikilink:hover { border-bottom-color: var(--accent); }
.post-body .wikilink.broken { color: var(--muted); text-decoration: none; border-bottom: 1px dashed var(--muted); cursor: help; }
.post-body img.attachment { max-width: 100%; height: auto; border-radius: 6px; display: block; margin: 0.5rem 0; }
.post-body a.attachment-link { color: var(--accent); text-decoration: none; border-bottom: 1px solid color-mix(in srgb, var(--accent) 40%, transparent); }
.post-body a.attachment-link::before { content: "📎 "; }
.post-body a.attachment-link.broken { color: var(--muted); border-bottom-color: var(--muted); }
.pm-backlinks { margin: 18px 0 4px; padding-top: 12px; border-top: 1px solid var(--border); }
.pm-backlinks h4 { color: var(--accent-3); font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: .04em; margin: 0 0 8px; }
.pm-backlinks ul { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 4px; }
.pm-backlinks a { color: var(--accent); cursor: pointer; font-size: 12px; }
.pm-backlinks .bl-id { color: var(--muted); margin-right: 6px; }
.post-body code      { background: var(--bg); padding: 1px 5px; border-radius: 3px; font-size: 11px; border: 1px solid var(--border); }
.post-body pre       { position: relative; background: var(--bg); padding: 10px 12px; border-radius: 4px; overflow-x: auto; margin: 8px 0; border: 1px solid var(--border); }
.post-body pre code  { background: none; padding: 0; border: none; font-size: 11px; }
.code-copy {
  position: absolute; top: 6px; right: 6px;
  background: var(--surface); border: 1px solid var(--border); border-radius: 3px;
  color: var(--muted); font-family: inherit; font-size: 10px; font-weight: 600;
  letter-spacing: 0.05em; text-transform: uppercase; padding: 2px 7px;
  cursor: pointer; opacity: 0; transition: opacity 0.15s, color 0.15s, border-color 0.15s;
  line-height: 1.6;
}
.post-body pre:hover .code-copy { opacity: 1; }
.code-copy:hover { color: var(--text); }
.code-copy.copied { color: var(--accent); border-color: var(--accent); opacity: 1; }
.post-body blockquote { border-left: 2px solid var(--accent); padding-left: 12px; color: var(--muted); margin: 8px 0; }
.post-body strong    { color: var(--text); font-weight: 600; }
.post-body em        { font-style: italic; color: var(--body-strong); }
.post-body table     { border-collapse: collapse; width: 100%; margin: 8px 0; font-size: 12px; }
.post-body th, .post-body td { border: 1px solid var(--border); padding: 5px 10px; text-align: left; white-space: nowrap; }
.post-body th        { background: var(--bg); color: var(--text); font-weight: 600; }
.post-body tr:nth-child(even) td { background: var(--wash-raise); }
.table-scroll        { overflow-x: auto; margin: 8px 0; }
.table-scroll table  { margin: 0; }
/* Feed previews have no room to scroll sideways, so cells wrap to fit the
   card instead of being sliced off at its edge. `fixed` splits the width
   evenly and — unlike auto layout — ignores cell min-content, which would
   otherwise let a long cell squeeze its neighbours down to one character
   per line.

   The modal now does the same, and for a related reason. It kept nowrap cells
   plus `.table-scroll` back when it was simply a wide panel — a fair trade with
   the width to spend. As a reading column it is not: the horizontal overflow
   carries no affordance, so a clipped column does not look clipped, it looks
   absent, and the one that vanishes is the rightmost — in a table answering
   "is this deployable?", the answer. Wrapping costs row height and shows every
   cell. `.table-scroll` stays as the backstop for a table too wide even
   wrapped. */
.feed .post-body table,
.pm-body .post-body table { table-layout: fixed; }
.feed .post-body th, .feed .post-body td,
.pm-body .post-body th, .pm-body .post-body td { white-space: normal; overflow-wrap: break-word; }
/* A many-column table divides the fixed layout's width evenly, so on a narrow
   modal a wide table (10+ columns) can land each cell under 40px — headers
   wrap one syllable per line and the table reads as noise.

   ⚠️ `min-width` on `th`/`td` cannot fix this — verified against a real
   Chromium render, not just the spec: under `table-layout: fixed` the browser
   computes column widths purely from the table's own resolved width and
   ignores any per-cell min/width entirely, so a `min-width` on the cells is a
   silent no-op (a build shipped with exactly that and the table stayed just
   as crushed). `min-width` set on the `<table>` element itself, by contrast,
   *is* honored as a hard floor even under fixed layout, and forces the table
   wider than its `.table-scroll` wrapper — that's the actual backstop the
   comment above describes. There is no column count in CSS, so main.js sets
   it inline per table (colCount * 80px) when it wraps the table in
   `.table-scroll`. The feed has no such wrapper, so its cards keep shrinking
   columns to fit rather than overflow. */

.post-footer {
  display: flex; align-items: center; justify-content: space-between;
  margin-top: 12px; padding-top: 10px; border-top: 1px solid var(--border);
}
/* Never break a stamp mid-phrase — an orphaned "ago" on its own line was
   the ugliest part of a wrapped footer. Chunks wrap whole or not at all. */
.post-time { font-size: 10px; color: var(--muted); letter-spacing: 0.02em; white-space: nowrap; }
.post-actions { display: flex; gap: 6px; opacity: 0; transition: opacity 0.15s; flex-shrink: 0; }
.post:hover .post-actions { opacity: 1; }

.btn-delete {
  background: none; border: 1px solid color-mix(in srgb, var(--red) 25%, transparent); color: var(--red);
  padding: 3px 9px; border-radius: 3px; cursor: pointer;
  font-family: inherit; font-size: 11px;
  transition: background 0.15s, border-color 0.15s;
}
.btn-delete:hover { background: color-mix(in srgb, var(--red) 10%, transparent); border-color: var(--red); }

.btn-edit {
  background: none; border: 1px solid var(--border); color: var(--muted);
  padding: 3px 9px; border-radius: 3px; cursor: pointer;
  font-family: inherit; font-size: 11px;
  transition: border-color 0.15s, color 0.15s;
}
.btn-edit:hover { border-color: var(--accent); color: var(--accent); }

/* ── Edit / Compose form ─────────────────────────────── */
.edit-form { display: flex; flex-direction: column; gap: 10px; }
.edit-form input, .edit-form textarea, .edit-form select {
  background: var(--bg); border: 1px solid var(--border); color: var(--text);
  padding: 7px 10px; border-radius: 4px; font-size: 12px; font-family: inherit; width: 100%;
  transition: border-color 0.15s;
}
.edit-form textarea       { resize: vertical; min-height: 120px; line-height: 1.65; }
.edit-form input:focus,
.edit-form textarea:focus,
.edit-form select:focus   { outline: none; border-color: var(--accent); }
.edit-form label {
  font-size: 9px; color: var(--accent-3); margin-bottom: 3px;
  display: block; text-transform: uppercase; letter-spacing: 0.12em;
}
.edit-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 2px; }

.btn-save {
  background: var(--accent); border: none; color: var(--on-accent);
  padding: 5px 14px; border-radius: 3px; cursor: pointer;
  font-family: inherit; font-size: 11px; font-weight: 600;
  letter-spacing: 0.1em; text-transform: uppercase;
  transition: background 0.15s;
}
.btn-save:hover    { background: var(--accent-dim); }
.btn-save:disabled { opacity: 0.55; cursor: not-allowed; background: var(--accent); }

.btn-cancel {
  background: none; border: 1px solid var(--border); color: var(--muted);
  padding: 5px 14px; border-radius: 3px; cursor: pointer;
  font-family: inherit; font-size: 11px;
  transition: border-color 0.15s, color 0.15s;
}
.btn-cancel:hover { border-color: var(--text); color: var(--text); }

.attach-row { display: flex; align-items: center; gap: 10px; margin-top: 6px; }
.btn-attach {
  background: none; border: 1px solid var(--border); color: var(--muted);
  padding: 4px 10px; border-radius: 3px; cursor: pointer;
  font-family: inherit; font-size: 11px;
  transition: border-color 0.15s, color 0.15s;
}
.btn-attach:hover { border-color: var(--accent); color: var(--accent); }
.attach-status { font-size: 11px; color: var(--muted); }
.attach-status.error { color: var(--red); }
.ef-attachments { margin-top: 6px; display: flex; flex-direction: column; gap: 3px; }
.ef-attach-head { font-size: 10px; text-transform: uppercase; letter-spacing: 0.1em; color: var(--accent-3); margin-bottom: 2px; }
.ef-attach-item { display: flex; align-items: center; gap: 8px; font-size: 12px; }
.ef-attach-name { color: var(--text); overflow-wrap: anywhere; }
.ef-attach-size { color: var(--muted); font-size: 11px; margin-left: auto; white-space: nowrap; }
.ef-attach-del { background: none; border: none; color: var(--muted); cursor: pointer; font-size: 15px; line-height: 1; padding: 0 4px; }
.ef-attach-del:hover { color: var(--red); }

/* ── Attachment gallery ─────────────────────────────── */
.attachments-view { flex: 1; overflow-y: auto; overflow-x: hidden; padding: 4px 20px 40px; scrollbar-width: thin; scrollbar-color: var(--border) transparent; }
.attachments-view::-webkit-scrollbar { width: 4px; }
.attachments-view::-webkit-scrollbar-track { background: transparent; }
.attachments-view::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
.att-folder-head { font-size: 11px; text-transform: uppercase; letter-spacing: 0.12em; color: var(--accent-3); margin: 18px 2px 8px; }
.att-folder-head:first-child { margin-top: 4px; }
.att-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: 12px; }
.att-card { position: relative; border: 1px solid var(--border); border-radius: 6px; overflow: hidden; background: color-mix(in srgb, var(--accent) 4%, transparent); }
.att-thumb { height: 110px; display: flex; align-items: center; justify-content: center; overflow: hidden; background: rgba(var(--scrim-rgb), 0.15); cursor: pointer; }
.att-thumb img { width: 100%; height: 100%; object-fit: cover; }
.att-thumb .att-ext { font-size: 11px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--accent-2); }
.att-thumb .att-ext::before { content: "📄"; display: block; font-size: 26px; margin-bottom: 4px; }
.att-meta { padding: 6px 8px; display: flex; flex-direction: column; gap: 2px; }
.att-name { font-size: 12px; overflow-wrap: anywhere; color: var(--text); }
.att-sub { font-size: 10px; color: var(--muted); display: flex; justify-content: space-between; }
.att-del { position: absolute; top: 4px; right: 4px; width: 22px; height: 22px; border: none; border-radius: 4px;
  background: rgba(var(--scrim-rgb), 0.55); color: var(--on-scrim); cursor: pointer; font-size: 15px; line-height: 1; opacity: 0; transition: opacity 0.15s; }
.att-card:hover .att-del { opacity: 1; }
.att-del:hover { background: var(--red); color: var(--on-scrim); }
.att-empty { color: var(--muted); font-size: 13px; padding: 20px 2px; }

.lightbox { position: fixed; inset: 0; background: rgba(var(--scrim-rgb), 0.85); z-index: 100; display: flex; align-items: center; justify-content: center; padding: 24px; cursor: pointer; }
.lightbox-inner { max-width: 92vw; max-height: 92vh; display: flex; flex-direction: column; align-items: center; gap: 10px; }
.lightbox img { max-width: 92vw; max-height: 82vh; object-fit: contain; border-radius: 4px; }
.lightbox-cap { color: var(--on-scrim); font-size: 12px; }
.edit-form textarea.drag-over { border-color: var(--accent); box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 25%, transparent); }

/* ── Recovery browser (inside the status panel) ───────── */
/* Not a sidebar view: Tags/Tree/Files are ways to browse what *exists*, and
   this is a different corpus. It renders into `.sm-body`, so it inherits the
   panel's scroll shell and 560px measure rather than owning either. */
.sm-back {
  align-self: flex-start; background: none; border: none; cursor: pointer;
  font: inherit; font-size: 11px; color: var(--muted); padding: 0;
  transition: color 0.15s;
}
.sm-back:hover { color: var(--accent); }
.del-filters { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 4px; }
.del-filter {
  background: none; cursor: pointer; font: inherit; font-size: 10px;
  letter-spacing: 0.06em; text-transform: uppercase; padding: 3px 8px;
  border: 1px solid var(--border); border-radius: 3px; color: var(--muted);
  transition: color 0.15s, border-color 0.15s;
}
.del-filter:hover { color: var(--text); }
.del-filter.active { color: var(--accent); border-color: var(--accent); }
.del-card {
  border: 1px solid var(--border); border-radius: 6px;
  padding: 12px 14px; margin-bottom: 10px;
  background: color-mix(in srgb, var(--red) 4%, transparent);
}
.del-head { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-bottom: 4px; }
.del-title { font-size: 13px; font-weight: 600; color: var(--text); min-width: 0; overflow-wrap: anywhere; }
/* The reason a post went away decides how alarming it should look: a TTL sweep
   is routine, a delete is deliberate, and an editor removing the file is the
   case this view exists for. */
.del-reason {
  font-size: 9px; letter-spacing: 0.1em; text-transform: uppercase;
  padding: 1px 7px; border-radius: 3px; flex-shrink: 0;
  color: var(--accent-3); background: color-mix(in srgb, var(--accent-3) 10%, transparent);
}
.del-reason-deleted { color: var(--red); background: color-mix(in srgb, var(--red) 10%, transparent); }
.del-reason-external { color: var(--accent-2); background: color-mix(in srgb, var(--accent-2) 10%, transparent); }
.del-meta { font-size: 10px; color: var(--muted); overflow-wrap: anywhere; margin-bottom: 8px; }
.del-actions { display: flex; gap: 6px; }
.del-failed { color: var(--red); margin: 8px 0 0; }
.btn-restore {
  background: none; border: 1px solid color-mix(in srgb, var(--green) 35%, transparent);
  color: var(--green); padding: 3px 9px; border-radius: 3px; cursor: pointer;
  font-family: inherit; font-size: 11px;
  transition: background 0.15s, border-color 0.15s;
}
.btn-restore:hover:not(:disabled) { background: color-mix(in srgb, var(--green) 12%, transparent); border-color: var(--green); }
.btn-restore:disabled { opacity: 0.6; cursor: default; }
.del-body {
  margin: 10px 0 0; padding: 10px 12px; max-height: 260px; overflow: auto;
  background: var(--bg); border: 1px solid var(--border); border-radius: 4px;
  font-family: inherit; font-size: 12px; line-height: 1.55; color: var(--body);
  white-space: pre-wrap; overflow-wrap: anywhere;
  scrollbar-width: thin; scrollbar-color: var(--border) transparent;
}
@media (max-width: 768px) {
  /* `.sm-body .btn-edit` reaches the Recovery section's own button too — it is
     not inside `.del-actions`, and the sheet touch-floor smoke caught it at
     132x23 rather than my noticing. */
  .del-actions button, .del-filter, .sm-back, .sm-body .btn-edit {
    min-height: 44px; display: inline-flex; align-items: center;
  }
}

/* ── Search bar ─────────────────────────────────────── */
.search-bar {
  display: flex; align-items: center; gap: 8px;
  padding: 8px 20px; border-bottom: 1px solid var(--border);
  background: var(--surface); flex-shrink: 0;
}
.search-bar input {
  flex: 1; background: var(--bg); border: 1px solid var(--border);
  color: var(--text); padding: 5px 10px; border-radius: 4px;
  font-size: 12px; font-family: inherit; transition: border-color 0.15s;
}
.search-bar input:focus { outline: none; border-color: var(--accent); }
.search-bar input::placeholder { color: var(--muted); }
.search-clear {
  background: none; border: none; color: var(--muted); cursor: pointer;
  font-family: inherit; font-size: 18px; line-height: 1; padding: 0 2px;
  transition: color 0.15s; display: none;
}
.search-clear:hover { color: var(--text); }
.search-bar.active input { border-color: var(--accent); }
.search-bar.active .search-clear { display: block; }
.search-label {
  font-size: 10px; color: var(--accent); letter-spacing: 0.08em;
  text-transform: uppercase; flex-shrink: 0; display: none;
}
.search-bar.active .search-label { display: block; }

.view-toggle { display: flex; flex-shrink: 0; }
.vt-btn {
  background: none; border: 1px solid var(--border); color: var(--muted);
  cursor: pointer; font-size: 13px; line-height: 1; padding: 5px 9px;
  font-family: inherit; transition: color 0.15s, border-color 0.15s, background 0.15s;
}
.vt-btn:first-child { border-radius: 4px 0 0 4px; }
.vt-btn:last-child  { border-radius: 0 4px 4px 0; border-left: none; }
.vt-btn.active { color: var(--accent); border-color: color-mix(in srgb, var(--accent) 40%, transparent); background: var(--tag-bg); }
.vt-btn:hover:not(.active) { color: var(--text); }

.sort-control { display: flex; flex-shrink: 0; }
.sort-select, .mode-select {
  appearance: none; -webkit-appearance: none;
  background-color: var(--bg); border: 1px solid var(--border); color: var(--text);
  cursor: pointer; font-size: 12px; font-family: inherit;
  padding: 4px 22px 4px 8px; border-radius: 4px;
  transition: border-color 0.15s;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='7' viewBox='0 0 10 7' fill='none' stroke='%238a8a8a' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M1 1l4 4 4-4'/%3E%3C/svg%3E");
  background-repeat: no-repeat; background-position: right 7px center;
}
.sort-select { border-radius: 4px 0 0 4px; }
.sort-select:hover, .mode-select:hover { border-color: var(--muted); }
.sort-select:focus, .mode-select:focus { outline: none; border-color: var(--accent); }
.sort-select option, .mode-select option { background: var(--surface); color: var(--text); }
.sort-dir { border-radius: 0 4px 4px 0; border-left: none; min-width: 30px; }
/* Off (embedding_enabled=false server-side, the default) until confirmed via
   GET /status — main.js reveals it, never CSS, so there is no theme/media
   query to keep in sync here. */
.mode-select { flex-shrink: 0; }
/* Separate the sort group from the list/grid view group. */
.search-bar .view-toggle { margin-left: 6px; padding-left: 12px; border-left: 1px solid var(--border); }

/* New-posts pill — shown when live posts arrive but the current sort
   wouldn't place them at the top (so we don't insert them out of order). */
.new-posts-pill {
  position: sticky; top: 8px; z-index: 20;
  display: block; width: fit-content; margin: 8px auto -4px; padding: 6px 16px;
  background: var(--accent); color: var(--on-accent); border: none; border-radius: 999px;
  font-family: inherit; font-size: 12px; font-weight: 600; cursor: pointer;
  box-shadow: var(--shadow-pill); transition: opacity 0.15s, transform 0.15s;
}
.new-posts-pill:hover { transform: translateY(-1px); }
.new-posts-pill .np-arrow { font-weight: 700; }

/* ── Load more / states ──────────────────────────────── */
.load-more { text-align: center; padding: 14px 20px; }
.load-more button {
  background: none; border: 1px solid var(--border); color: var(--muted);
  padding: 7px 20px; border-radius: 4px; cursor: pointer;
  font-family: inherit; font-size: 11px; letter-spacing: 0.1em; text-transform: uppercase;
  transition: border-color 0.15s, color 0.15s;
}
.load-more button:hover { border-color: var(--accent); color: var(--accent); }

.empty {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  color: var(--muted); padding: 64px 16px; gap: 14px; flex: 1;
}
.empty svg { opacity: 0.25; }
.empty p   { font-size: 11px; letter-spacing: 0.08em; text-transform: uppercase; }

.auth-prompt {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  color: var(--muted); padding: 64px 16px; gap: 8px;
}
.auth-prompt p        { font-size: 12px; }
.auth-prompt p + p    { font-size: 11px; opacity: 0.6; margin-top: 4px; }

/* ── Responsive ──────────────────────────────────────── */

/* iOS dynamic viewport height */
@supports (height: 100dvh) { body { height: 100dvh; } }

/* Hamburger button — desktop hidden */
.menu-btn {
  display: none; background: none; border: none; cursor: pointer;
  flex-direction: column; justify-content: space-between;
  width: 18px; height: 12px; padding: 0; flex-shrink: 0;
}
.menu-btn span {
  display: block; width: 100%; height: 1.5px;
  background: var(--muted); border-radius: 1px; transition: background 0.15s;
}
.menu-btn:hover span, .menu-btn.active span { background: var(--accent); }

/* Sidebar overlay */
.sidebar-overlay {
  display: none; position: fixed; inset: 0; z-index: 49;
  background: rgba(var(--scrim-rgb), 0.6);
  -webkit-backdrop-filter: blur(2px); backdrop-filter: blur(2px);
}
.sidebar-overlay.visible { display: block; }

@media (max-width: 768px) {
  /* Header */
  header { padding: 0 14px; gap: 10px; }
  .menu-btn { display: flex; }
  /* Drawer model on mobile — desktop collapse is meaningless here */
  .collapse-btn { display: none !important; }
  aside.collapsed { width: 260px; opacity: 1; }
  .brand-sep { display: none; }
  .live-label { display: none; }
  .key-wrap input { width: 140px; }
  .btn-new-post { height: 40px; font-size: 10px; padding: 0 11px; letter-spacing: 0.06em; }

  /* Sidebar → slide-in drawer */
  aside {
    position: fixed; top: 48px; left: 0; bottom: 0; z-index: 50;
    width: 260px;
    transform: translateX(-100%);
    transition: transform 0.24s cubic-bezier(0.4,0,0.2,1);
    box-shadow: var(--shadow-drawer);
  }
  aside.open { transform: translateX(0); }

  /* Search bar. `min-width: 0` alone (the previous fix here) let the input
     give up space under flex, but with nothing stopping it, four siblings —
     mode select, sort select, sort direction, two view-toggle buttons — could
     still squeeze it down to a few unusable pixels on a real phone width.
     `input, select { font-size: 16px !important }` above (iOS zoom guard,
     @media (hover: none)) also means the 11px this block used to set on
     .sort-select/.mode-select never actually applied on a touch device —
     shrinking those was never doing anything there. The real levers on a
     touch device are padding and each control's own rendered width, not
     font-size: tighter padding throughout, and a capped, ellipsis-truncated
     width on the two <select>s (their closed-state label — "Keyword",
     "Updated" — otherwise sizes the box to fit the longest option in full).
     The input's own min-width is the floor that actually matters: it can
     still shrink below its content size for a long placeholder, just not
     below something you can read and type into. */
  /* wrap is a safety net, not the expectation: mode-select (opt-in, off by
     default everywhere) plus an active search's "×" plus the narrowest
     supported phone (320px, the @media (max-width: 400px) tier) is the one
     combination where the floors above can still add up past the viewport.
     Wrapping to a second line beats silently clipping controls off-screen. */
  .search-bar { gap: 5px; padding: 8px 14px; flex-wrap: wrap; }
  .search-bar input { min-width: 72px; }
  .search-bar .search-label { display: none !important; }
  .sort-select, .mode-select {
    padding: 4px 16px 4px 5px; max-width: 62px;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
  .sort-dir { min-width: 24px; padding: 5px 5px; }
  .vt-btn { padding: 5px 6px; font-size: 12px; }
  .search-bar .view-toggle { margin-left: 3px; padding-left: 6px; }

  /* Feed — full width, tighter spacing */
  .feed { padding: 12px; gap: 8px; }
  .compose-panel { padding: 0 12px; }
  .compose-panel.open { padding: 12px 12px 0; }
  .post { padding: 12px; }
  .load-more { padding: 10px 12px; }
  /* Per-card Edit/Delete are hazardous in a touch scroll feed — drive
     edits/deletes from the post detail view (tap a card to open it). */
  .post-actions { display: none; }
}

/* Touch devices: always show post actions (no hover) */
@media (hover: none) {
  .post-actions { opacity: 1; }
  /* Prevent iOS auto-zoom on input focus (triggered when font-size < 16px) */
  input, textarea, select { font-size: 16px !important; }
}

@media (max-width: 400px) {
  header { gap: 8px; }
  .key-wrap input { width: 110px; font-size: 11px; }
  .btn-connect { padding: 5px 8px; }
  .btn-new-post { padding: 0 9px; }
}

/* ── Post ID pill ────────────────────────────────────── */
.post-id-pill {
  font-size: 10px; color: var(--accent-2);
  background: color-mix(in srgb, var(--accent-2) 10%, transparent);
  padding: 1px 6px; border-radius: 3px;
  letter-spacing: 0.02em; font-variant-numeric: tabular-nums; flex-shrink: 0;
}

/* ── Master document card ────────────────────────────── */
.post.master-doc {
  border-color: color-mix(in srgb, var(--accent) 45%, transparent);
  background: color-mix(in srgb, var(--accent) 2.5%, transparent);
}
.post.master-doc:hover {
  border-color: var(--accent);
  box-shadow: 0 4px 28px color-mix(in srgb, var(--accent) 12%, transparent);
}
.post.pinned {
  border-color: var(--accent);
  box-shadow: inset 3px 0 0 var(--accent);
}
.master-badge {
  display: inline-flex; align-items: center; gap: 5px;
  font-size: 9px; font-weight: 600; color: var(--accent);
  background: color-mix(in srgb, var(--accent) 10%, transparent); border: 1px solid color-mix(in srgb, var(--accent) 30%, transparent);
  padding: 2px 8px; border-radius: 3px; letter-spacing: 0.14em;
  text-transform: uppercase; margin-bottom: 10px;
}
.accordion-chevron {
  display: inline-block; margin-left: 2px; font-size: 8px;
  transition: transform 0.28s cubic-bezier(0.4,0,0.2,1);
}
.post.master-doc.accordion.collapsed .accordion-chevron { transform: rotate(-90deg); }

/* ── Master document accordion ───────────────────────── */
/* max-height is driven by JS (scrollHeight ↔ a few-line peek) for a smooth
   animation over arbitrary content; the collapsed peek keeps the fade. */
.post.master-doc.accordion .post-body-wrap {
  overflow: hidden;
  transition: max-height 0.3s cubic-bezier(0.4,0,0.2,1);
}
.post.master-doc.accordion .post-body-wrap::after { display: none; }

/* ── Post footer left group ──────────────────────────── */
.post-footer-left { display: flex; align-items: center; gap: 8px; }

/* ── Post expires label ──────────────────────────────── */
.post-expires {
  font-size: 10px; color: var(--accent-dim); letter-spacing: 0.02em;
  margin-left: 8px;
}

/* ── Tag config inline form ──────────────────────────── */
/* The row is a flex container, so its child's automatic minimum is min-content —
   and `datetime-local` has a wide one (the mm/dd/yyyy, --:-- -- placeholder plus
   the picker icon). Without min-width:0 the form refused to shrink and spilled
   out of the sidebar, taking its controls off-screen with it: the same automatic
   minimum that bites the grid tiles. `tag-editing` drops the row out of flex
   layout entirely while a form is in it. */
/* Last-resort clip: even if some future control has an intrinsic minimum wider
   than the sidebar, it is scrolled rather than painted over the feed. */
.tag-list { overflow-x: hidden; }
.tag-item.tag-editing {
  display: block; padding: 4px 12px 10px; cursor: default;
  background: var(--wash-raise);
}
.tag-config-form {
  display: flex; flex-direction: column; gap: 5px;
  min-width: 0; max-width: 100%;
}
.tc-label {
  font-size: 9px; letter-spacing: 0.12em; text-transform: uppercase;
  color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.tag-config-form input {
  background: var(--bg); border: 1px solid var(--accent); color: var(--text);
  border-radius: 3px; font-size: 11px; padding: 4px 6px;
  width: 100%; min-width: 0; max-width: 100%;
  outline: none; font-family: inherit;
}
.tc-actions { display: flex; gap: 6px; min-width: 0; }
.tc-actions button {
  flex: 1; min-width: 0;
  background: none; border: 1px solid var(--border); border-radius: 3px;
  color: var(--muted); font-family: inherit; font-size: 10px;
  padding: 4px 6px; cursor: pointer;
  transition: color 0.12s, border-color 0.12s;
}
.tc-actions .tc-save { color: var(--accent); border-color: var(--accent-dim); }
.tc-actions button:hover { color: var(--text); border-color: var(--accent); }
/* The rename input lives inside the row's flex layout and needs the same escape. */
.tag-rename-input { min-width: 0; max-width: 100%; }
/* .tag-config-btn is styled with .tag-rename above — same shape, same behaviour. */

/* ── Card truncation ─────────────────────────────────── */
.post:not(.editing) { cursor: pointer; }
.post-body-wrap { position: relative; max-height: 72px; overflow: hidden; }
.post-body-wrap::after {
  content: ''; position: absolute; bottom: 0; left: 0; right: 0;
  height: 24px;
  background: linear-gradient(transparent, var(--surface));
  pointer-events: none;
}
.post.editing { border-color: color-mix(in srgb, var(--accent) 40%, transparent); background: color-mix(in srgb, var(--accent) 2.5%, transparent); }
.post.editing .post-body-wrap { max-height: none; overflow: visible; }
.post.editing .post-body-wrap::after { display: none; }
/* The pinned master doc is a reference, not a read — keep its preview short. */
.post.pinned .post-body-wrap { max-height: 44px; }
/* Tighter previews on mobile so more posts fit per screen. Must follow the
   base rules above (equal specificity → later source wins). */
@media (max-width: 768px) {
  .post-body-wrap { max-height: 60px; }
  .post.pinned .post-body-wrap { max-height: 40px; }
}

/* ── Post modal ──────────────────────────────────────── */
/* Status / about modal. Reuses .pm-backdrop and the modalIn keyframes; its own
   shell because it is a narrow read-only panel, not the post viewer. */
.status-modal { display: none; position: fixed; inset: 0; z-index: 110; align-items: center; justify-content: center; }
.status-modal.open { display: flex; }
.sm-inner {
  position: relative; z-index: 1;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: 8px; width: 94%; max-width: 560px; max-height: 92vh;
  display: flex; flex-direction: column; overflow: hidden;
  box-shadow: var(--shadow-modal);
  animation: modalIn 0.18s ease backwards;
}
.sm-head { display: flex; align-items: baseline; gap: 8px; padding: 18px 20px 0; flex-shrink: 0; }
.sm-name { font-size: 15px; font-weight: 600; color: var(--text); }
.sm-version { font-size: 12px; color: var(--accent); font-variant-numeric: tabular-nums; }
.sm-body {
  padding: 16px 20px 18px; overflow-y: auto;
  display: flex; flex-direction: column; gap: 18px;
  /* One label column for the whole panel. Every section used to size its own —
     `auto 1fr` per grid — so Vault's values started at one x and Server's at
     another, while Health pushed its values to the far right edge entirely.
     Three alignments down one 560px panel is what "unbalanced" was. Widest
     label is "Full-text search" at ~120px plus the 15px status-dot gutter. */
  --sm-label: 150px;
}
.sm-section-title {
  font-size: 10px; letter-spacing: 0.12em; text-transform: uppercase;
  color: var(--accent-3); margin-bottom: 8px;
}
/* The value column may wrap — a long vault path must not widen the panel (same
   min-width:0 lesson as the grid tiles). */
.sm-rows {
  display: grid; grid-template-columns: minmax(0, var(--sm-label)) 1fr;
  gap: 6px 14px; font-size: 12.5px; margin: 0;
}
.sm-rows dt { color: var(--muted); }
.sm-rows dd { margin: 0; color: var(--text); min-width: 0; overflow-wrap: anywhere; font-variant-numeric: tabular-nums; }
/* Health is the same grid; the row is `contents` so the dot+label pair and the
   note fall into those two columns rather than forming a flex row of their own. */
.sm-health { gap: 7px 14px; }
.sm-feat { display: contents; }
.sm-feat-name { display: flex; align-items: center; gap: 8px; color: var(--muted); min-width: 0; }
.sm-dot { width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0; }
.sm-dot.ok   { background: var(--green); }
.sm-dot.warn { background: var(--accent); }
.sm-dot.bad  { background: var(--red); }
.sm-dot.off  { background: var(--muted); }
.sm-feat-note { color: var(--text); min-width: 0; overflow-wrap: anywhere; }
.sm-recovery-line { font-size: 12.5px; color: var(--text); margin-bottom: 10px; }
.sm-error { font-size: 12.5px; color: var(--red); }
.sm-embed-controls { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 10px; }
.sm-embed-controls .btn-edit:disabled { opacity: 0.5; cursor: default; }
.sm-embed-controls .btn-edit:disabled:hover { border-color: var(--border); color: var(--muted); }

.post-modal { display: none; position: fixed; inset: 0; z-index: 100; align-items: center; justify-content: center; }
.post-modal.open { display: flex; }

.pm-backdrop {
  position: absolute; inset: 0;
  background: rgba(var(--scrim-rgb), 0.72);
  backdrop-filter: blur(3px); -webkit-backdrop-filter: blur(3px);
  cursor: pointer;
}
.pm-inner {
  position: relative; z-index: 1;
  background: var(--surface); border: 1px solid var(--border);
  border-radius: 8px; width: 94%; max-width: 1320px; max-height: 92vh;
  display: flex; flex-direction: column; overflow: hidden;
  box-shadow: var(--shadow-modal);
  animation: modalIn 0.18s ease backwards;
}
@keyframes modalIn {
  from { opacity: 0; transform: translateY(10px) scale(0.98); }
  to   { opacity: 1; transform: none; }
}
.pm-header {
  display: flex; align-items: flex-start; gap: 12px;
  padding: 18px 20px 0; flex-shrink: 0;
}
.pm-title { flex: 1; min-width: 0; font-size: 15px; font-weight: 600; color: var(--text); line-height: 1.4; overflow-wrap: anywhere; }
.pm-back {
  display: none; background: none; border: none;
  color: var(--muted); cursor: pointer; font-family: inherit;
  font-size: 11px; font-weight: 600; letter-spacing: 0.1em; text-transform: uppercase;
  padding: 0; flex-shrink: 0; transition: color 0.15s;
}
.pm-back:hover { color: var(--accent); }
/* When navigating between notes: drop the ALL-CAPS shout, let the title show naturally.
   On desktop we also flip the header to a column so the breadcrumb has its own row. */
.pm-inner.has-back .pm-back {
  text-transform: none;
  letter-spacing: 0;
  font-size: 12px;
  font-weight: 500;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
@media (min-width: 769px) {
  .pm-inner.has-back .pm-header {
    flex-direction: column;
    gap: 4px;
  }
  .pm-inner.has-back .pm-back {
    max-width: calc(100% - 40px); /* leave room for the absolute-positioned × */
  }
}
.pm-close, .sm-close {
  background: none; border: none; color: var(--muted); cursor: pointer;
  font-size: 22px; line-height: 1; padding: 0; flex-shrink: 0;
  font-family: inherit; transition: color 0.15s;
}
.pm-close:hover, .sm-close:hover { color: var(--text); }
/* Pinned to the panel, not to the header. The header is a centred column now,
   so a close button inside its flow lands in the middle of the panel rather
   than in its corner. `.pm-inner` is already `position: relative`. */
.pm-close {
  position: absolute; top: 16px; right: 18px; z-index: 3;
}
.sm-close { margin-left: auto; }
.pm-meta { padding: 12px 20px 0; display: flex; flex-direction: column; gap: 8px; flex-shrink: 0; }
.pm-meta .post-tags { row-gap: 6px; }
.pm-time { font-size: 10px; color: var(--muted); letter-spacing: 0.02em; }
.pm-body-wrap { flex: 1; min-height: 0; position: relative; display: flex; flex-direction: column; }
.pm-body {
  flex: 1; min-height: 0; overflow-y: auto; overflow-x: hidden; padding: 16px 20px;
}
.pm-body, .sm-body, .hm-list, .hm-body-text {
  scrollbar-width: thin; scrollbar-color: var(--border) transparent;
}
.pm-body::-webkit-scrollbar, .sm-body::-webkit-scrollbar,
.hm-list::-webkit-scrollbar, .hm-body-text::-webkit-scrollbar { width: 4px; }
.pm-body::-webkit-scrollbar-thumb, .sm-body::-webkit-scrollbar-thumb,
.hm-list::-webkit-scrollbar-thumb, .hm-body-text::-webkit-scrollbar-thumb {
  background: var(--border); border-radius: 2px;
}
/* One reading column, and everything lives in it — prose, headings, rules,
   tables, code, images. Wide blocks used to break out of it: first centred
   (which put a table ~300px left of every paragraph, aligned with nothing) and
   then anchored to the column's left edge and spilling right (better, still
   unbalanced — the panel had one ragged edge). Constraining them costs some row
   height on a wide table and buys a page with two straight sides.

   `.table-scroll` and `pre` keep their own `overflow-x`, so anything that still
   cannot fit scrolls inside the column rather than out of it.

   ⚠️ **The measure is `rem`, never `ch`.** `ch` resolves against each element's
   *own* font, and headings render in a proportional sans-serif while the body is
   monospace — 88ch gave paragraphs 704px and an h3 654px, so once the column was
   centred every block landed on a different left edge and the headings appeared
   to hang outward. `rem` resolves against the root size whatever the element is
   set in. 64rem is 832px at the root's 13px, about 104 monospace characters. */
.pm-inner { --measure: 64rem; }
.pm-body .post-body > * { max-width: var(--measure); margin-inline: auto; }

/* The title and meta ride the same column, or centring the prose alone just
   moves it away from everything else — the header stays hard left and the text
   floats in the middle, which is worse than the gap it was meant to fix. With
   all three centred the panel reads as one article column and the wide blocks
   break out of it on both sides. The footer is chrome, not content, so it keeps
   the full width. */
/* `--measure + 40px` so the *content* box is exactly the measure once the 20px
   side padding is taken off — the same box `.pm-body`'s prose is centred in,
   which is what makes the title and the first paragraph share a left edge.
   Asymmetric padding here put them 16px apart, which is precisely visible
   enough to look wrong. */
.pm-header, .pm-meta { max-width: calc(var(--measure) + 40px); margin-inline: auto; width: 100%; }
/* The close button is pinned to the panel, so reserve room in the title rather
   than in the header — at narrow widths the column fills the panel and a long
   ellipsised title would otherwise run underneath it. */
.pm-title { padding-right: 30px; }

.pm-body-fade {
  position: absolute; bottom: 0; left: 0; right: 0; height: 52px;
  background: linear-gradient(to bottom, transparent, var(--surface));
  pointer-events: none; transition: opacity 0.2s;
}
.pm-body-fade.hidden { opacity: 0; }
.pm-footer {
  display: flex; gap: 8px; justify-content: flex-end;
  padding: 12px 20px; border-top: 1px solid var(--border); flex-shrink: 0;
  background: var(--surface);
}
/* ── Wikilink hover preview ─────────────────────────────────── */
.link-preview {
  position: fixed; z-index: 200; pointer-events: none;
  background: var(--surface); border: 1px solid var(--border); border-radius: 6px;
  padding: 10px 13px; width: 280px; max-width: calc(100vw - 24px);
  box-shadow: var(--shadow-modal);
  animation: modalIn 0.12s ease backwards;
}
.lp-title { font-size: 13px; font-weight: 600; color: var(--text); margin-bottom: 5px; }
.lp-tags  { display: flex; flex-wrap: wrap; gap: 4px; margin-bottom: 6px; }
.lp-tag   { font-size: 10px; color: var(--muted); background: var(--bg); border: 1px solid var(--border); border-radius: 3px; padding: 1px 5px; }
.lp-body  { font-size: 11px; color: var(--muted); line-height: 1.5; }

/* ── Keyboard shortcuts modal ───────────────────────────────── */
.kb-inner { max-width: 360px; }
.kb-inner .sm-body { gap: 20px; }
.kb-group { display: flex; flex-direction: column; gap: 7px; }
.kb-group-label { font-size: 10px; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; color: var(--muted); padding-bottom: 2px; border-bottom: 1px solid var(--border); }
.kb-row { display: flex; align-items: center; justify-content: space-between; gap: 16px; font-size: 13px; color: var(--body); }
.kb-row > span:first-child { display: flex; gap: 4px; flex-shrink: 0; }
kbd {
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--bg); border: 1px solid var(--border); border-bottom-width: 2px;
  border-radius: 4px; padding: 1px 7px; font-family: inherit; font-size: 11px;
  font-weight: 600; color: var(--text); min-width: 24px; white-space: nowrap;
}

@media (max-width: 768px) {
  .link-preview { display: none; } /* touch devices don't hover */
}

@media (max-width: 768px) {
  .post-modal.open { align-items: flex-end; }
  .status-modal.open { align-items: flex-end; }
  .sm-inner { width: 100%; max-width: none; }
  /* `backwards`, and no `to` keyframe — both deliberate, and together they are
     what lets a drag move the sheet. A forwards fill outranks inline styles, so
     `both` plus an explicit `to { transform: none }` pinned the panel at the
     origin for the whole gesture: the swipe handler wrote transforms nothing
     ever painted. An omitted `to` resolves against the element's own style,
     which is exactly where the drag writes. */
  .pm-inner, .sm-inner { animation: modalInMobile 0.22s ease backwards; }
  @keyframes modalInMobile {
    from { opacity: 0; transform: translateY(20px); }
  }

  /* Every sheet is draggable, so every sheet shows the same handle. It hangs
     off the panel rather than sitting in the header's flow because .sm-head is
     a baseline row (name · version · close) that stacking would break, while
     .pm-header is a column — one rule has to serve both. */
  .pm-inner, .sm-inner { border-radius: 12px 12px 0 0; }
  .pm-inner::before, .sm-inner::before {
    content: ""; position: absolute; z-index: 2; pointer-events: none;
    top: 8px; left: 50%;
    width: 40px; height: 4px; margin-left: -20px; border-radius: 999px;
    background: var(--border);
    transition: background-color 0.18s ease, width 0.18s ease, margin-left 0.18s ease;
  }
  /* The handle answers the thumb: it darkens on contact and turns accent once
     you are past the point where letting go dismisses. Without that the only
     feedback is distance travelled, which you cannot judge mid-gesture. */
  .pm-inner.sheet-dragging::before, .sm-inner.sheet-dragging::before { background: var(--muted); }
  .pm-inner.sheet-armed::before, .sm-inner.sheet-armed::before {
    background: var(--accent); width: 56px; margin-left: -28px;
  }
  @media (prefers-reduced-motion: reduce) {
    .pm-inner::before, .sm-inner::before { transition: none; }
  }

  /* Room for the handle above the first row of each header. */
  .pm-header {
    flex-direction: column; align-items: stretch; gap: 6px;
    padding: 20px 16px 0;
  }
  .sm-head { padding-top: 22px; }
  .pm-back {
    display: inline-flex; align-items: center; align-self: flex-start;
    margin-left: -2px; padding: 6px 10px 6px 0; min-height: 44px;
  }
  /* No pinned close button here — back is the dismissal — so the title needs no
     room reserved for it, and the column cap is a no-op at this width anyway. */
  .pm-title { font-size: 18px; font-weight: 700; line-height: 1.3; padding-right: 0; }
  .pm-close { display: none; }
  .pm-meta { padding-left: 16px; padding-right: 16px; }
  .pm-body { padding-left: 16px; padding-right: 16px; }
  .pm-footer { padding-bottom: calc(12px + env(safe-area-inset-bottom, 0px)); }
}

/* ── Post history panel ───────────────────────────────────── */
/* Fixed height and a two-pane grid, both deliberate. The panel used to be sized
   by its content, so selecting a revision collapsed it to the loading state and
   re-inflated when the body arrived — a visible jump on every click. Panes that
   exist up front and scroll internally cannot do that. The extra width is for
   reading an old version, which is the actual task here. */
.hm-inner {
  max-width: 1280px;
  width: 94%;
  height: min(92vh, 1000px);
}
#historyModal .sm-body { flex: 1; min-height: 0; overflow: hidden; }

.hm-layout {
  display: grid;
  grid-template-columns: minmax(260px, 340px) 1fr;
  gap: 0;
  height: 100%;
  min-height: 0;
}
.hm-list {
  display: flex; flex-direction: column; gap: 2px;
  overflow-y: auto; min-height: 0; padding-right: 12px;
}
.hm-pane {
  display: flex; flex-direction: column; min-height: 0; min-width: 0;
  border-left: 1px solid var(--border); padding-left: 16px;
}
.hm-rev {
  display: flex; flex-direction: column; align-items: stretch; gap: 3px; width: 100%;
  background: none; border: 1px solid transparent; border-radius: 4px;
  padding: 7px 9px; cursor: pointer; text-align: left; flex-shrink: 0;
  font-family: inherit; font-size: 12.5px; color: var(--text);
  transition: background 0.12s, border-color 0.12s;
}
.hm-meta { display: flex; align-items: center; gap: 7px; min-width: 0; }
.hm-badge {
  color: var(--accent); border: 1px solid var(--accent-dim); border-radius: 3px;
  padding: 0 4px; font-size: 8.5px; letter-spacing: 0.1em; text-transform: uppercase;
  flex-shrink: 0;
}
.hm-rev:hover { border-color: var(--border); background: var(--accent-glow); }
.hm-rev.active { border-color: var(--accent); background: var(--accent-glow); }
.hm-sha { color: var(--accent); font-size: 10.5px; font-variant-numeric: tabular-nums; flex-shrink: 0; }
/* The message wraps to two lines rather than ellipsizing on one: it is the only
   thing that says what a revision *was*. min-width:0 still keeps it from widening
   the column. */
.hm-msg {
  min-width: 0; line-height: 1.35;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden; overflow-wrap: anywhere;
}
.hm-when { color: var(--muted); font-size: 10.5px; flex-shrink: 0; }

.hm-pane-head { flex-shrink: 0; margin-bottom: 8px; }
.hm-body-text {
  flex: 1; min-height: 0; overflow: auto; margin: 0;
  padding: 12px;
  background: var(--bg); border: 1px solid var(--border); border-radius: 4px;
  font-family: inherit; font-size: 12.5px; line-height: 1.55; color: var(--text);
  white-space: pre-wrap; overflow-wrap: anywhere;
}
.hm-view-toggle { display: flex; flex-shrink: 0; margin-bottom: 8px; }
.hm-view-toggle .vt-btn { font-size: 10px; padding: 3px 9px; }
/* Diff lines are `display: block` inside the same `pre` shell the body uses, so
   both panes measure identically and switching between them cannot resize the
   panel — the fixed-height rule the whole panel depends on. */
.hm-diff .hm-d { display: block; padding: 0 6px; margin: 0 -6px; }
.hm-d-add { background: color-mix(in srgb, var(--green) 14%, transparent); color: var(--text); }
.hm-d-del { background: color-mix(in srgb, var(--red) 14%, transparent); color: var(--text); }
.hm-d-same { color: var(--muted); }
/* Markers are pseudo-elements so a copied diff yields the text itself, with no
   column of +/- to strip before pasting it back into a note. */
.hm-d-add::before { content: "+ "; color: var(--green); }
.hm-d-del::before { content: "− "; color: var(--red); }
.hm-d-same::before { content: "  "; }
.hm-restore { width: 100%; margin-top: 10px; flex-shrink: 0; }
/* Placeholder and loading share the pane's centre so neither changes its size. */
.hm-placeholder {
  flex: 1; display: flex; align-items: center; justify-content: center;
  color: var(--muted); font-size: 12.5px; text-align: center; padding: 20px;
}

@media (max-width: 860px) {
  .hm-inner { height: 88vh; }
  .hm-layout { grid-template-columns: 1fr; grid-template-rows: minmax(0, 40%) 1fr; }
  .hm-list { padding-right: 0; padding-bottom: 10px; }
  .hm-pane { border-left: none; border-top: 1px solid var(--border); padding-left: 0; padding-top: 12px; }
}

/* ── Edit modal ───────────────────────────────────────────── */
/* Editing used to happen inside the post card. In grid view that meant a ~200px
   column: the textarea was a few words wide and long notes were unusable. The
   form is the same, it just gets the room the reading modal already had. */
.em-inner {
  max-width: 1320px;
  width: 94%;
  height: min(92vh, 1040px);
}
#editModal .sm-body { flex: 1; min-height: 0; display: flex; }
#editModal .edit-form {
  display: flex; flex-direction: column; gap: 10px;
  flex: 1; min-height: 0; min-width: 0;
}
/* The content field takes whatever is left, so a long note gets the whole panel
   rather than a fixed few rows. */
#editModal .edit-form > .ef-content-wrap { display: flex; flex-direction: column; flex: 1; min-height: 0; }
#editModal .edit-form textarea.ef-content { flex: 1; min-height: 140px; resize: none; }
#editModal .edit-form .ef-attachments { max-height: 22vh; overflow-y: auto; }
#editModal .edit-actions {
  margin: 4px -20px -18px; padding: 12px 20px; flex-shrink: 0;
  border-top: 1px solid var(--border); background: var(--surface);
}
@media (max-width: 860px) {
  .em-inner { height: 92vh; }
}


/* ── Mobile sheets: geometry, reach, safe areas ──────────────────────────── */
/* ⚠️ `--sm-label` belongs in *this* block, not the ≤768px one further up: the
   base `.sm-body` rule is declared after that media query at equal specificity,
   so the override lost on source order and the phone sheet kept the full 150px
   column — measured, not noticed. Same trap as `.em-inner`/`.hm-inner` below.
   A 150px label column of a ~350px sheet leaves the vault path wrapping every
   few characters. */
@media (max-width: 768px) {
  .sm-body { --sm-label: 108px; }
}

/* This block sits at the end of the file deliberately. `.em-inner`/`.hm-inner`
   are declared *after* the modal media block above and carry equal specificity,
   so the `width: 100%` up there lost on source order — the edit and history
   sheets sat 23px narrower than the post and status ones, which nobody chose.
   Position is the entire fix: both panels also carry `.sm-inner`, so one rule
   placed after their declarations covers them. Naming `.em-inner`/`.hm-inner`
   here would read as though the selector were doing the work. */
@media (max-width: 768px) {
  .pm-inner, .sm-inner { width: 100%; max-width: none; }

  /* The post modal used to be full-bleed at 100vh: its rounded top and grab
     handle were pressed against the screen edge (under the notch on a real
     phone) and there was no backdrop left to tap. Leaving a strip makes it read
     as a sheet like the other three, and gives back the tap-outside dismissal. */
  /* The inset keeps the tallest sheet clear of the notch. It belongs here,
     on the one panel that comes close to the top, rather than on every header. */
  .pm-inner { max-height: calc(94vh - env(safe-area-inset-top, 0px)); }
  .em-inner { height: 92vh; }
  .hm-inner { height: 88vh; }
  /* dvh, not vh. `vh` is the *large* viewport and ignores the software
     keyboard, so focusing the edit sheet's textarea pushed Save and Cancel
     underneath the keyboard with no way to reach them. */
  @supports (height: 100dvh) {
    .pm-inner { max-height: calc(94dvh - env(safe-area-inset-top, 0px)); }
    .em-inner { height: 92dvh; }
    .hm-inner { height: 88dvh; }
  }

  /* Reaching the end of a modal's scroll handed the gesture to the page behind
     it, which on iOS shows as the whole sheet rubber-banding. */
  .pm-body, .sm-body, .hm-list, .hm-body-text { overscroll-behavior: contain; }

  /* Touch targets. Measured before this: the close button was 13x22 on three of
     the four sheets, the post footer's actions 22px tall, Save/Cancel 29px.
     44px is the floor; negative margins keep each glyph optically where it was
     so only the hit area grows. */
  /* `.sm-close` only — the post sheet hides its "x" and dismisses via "back",
     and a blanket `display: inline-flex` here silently un-hid it, dropping a
     stray glyph into the middle of that header. */
  .sm-close {
    min-width: 44px; min-height: 44px;
    display: inline-flex; align-items: center; justify-content: center;
    margin-top: -10px; margin-right: -12px; margin-left: auto;
  }
  .pm-footer button,
  #editModal .edit-actions button,
  .btn-attach {
    min-height: 44px; display: inline-flex; align-items: center; justify-content: center;
  }
  /* Delete sits next to Edit; a wider gap is cheap insurance now that both are
     comfortable to hit. */
  .pm-footer, #editModal .edit-actions { gap: 12px; }
  .sm-body { padding-bottom: calc(18px + env(safe-area-inset-bottom, 0px)); }
  #editModal .edit-actions {
    padding-bottom: calc(12px + env(safe-area-inset-bottom, 0px));
    margin-bottom: calc(-18px - env(safe-area-inset-bottom, 0px));
  }

  /* Roomier fields, and the 44px floor. Font size is deliberately not set
     here: `@media (hover: none)` already forces 16px on every input to stop
     iOS zooming the page on focus, and that keys on the device rather than on
     a narrow window, which is the better test. */
  .edit-form input, .edit-form textarea, .edit-form select { padding: 10px 12px; }
  .edit-form input, .edit-form select { min-height: 44px; }

  /* The attachment list's delete × was 17x15 — the touch floor never reached it
     because it only exists when the post *has* an attachment, and no test had
     ever created one. It surfaced the moment a fixture did. Negative margins
     grow the hit area without stretching the row, the same shape the sidebar's
     tag controls use. */
  .ef-attach-del {
    min-width: 44px; min-height: 44px;
    display: inline-flex; align-items: center; justify-content: center;
    margin: -12px -8px -12px 0;
  }

  /* List-mode cards keep the single-row header (title + tags side by side)
     at any width otherwise, so a handful of tags squeeze the title down to a
     hard ellipsis of a couple characters. Grid mode already stacks the header
     unconditionally (`.feed.grid .post-header`, above) — give list mode the
     same treatment, but only on phones, where the row is too narrow to share. */
  .post-header { flex-direction: column; align-items: stretch; gap: 6px; }
  .post-title {
    white-space: normal; overflow: hidden; text-overflow: clip;
    display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  }
}
