/*
 * SPDX-License-Identifier: GPL-3.0-or-later
 *
 * One set of values here comes from elsewhere: the dark palette is
 * Catppuccin Mocha (MIT). GPL-compatible; the notice travels with it in
 * THIRD-PARTY.md, which is the licence's one condition. The content-column
 * typography and spacing tokens are opensidian's own; their derivation is
 * in TOKENS.md.
 */
/*  MERGE UNION (goal/theme-mode <- main): this file had an add-vs-add
 *  conflict at its very top. main added the SPDX/licence header above;
 *  goal/theme-mode added the THEME TOKENS block below. BOTH are kept.
 *  The licence header goes first because it is a file header and
 *  scripts/add-license-headers.sh looks for it at the top; the token
 *  block follows because scripts/lint-colors.sh keys on its BEGIN/END
 *  markers by name, not by line number. Neither side lost a byte.
 *  (The R37 pane-header and R9.8 bookmark chrome rules further down
 *   this file came from main and merged WITHOUT conflict - they are
 *   in different regions - and they consume the tokens below.) */
/* ============================================================================
   THE COLOUR PALETTE. Everything below this block refers to colour only through
   var(--...): scripts/lint-colors.sh fails on a hex, an rgb / hsl / rgba
   colour anywhere else in ui/, and scripts/gate.sh runs the lint, so a literal
   reintroduced by a later feature fails the gate instead of a code review.
   The two marker comments below are what the lint keys on — do not duplicate
   them and do not move them apart to widen the allowance.
   ========================================================================== */
/* THE INVERSION (themeone item 5, DESIGN §2 "INVERT, do not extend").
   Three layers, and the direction of every arrow is the whole point:

     LAYER 1  Obsidian's theme names, on a ramp — the values live HERE
     LAYER 2  our names, each RESOLVED FROM the layer-1 name it corresponds to
     LAYER 3  ours alone: no Obsidian theme name exists for these

   WHY body AND NOT :root. T6 RESULT 3 found Obsidian's mode switch is a CLASS
   ON <body>, and T6b/C found that the SAME declaration on html changes
   nothing (a sentinel declaration paints on body, not on html).
   A theme therefore declares on body.theme-dark / body.theme-light.
   Custom-property substitution happens on the element that CARRIES the
   declaration, so a chain written at :root would substitute :root's own value
   and could never see the theme's — inheriting the RESULT, not the lookup.
   Written on body, `var(--background-primary)` reads whatever won the cascade
   ON BODY: the theme's declaration when it has one, ours when it does not.
   That one sentence is why the palette blocks below this line could go.

   THE DEFINED FALLBACK (R4: never empty, never a transparent pixel) is the
   LITERAL at the end of every layer-1 chain — layer 1 is declared
   unconditionally, so a layer-2 var() can never fall through to nothing. The
   failure direction is "our default colour", never the guaranteed-invalid
   value that makes a background transparent.

   MODE. Layer 2 is mode-INDEPENDENT (an alias resolves per element, so it
   follows whatever layer 1 says in this mode); only layers 1 and 3 have a
   light variant. The old cascade hazard — "add a token to one palette variant
   and not the other" — is gone with the palette blocks that caused it.
   The light selector is body.theme-light, the SAME specificity (0,1,1) Obsidian
   themes use, so a theme's own light rules tie with ours and win on order
   (#vault-theme is injected into <head> after this stylesheet, main.js
   themeInject). The old :root[data-theme="light"] was (0,2,0) and OUT-RANKED
   every Obsidian-shaped light rule a theme could write — that shadowing is what
   main.js:4698 warned about, and it ends here. */
/* THEME TOKENS BEGIN */
body {
  /* ======================= LAYER 1 — OBSIDIAN'S THEME NAMES ===========
     The theme format has a TWO-layer palette (T6b): a low-level --color-* ramp and
     semantic names derived from it, and a real third-party theme commonly
     enters at the ramp and never spells the semantic name. So each row below
     is `var(<ramp name>, <our literal>)` where the ramp pairing is EVIDENCED,
     and a bare literal where it is not. A half-invented ramp is worse than
     none: it hands a surface to a name we never measured.
     Evidence key — T6b = docs/recon-themes/logs/t6b-cascade.log, where the
     ramp row produced the SAME pixel count as the semantic name it feeds;
     R15.14 = docs/requirements.md, the derivation as recorded there;
     T9 = docs/recon-themes/README.md (the graph canvas). */
  --background-primary: var(--color-base-00, #1c1c1c);        /* T6b: base-00 = --background-primary; value: themelight audit */
  --background-primary-alt: var(--color-base-10, #232323);    /* T6b: base-10 = the code/alt surface */
  --background-secondary: #282828;                            /* no ramp row measured; value: themelight audit */
  --background-secondary-alt: #313244;                        /* no ramp row measured */
  --ribbon-background: #282828;                               /* no ramp row measured; value: themelight audit */
  --background-modifier-border: var(--color-base-30, #45475a);/* T6b: base-30 = --divider-color/--background-modifier-border */
  --indentation-guide-color: rgba(255, 255, 255, 0.12);   /* listlinks REQ-9: a faint guide on the note surface */
  --text-normal: var(--color-base-100, #dadada);              /* R15.14: --text-normal: var(--color-base-100) */
  --text-muted: var(--color-base-70, #b3b3b3);                /* R15.14 */
  --text-faint: var(--color-base-50, #666666);                /* R15.14 */
  --text-error: var(--color-red, #f5c2c7);                    /* T6b: color-red = --text-error */
  /* ---- THE ACCENT CHAIN (goal themematch). Obsidian themes derive every
     accent surface from ONE triple, --accent-h/-s/-l, through --color-accent
     and two shifted steps. The step factors below are FITTED in the
     themelight audit (docs/themelight/before, default theme), not read from
     any bundle: step 1 paints link/tag ink (dark) and link hover (light),
     step 2 paints link hover (dark); the triple itself paints toggles and
     buttons (dark) and link/tag ink (light).
     A theme that sets only the triple (Minimal does, on body) therefore moves
     links, tags, toggles, buttons, checkboxes and selection together, as in
     Obsidian. The old row was a hardcoded blue link colour: the operator's
     "accent colours are off". */
  --accent-h: 258;
  --accent-s: 88%;
  --accent-l: 66%;
  --color-accent: hsl(var(--accent-h), var(--accent-s), var(--accent-l));
  --color-accent-1: hsl(calc(var(--accent-h) - 3), calc(var(--accent-s) * 1.02), calc(var(--accent-l) * 1.15));
  --color-accent-2: hsl(calc(var(--accent-h) - 5), calc(var(--accent-s) * 1.05), calc(var(--accent-l) * 1.29));
  --text-accent: var(--color-accent-1);
  --text-accent-hover: var(--color-accent-2);
  --interactive-accent: var(--color-accent);
  /* selection: --interactive-accent at alpha .33 (dark) / .2 (light), fitted in
     the themelight audit over 4 themes — Wasp/1984 declare only
     --interactive-accent and Obsidian still tints selection with it, so the
     triple is the wrong source. tag pill: the triple at .1 */
  --text-selection: color-mix(in srgb, var(--interactive-accent) 33%, transparent);
  --tag-background: hsla(var(--accent-h), var(--accent-s), var(--accent-l), .1);
  /* ---- chrome surfaces under Obsidian's theme names (themelight audit):
     the active tree row and the selected palette row are the SAME white veil;
     the active tab = the note surface; file tree ink = --text-muted. */
  --background-modifier-hover: rgba(255, 255, 255, .067);
  --background-modifier-border-focus: #555555;
  --nav-item-color: var(--text-muted);
  --nav-item-color-active: var(--text-normal);
  --nav-item-background-active: var(--background-modifier-hover);
  --tab-container-background: #333333;
  --tab-background-active: var(--background-primary);
  --text-highlight-bg: rgba(249,226,175,.35);
  /* ============ LAYER 2 — OUR NAMES, RESOLVED FROM LAYER 1 ================
     One definition per surface. These rows carry NO colour: the value is
     wherever the layer-1 name landed on this element. Pairings are the ones the
     item-3 generator emitted into the built-in assets (scripts/gen-builtin-
     themes.sh, "THE MAPPING TABLE"), which is the same table read backwards.
     THREE PAIRS COLLAPSED HERE, and the default's pixels moved because of it:
       --text-chrome        #cdd6f4 -> #dadada  (= --text-normal)
       --text-chrome-muted  #a6adc8 -> #b3b3b3  (= --text-muted)
       --bg-button-primary  #7c6cf0 -> hsl(258,88%,66%) (= --interactive-accent)
     All three built-ins (1984, Slate, Wasp) already give the two names of each
     pair ONE value — only the default had drifted apart, which is exactly the
     second definition this goal deletes. The alternative (leave chrome ink on
     its own literal) keeps a byte-identical default and breaks the thing the
     operator asked for: a third-party theme that repaints the background but
     cannot reach the ink paints unreadable chrome. --text-chrome-faint is NOT
     collapsed onto --text-faint: 1984 gives them different values (#8184b8 vs
     #6b6ea3), so they are two surfaces and the data refuses the row. */
  --bg-base: var(--background-primary);
  --bg-sidebar: var(--background-secondary);
  --bg-ribbon: var(--ribbon-background);
  --bg-surface: var(--background-secondary-alt);
  --border: var(--background-modifier-border);
  --text-chrome: var(--text-normal);
  --text-chrome-muted: var(--text-muted);
  --bg-button-primary: var(--interactive-accent);
  --text-banner-error: var(--text-error);
  --accent-blue: var(--text-accent);
  /* THE GRAPH CANVAS (2D fill + WebGL clear). MEASURED, not chosen: T9
     (docs/recon-themes/README.md, "which theme name paints the GRAPH VIEW
     canvas background") found Obsidian's graph painted with --background-primary
     in BOTH graphs and BOTH modes, each filling its pane — while --graph-bg
     itself moved nothing there, i.e. it is OUR name
     and no theme will ever spell it. So the row belongs HERE, in layer 2, and
     not in layer 3 where item 5 parked it: one hop to the layer-1 name, and
     because --background-primary is itself var(--color-base-00, ...), a theme
     that enters at the RAMP and never spells a semantic name reaches the
     canvas too. That is the Solarized-class entry point T9 measured, and it
     is what an alias TABLE could not have carried (DESIGN §3 cause 2 —
     "dissolves rather than being fixed row by row").
     THE DEFAULT'S GRAPH PIXEL MOVES because of this row, and that is the
     point, not a side effect: dark #11111b -> #1e1e2e, light #f6f6f6 ->
     #ffffff. It was --bg-ribbon (the pane colour BEHIND the canvas), which
     made the untuned default look right by painting the graph the colour of
     the hole it sits in — and made every theme wrong, because a theme that
     repaints the editor background does not repaint our ribbon. */
  --graph-bg: var(--background-primary);
  --graph-node-tag: var(--accent-green);         /* graphctx GC8: tag nodes (stock #08B94E) */
  --graph-node-attachment: var(--accent-yellow); /* graphctx GC9: attachment nodes */
  --accent-red: var(--color-red, #f38ba8);   /* ramp only: the theme format's --color-red IS this surface (T6b), and no semantic name of ours sits between */
  --tag-bg: var(--tag-background);
  --highlight-yellow: var(--text-highlight-bg);
  /* ---- R15.10-13 CONTENT COLOURS, MOVED HERE FROM :root (typo regression) --
     These seven rows derive a content-area colour from a layer-1 theme name,
     so they are layer 2 by definition and they have to be DECLARED where
     layer 1 is. They used to sit in the R15 typography block at `:root`
     (html) with the rest of that block, which was correct while layer 1 also
     lived at `:root` — item 5 moved layer 1 onto `body` (an Obsidian theme
     declares on body.theme-dark / body.theme-light, T6 RESULT 3), and a
     `var()` naming a property the element does not have resolves to the
     GUARANTEED-INVALID value, not to a fallback that was never written. So
     every one of them computed to nothing on html and inherited that nothing
     down: `background-color: var(--code-background)` and
     `border-inline-start: 2px solid var(--blockquote-border-color)` are then
     INVALID AT COMPUTED-VALUE TIME and drop out entirely. That is what the
     gate measured: CODE_H=1 (no code box to trim at all) and
     BQ_BORDER_W / LP_BQ_TEXT_X empty (no blockquote border to find).
     The metric half of the R15 block (sizes, weights, indents, radii) names
     no colour and stays at `:root`, where the fonts and the layout numbers
     belong. Splitting on "does this row reference a layer-1 name" is the
     whole rule, and it is checkable by grepping that block for `var(--text-`
     / `var(--background-` / `var(--interactive-`. */
  --list-marker-color: var(--text-chrome-muted);
  --checkbox-border-color: var(--text-chrome-muted);
  --checkbox-color: var(--bg-button-primary);
  --checkbox-marker-color: var(--bg-base);
  --checklist-done-color: var(--text-chrome-faint);
  --code-background: var(--bg-surface);
  --blockquote-border-color: var(--text-accent);
  /* ==================== LAYER 3 — OURS ALONE =============================
     An editor has surfaces the theme recon never showed painting through any
     name we probed (a ribbon ink, an elevated popover, a faint rule, the
     scrims and shadows, the bookmark drop feedback). They keep OUR names and
     OUR values, which is legal for any theme file to declare — the built-in
     assets declare every one of them, and a third-party theme may too. */
  --bg-elevated: var(--modal-background, var(--background-primary));   /* palette card = the note surface (themelight audit) */
  --bg-banner-error: #45283c;
  --bg-button-plain: #e0e0e0;
  --bg-button-default: #fff;   /* the focused (default) answer in the rename/delete prompts. Ours, not --text-on-accent: a theme with a light accent sets that one DARK (AnuPpuccin: its base), which painted the default button in the label colour */
  --border-strong: #585b70;
  --border-focus-ring: #bdbdbd;
  --rule-faint: #31324499;
  --text-chrome-alt: #cdd6f5;
  --text-chrome-faint: #7f849c;
  --text-chrome-dim: #6c7086;
  --text-on-accent: #fff;
  --text-quiet: #bac2de;
  --link-unresolved: #7c86c8;
  --link-unresolved-underline: color-mix(in srgb, currentColor 30%, transparent);   /* listlinks REQ-15: underline = link colour at 0.3 alpha */
  --accent-mauve: var(--color-accent, #cba6f7);
  --accent-yellow: #f9e2af;
  --accent-green: #a6e3a1;
  --scrim: var(--background-modifier-cover, rgba(0,0,0,.35));
  --scrim-strong: var(--background-modifier-cover, rgba(0,0,0,.45));
  --shadow-popover: rgba(0,0,0,.55);
  --shadow-modal: rgba(0,0,0,.5);
  --shadow-drop: #0008;
  --accent-blue-veil: rgba(137,180,250,.22);
  /* ---- bmdrag drop feedback: values MEASURED in the bmdrag recon (docs/recon-bmdrag) —
     the gap indicator line (cases 1/3), the onto-a-group row fill + its ink
     (case 2), and the dragged row's own fill (case 1). ---- */
  --bm-drop-line: #9873F7;
  --bm-drop-into-bg: #ECE9F6;
  --bm-drop-into-fg: #3f3f3f;
  --bm-drop-src-bg: rgba(152,115,247,.28);
  --tag-bg-hover: rgba(137,180,250,.3);
  --transparent: transparent;
  /* minimalmargin: the note scroller's 12px scrollbar (.lp/.preview). Sampled from
     stock 1.13.7 Default dark, docs/recon-minimalmargin/stock/stock-default-dark-rlwoff-read-900.png
     x=889 y=400 (thumb) / x=884 (track = page). Obsidian's names: themes override them. */
  --scrollbar-bg: transparent;
  --scrollbar-thumb-bg: #323233;
}
/* ---------------------------------------------------------------------------
   LIGHT. Every value below was SAMPLED from a pixel of a running Obsidian
   instance in its light theme — shot file + x,y per value in
   notes/theme/provenance.txt, sampler and shots in notes/theme/light-sample/.
   Nothing here was invented, eyeballed or read out of a bundle.
   A token absent from this block KEEPS its value above on purpose: alpha
   scrims/shadows composite correctly over a light surface, and an unchanged
   value is not a new colour, so it needs no provenance.
   LAYER 2 IS ABSENT FROM THIS BLOCK BY CONSTRUCTION, not by omission: an alias
   resolves against whatever layer 1 says on this element, so re-declaring it
   per mode could only introduce a disagreement.
   --------------------------------------------------------------------------- */
/* body:where(.theme-light) = (0,0,1), NOT body.theme-light (0,1,1): Obsidian
   ships its light palette at .theme-light and a theme wins over it ON ORDER. Minimal 9.0.2
   declares at .theme-light, so body.theme-light out-ranked every light row it
   wrote (themelight BEFORE: Minimal's light text and links lost to ours).
   Ours now ties the dark block (body, 0,0,1) and wins it on order; every
   theme rule (.theme-light, body.theme-light, or body after this sheet)
   beats it. :where() keeps the lint-colors selector shape (^body). */
body:where(.theme-light) {
  /* ---- layer 1: Obsidian's theme names ---- */
  --background-primary: var(--color-base-00, #ffffff);
  --background-primary-alt: var(--color-base-10, #fafafa);
  --background-secondary: #f6f6f6;
  --background-secondary-alt: #e5e5e5;
  --ribbon-background: #f6f6f6;
  --background-modifier-border: var(--color-base-30, #e4e4e4);
  --indentation-guide-color: rgba(0, 0, 0, 0.12);         /* listlinks REQ-9: a faint guide on the note surface */
  --text-normal: var(--color-base-100, #222222);
  --text-muted: var(--color-base-70, #5c5c5c);
  --text-faint: var(--color-base-50, #ababab);
  --text-error: var(--color-red, #e93147);
  --text-highlight-bg: #ffec99;
  /* the accent chain in light (factors fitted in the themelight audit, see
     the dark block): ink = the triple, fills = step 1, hover = step 2 */
  --color-accent-1: hsl(calc(var(--accent-h) - 1), calc(var(--accent-s) * 1.01), calc(var(--accent-l) * 1.075));
  --color-accent-2: hsl(calc(var(--accent-h) - 3), calc(var(--accent-s) * 1.02), calc(var(--accent-l) * 1.15));
  --text-accent: var(--color-accent);
  --interactive-accent: var(--color-accent-1);
  /* light selection = --interactive-accent at .2 (Wasp and 1984 agree); the
     tree row and palette row hover = black at .067 */
  --text-selection: color-mix(in srgb, var(--interactive-accent) 20%, transparent);
  /* light search/filter field paper: white even on a tinted page (Wasp light) */
  --background-modifier-form-field: #ffffff;
  --background-modifier-hover: rgba(0, 0, 0, .067);
  --background-modifier-border-focus: #bdbdbd;
  --tab-container-background: #fcfcfc;
  /* ---- layer 2 in light: only the one row whose fallback is mode-specific ---- */
  --accent-red: var(--color-red, #e93147);
  /* ---- layer 3: ours alone ---- */
  --bg-banner-error: #fdeaec;
  --border-strong: #ababab;
  --rule-faint: #eeeeee;
  --text-chrome-alt: #222222;
  --text-chrome-faint: #ababab;
  --text-chrome-dim: #b6b6b6;
  --text-quiet: #5c5c5c;
  --link-unresolved: #ad8cf8;
  --accent-mauve: var(--color-accent, #8a5cf5);
  --accent-yellow: #ec7500;
  /* minimalmargin: scrollbar thumb, stock-default-light-rlwoff-read-900.png x=889 y=400 */
  --scrollbar-thumb-bg: #e5e5e5;
  --accent-green: #08b94e;
}
/* THEME TOKENS END */
* { box-sizing: border-box; margin: 0 }
/* goal fontset REQ-9: Obsidian's font token chain, declared ON BODY because that is where
   the Settings rows write their inline --font-<kind>-override (a chain on :root
   would resolve before it ever saw them). Unset overrides are the "??" family no
   font matches, as in Obsidian, so each chain falls through to its default stack;
   chrome keeps its old system-ui stack, text and code their R15 stacks. */
body { --font-interface-override: "??"; --font-text-override: "??"; --font-monospace-override: "??";
       --font-interface: var(--font-interface-override), system-ui, sans-serif;
       --font-text: var(--font-text-override), var(--font-default);
       --font-monospace: var(--font-monospace-override), var(--font-monospace-default) }
/* goal noscroll: the DOCUMENT never scrolls. body used to be a static height:100vh box, so
   any outer box a theme or vault snippet put on it (body{margin:6px}, html{padding:4px})
   made the document 100vh+extra tall and the window grew a root scrollbar (cause.md).
   body is now pinned to the viewport (fixed, inset 0, height auto): a snippet margin
   SHRINKS it inside the window instead of pushing past it, and html padding no longer
   adds to it (fixed is out of html's flow). The frame border stays inside (border-box).
   overflow:hidden on both is the guard behind that, not the fix.
   goal graphperf: ABSOLUTE, not fixed. With html non-positioned and never scrolling, an
   absolute inset:0 body sits on the initial containing block = the viewport, so the noscroll
   geometry is identical. A FIXED body, though, made WebKitGTK re-rasterize the whole page in
   llvmpipe on every caret blink: idle CPU after typing 470-570 -> 760-860 ms/10s on testvault
   (gidle2: 3a564b00^ vs 3a564b00; gidle3: HEAD body fixed vs absolute) — notes/cause.md.
   goal graphperf (big vault): ABSOLUTE still cost it — idle CPU after typing on the big fixture,
   WebKitWebProcess 760-850 (v0.6.1, 88cb11d9) -> 1000-1160 ms/10s (3a564b00 fixed, 02be43fa
   absolute, fc780bc7), and a static body drops it back (gidleB1/gidleB2). So body is STATIC
   again, as in v0.6.1, and html sizes it instead: html is the viewport (height 100%,
   border-box, so html padding/border come out of it) and a one-cell grid, whose item (body)
   stretches to the cell MINUS its own margin. A snippet margin or html padding shrinks body
   inside the window, the document never grows past it: the same noscroll geometry with no
   positioned body. min-width/height 0 keep body's content from forcing the cell wider/taller. */
/* goal graphperf4: html is a one-item FLEX column, not a grid. As a grid (c1e569ce) every root
   layout (a note tab switch) ran WebKit's grid track sizing over body first — implicit 'auto'
   tracks take body's max-content contribution (testvault tabsw_note_ms N=10: fc780bc7 8.5 ->
   c1e569ce 10.25 ms, v0.6.1 7.0 — tabswN), and explicit minmax(0,1fr) tracks (7e7aa2ab) only
   got back 1 ms (9.25 vs v0.6.1 6.25 — tabswF). A flex item with flex-basis 0 and min-height 0
   is sized from the container alone: main size = html's height minus body's margins (grow 1),
   cross size = stretch minus margins. Same stretch-minus-margin noscroll geometry, no grid. */
html { overflow: hidden; height: 100%; display: flex; flex-direction: column }
body { overflow: hidden; flex: 1 1 0; min-width: 0; min-height: 0 }
body { display: flex; font: 14px/1.5 var(--font-interface);
       /* the wasp ring: a THEME that declares --frame-width/--frame-color paints it here; every other theme resolves to 0 (colour irrelevant at 0 width) — docs/palette-wasp/SOURCE.md "The frame" */
       border: var(--frame-width, 0) solid var(--frame-color, currentColor);
       background: var(--bg-base); color: var(--text-chrome) }
/* R9.1 ribbon: fixed 44px icon strip, survives sidebar collapse */
#ribbon { width: 44px; background: var(--bg-ribbon); flex-shrink: 0; display: flex;
          flex-direction: column; align-items: center; gap: 8px;
          padding: 10px 0 }
#ribbon button { background: none; color: var(--text-chrome-muted); border: 0; padding: 5px;
                 cursor: pointer; border-radius: 4px }
#ribbon button:hover { background: var(--bg-surface); color: var(--text-chrome) }
#ribbon button svg { width: 18px; height: 18px; display: block }
/* R22: the sidebars keep their nominal width but SHRINK (never past 40% of the
   window each) instead of forcing the body wider than the viewport — 44 + 156
   + 260 = 460px of fixed chrome does not fit a 320px window. overflow:hidden
   is the backstop: a clipped icon strip beats a window scrollbar. */
#side { width: 156px; flex: 0 1 auto; min-width: 0; max-width: 40%;
        overflow: hidden; background: var(--bg-sidebar);
        display: flex; flex-direction: column }
/* R9.2 sidebar tab strip: Files / Search / Bookmarks */
#stabs { display: flex; flex-wrap: wrap; gap: 2px; padding: 6px 6px 2px; overflow: hidden }   /* R22: wrap, then clip — never push the sidebar wider */
.stab { flex: 1; min-width: 0; overflow: hidden; background: none; color: var(--text-chrome-muted);
        border: 0; padding: 4px; cursor: pointer; border-radius: 4px }   /* R22: min-width:0 = shrinkable icon button */
.stab:hover { color: var(--text-chrome) }
.stab.active { background: var(--bg-surface); color: var(--text-chrome) }
.stab svg { width: 15px; height: 15px; display: block; margin: 0 auto }
.spane { flex: 1; display: flex; flex-direction: column; min-height: 0 }
.spane[hidden] { display: none }
#side[hidden] { display: none }   /* R9.5: collapsed left sidebar (ribbon stays) */
/* ux-4: left resize divider — overlaps #side's last 6px (negative margin,
   net 0 layout width) so #main keeps its x=200 edge for the smoke coords */
#ldiv { flex: 0 0 6px; margin-left: -6px; z-index: 3; cursor: col-resize }
#ldiv:hover, #ldiv:active { background: var(--border-strong) }
#ldiv[hidden] { display: none }
/* collapseall R3: #bar is the explorer header; .hslot = a RESERVED header slot
   (sort / auto-reveal — out of scope, spec R3): it takes a button's width and
   paints nothing, so the toggle sits in Obsidian's slot ORDER. #bmbar shares the
   base rule and is restyled to stock below (bmcollbtn).
   #bar keeps its height (the tree's first row stays at y=82, R8). */
#bar, #bmbar { display: flex; gap: 4px; padding: 6px; flex-shrink: 0 }
.hslot { flex: 1; min-width: 0 }
#bar button, #bmbar button { flex: 1; min-width: 0; background: var(--bg-surface); color: var(--text-chrome); border: 0;
              padding: 4px; cursor: pointer; border-radius: 4px }
/* bmcollbtn: the bookmarks header is stock's ONE row of 4 visible icon buttons
   (notes/stock.md): compact (28 px wide), 32 px pitch, centred as a group, bare
   glyph at rest, a rounded hover square. Row height unchanged (24 px buttons in
   6 px padding), so #bmlist's first row does not move. */
#bmbar { justify-content: center }
#bmbar button { flex: 0 0 28px; padding: 4px 6px; background: none; color: var(--text-chrome-muted) }
#bmbar button:hover { background: var(--bg-surface); color: var(--text-chrome) }
#bmbar button[aria-disabled="true"] { opacity: .45; cursor: default }
#bmbar button[aria-disabled="true"]:hover { background: none; color: var(--text-chrome-muted) }
#fnew { padding: 2px 6px }
#fname { width: 100%; background: var(--bg-sidebar); color: var(--text-chrome);
         border: 1px solid var(--border); border-radius: 4px; padding: 4px 6px;
         outline: none; font-size: 12px }
#tree { flex: 1; overflow-y: auto }
/* R9.3 search pane */
#sbar { display: flex; gap: 4px; padding: 6px }
#sinput { flex: 1; min-width: 0; background: var(--bg-sidebar); color: var(--text-chrome);
          border: 1px solid var(--border); border-radius: 4px; padding: 4px 6px;
          outline: none; font-size: 12px }
#sinput:focus { border-color: var(--accent-blue) }
#sclear { background: none; color: var(--text-chrome-muted); border: 0; cursor: pointer;
          padding: 0 4px; border-radius: 4px }
#sclear:hover { color: var(--text-chrome); background: var(--bg-surface) }
#sresults { flex: 1; overflow-y: auto; font-size: 12px }
.sgroup { padding: 5px 8px 2px; color: var(--text-chrome); font-weight: 600; font-size: var(--sidebar-font-size);   /* sidefont: file rows = Q1; .shit snippets keep 12px */
          cursor: pointer; white-space: nowrap; overflow: hidden;
          text-overflow: ellipsis }
.sgroup:hover { background: var(--bg-surface) }
.sgroup .scount { color: var(--text-chrome-dim); font-weight: 400; margin-left: 4px }
.shit { padding: 2px 8px 2px 16px; color: var(--text-chrome-muted); cursor: pointer;
        white-space: nowrap; overflow: hidden; text-overflow: ellipsis }
.shit:hover { background: var(--bg-surface) }
.shit mark { background: none; color: var(--accent-yellow) }
.sempty { padding: 8px; color: var(--text-chrome-dim) }
/* bmfolder: `flex: 1` is LOAD-BEARING, not tidiness. .spane is a flex column, so
   a content-sized #bmlist ends at its last row and the area below the rows is the
   PANE, not the list — and bmPaneMenu (the pane-BACKGROUND "New group" of
   02-emptymenu.png) only fires for e.target === #bmlist. Without this the one
   route that creates a top-level group when the list is not empty is unreachable
   by a pointer, while every unit test and every model assertion stays green. */
#bmlist { flex: 1; overflow-y: auto }
.bmrow { padding: 5px 12px; cursor: pointer; white-space: nowrap;
  overflow: hidden; text-overflow: ellipsis; color: var(--text-chrome);
  display: flex; align-items: center; gap: 6px; font-size: var(--sidebar-font-size) }
.bmrow .bmstar { color: var(--accent-yellow); display: flex }
.bmrow .bmstar svg { width: 13px; height: 13px }
/* bmfolder: a GROUP row. The chevron sits in the same icon slot as a file row's
   bookmark glyph, so a group and a file at the same depth put their labels at
   the same x, as in Obsidian (docs/recon-bmfolder). The DEPTH
   is painted as padding-left on the row itself (renderBm: 12px + 17px/level), so
   the content shifts while the row background still spans the whole pane. */
.bmrow .bmchev { color: var(--text-chrome-dim); display: flex }
.bmrow .bmchev svg { width: 13px; height: 13px }
/* collapseall R1: a collapsed group's chevron points RIGHT (the svg is drawn
   pointing down), and every row inside it is kept in the DOM but not painted. */
.bmrow.bmfold .bmchev svg { transform: rotate(-90deg) }
.bmrow.bmhide { display: none }
.bmrow.bmgrp { font-weight: 500 }
.bmrow .bmren { flex: 1; min-width: 0; background: var(--bg-base); color: var(--text-chrome);
                border: 1px solid var(--border-focus-ring); border-radius: 3px;
                font: inherit; padding: 0 4px; outline: none }
.bmrow:hover { background: var(--bg-surface) }
/* bmdrag drop feedback — geometry as MEASURED in the bmdrag recon
   (docs/recon-bmdrag/README): the between-rows/background indicator is a 3px line spanning the
   pane (cases 1/3 — Obsidian's own render wobbles a pixel on subpixel offset, so
   phases assert colour+position, never AA), onto-a-group is a full-row fill
   with NO line (case 2), and the dragged row keeps its fill while the drag is
   live (case 1). Colours live in the theme-token block (lint-colors). AFTER
   .bmrow:hover on purpose: equal specificity, so source order keeps the drag
   fills over the hover fill. */
#bmdropline { position: fixed; height: 3px; background: var(--bm-drop-line); z-index: 98; pointer-events: none }
.bmrow.bmdrop-into, .bmrow.bmdrop-into:hover { background: var(--bm-drop-into-bg); color: var(--bm-drop-into-fg) }
.bmrow.bmdrop-src, .bmrow.bmdrop-src:hover { background: var(--bm-drop-src-bg) }
/* ux-2 Obsidian parity. HEIGHT MUST STAY 31px — smoke clicks rows at pitch 31 */
.trow { height: 31px; display: flex; align-items: center; cursor: pointer;
        margin: 0 8px; padding: 0 4px; border-radius: 5px;
        font-size: var(--sidebar-font-size); color: var(--nav-item-color) }   /* themematch: tree ink = --nav-item-color (muted), not body ink */
.trow:hover { background: var(--bg-surface) }
.trow.active { background: var(--nav-item-background-active); color: var(--nav-item-color-active) }
.tkids.collapsed { display: none }   /* R20: collapsed folder = its .tkids box hidden, rows stay built */
.trow .tn { flex: 1; min-width: 0; white-space: nowrap; overflow: hidden;
            text-overflow: ellipsis }
.trow .tg { flex: 0 0 14px; align-self: stretch; position: relative }
.trow .tg::before { content: ""; position: absolute; left: 7px; top: 0;
                    bottom: 0; width: 1px; background: var(--rule-faint) }
.trow .tc { flex: 0 0 16px; display: flex; align-items: center;
            justify-content: center; color: var(--text-chrome-dim) }
.trow .tc svg { width: 10px; height: 10px; transition: transform .1s }
.trow.folder.open .tc svg { transform: rotate(90deg) }
.trow .tfi { flex: 0 0 18px; display: flex; align-items: center;
             color: var(--text-chrome-muted) }
.trow .tfi svg { width: 14px; height: 14px }
.trow.folder { font-weight: 500 }
#vswitch { padding: 6px 12px; border-top: 1px solid var(--bg-surface); color: var(--text-chrome-muted);
           cursor: pointer; font-size: 12px; white-space: nowrap;
           overflow: hidden; text-overflow: ellipsis }
#vswitch:hover { background: var(--bg-surface) }
#main { flex: 1; display: flex; min-width: 0 }
/* R9.6 right sidebar: localgraph of the active note, resizable divider */
#rdiv { flex: 0 0 6px; background: var(--bg-ribbon); cursor: col-resize }
#rdiv:hover, #rdiv:active { background: var(--border-strong) }
#rdiv[hidden], #rside[hidden] { display: none }
#rside { width: 260px; flex: 0 1 auto; min-width: 0; max-width: 40%;   /* R22 */
         overflow: hidden; background: var(--bg-sidebar); display: flex;
         flex-direction: column }
/* rsidebar: icon strip + panes (Backlinks / Outgoing / Outline / Local graph) */
#rtabs { display: flex; flex-wrap: wrap; gap: 2px; padding: 6px 6px 2px 6px; overflow: hidden }   /* R22: wrap+clip. NO right padding reserve: R20 put #rtoggle IN FLOW as a flex child (margin-left:auto), so the old 30px inboard reserve only opened a dead gap and shifted the toggle 30px left of where R20's rside phase clicks it */
#rpane-graph { display: flex; position: relative }
#rpane-graph[hidden] { display: none }
.rhead { padding: 6px 10px 4px; font-size: 12px; font-weight: 600; color: var(--text-chrome) }
.rhead .scount { color: var(--text-chrome-dim); font-weight: 400; margin-left: 4px }
.rlist { flex: 1; overflow-y: auto; font-size: var(--sidebar-font-size) }   /* sidefont: backlinks/outgoing/outline rows = Q1 (docs/recon-sidefont Q3) */
.rempty { padding: 8px 10px; color: var(--text-chrome-dim) }
.blnote { padding: 4px 8px; color: var(--text-chrome); cursor: pointer; display: flex;
          align-items: center; gap: 4px; white-space: nowrap; overflow: hidden }
.blnote:hover { background: var(--bg-surface) }
.blnote .tc { flex: 0 0 12px; display: flex; color: var(--text-chrome-dim) }
.blnote .tc svg { width: 10px; height: 10px; transition: transform .1s }
.blnote.open .tc svg { transform: rotate(90deg) }
.blnote .bln { flex: 1; overflow: hidden; text-overflow: ellipsis }
.blnote .scount { color: var(--text-chrome-dim) }
.blline { padding: 2px 8px 2px 24px; color: var(--text-chrome-muted); cursor: pointer; font-size: 12px;   /* sidefont: context SNIPPET, smaller than Q1 (Q3) */
          white-space: nowrap; overflow: hidden; text-overflow: ellipsis }
.blline:hover { background: var(--bg-surface) }
.blline mark { background: none; color: var(--accent-yellow) }
.outrow { padding: 4px 10px; color: var(--text-chrome); cursor: pointer; white-space: nowrap;
          overflow: hidden; text-overflow: ellipsis }
.outrow:hover { background: var(--bg-surface) }
.outrow.unresolved { color: var(--text-chrome-dim); font-style: italic; cursor: default }
.outrow.unresolved::after { content: "unresolved"; margin-left: 6px; font-size: 10px;
                            color: var(--border-strong); font-style: normal }
.tocrow { display: flex; align-items: center; gap: 2px; padding: 3px 6px 3px 0;
          color: var(--text-chrome-muted); cursor: pointer; white-space: nowrap; overflow: hidden }
.tocrow:hover { background: var(--bg-surface) }
.tocrow.active { color: var(--text-chrome); background: var(--bg-surface) }
.tocrow .tc { flex: 0 0 14px; display: flex; justify-content: center; color: var(--text-chrome-dim) }
.tocrow .tc svg { width: 10px; height: 10px; transition: transform .1s }
.tocrow.open .tc svg { transform: rotate(90deg) }
.tocrow .tn { overflow: hidden; text-overflow: ellipsis }
.tockids { display: block }
.tockids[hidden] { display: none }
/* tags pane: rows share the outline tree look; #name left, count right */
.tagrow { display: flex; align-items: center; gap: 2px; padding: 3px 8px 3px 0;
          color: var(--text-chrome); cursor: pointer; white-space: nowrap; overflow: hidden }
.tagrow:hover { background: var(--bg-surface) }
.tagrow .tc { flex: 0 0 14px; display: flex; justify-content: center; color: var(--text-chrome-dim) }
.tagrow .tc svg { width: 10px; height: 10px; transition: transform .1s }
.tagrow.open .tc svg { transform: rotate(90deg) }
.tagrow .tn { flex: 1; overflow: hidden; text-overflow: ellipsis }
.tagrow .scount { color: var(--text-chrome-dim) }
/* R20 (#3): in flow — last item of #rtabs (sidebar open) or of the top-right
   pane's .tabbar after .modebtn (collapsed); placeRToggle() moves the node */
#rtoggle { margin-left: auto; align-self: center; flex-shrink: 0; background: none;
           border: 0; color: var(--text-chrome-muted); cursor: pointer; padding: 3px; margin-right: 6px;
           border-radius: 4px }
#rtoggle:hover { background: var(--bg-surface); color: var(--text-chrome) }
#rtoggle svg { width: 17px; height: 17px; display: block }
/* split tree (M6): .split nodes wrap .pane leaves; flex weights = fractions */
.split { display: flex; flex: 1 1 0; min-width: 0; min-height: 0 }
.split.row { flex-direction: row }
.split.col { flex-direction: column }
/* R6.6: draggable divider between sibling panes */
.divider { flex: 0 0 6px; background: var(--bg-ribbon) }
.divider:hover, .divider:active { background: var(--border-strong) }
.divider.row { cursor: col-resize }
.divider.col { cursor: row-resize }
.pane { flex: 1 1 0; display: flex; flex-direction: column;
        min-width: 0; min-height: 0; position: relative }
.pane:not(.focused) .tabbar { opacity: .7 }   /* dim inactive tab strips */
/* R22: the strip is a fixed-height row of shrinkable items — min-width:0 +
   overflow:hidden all the way down, so a pane squeezed to a few px clips the
   strip instead of pushing #main (and the window) wider than the viewport. */
.tabbar { display: flex; height: 32px; background: var(--tab-container-background); flex-shrink: 0;
          min-width: 0; overflow: hidden; user-select: none }   /* R18: a missed tab grab must not start a text-selection DnD (Obsidian: tab strip unselectable) */
.tabs { display: flex; flex: 1 1 0; min-width: 0; overflow: hidden }
/* R4X.1 (navbtn): Back/Forward at the header's left edge (docs/recon-navbtn §1—§3).
   Obsidian: enabled = dark glyph, disabled = light grey, hover paints a rounded
   light rect ONLY when enabled; the title tooltip shows in both states.
   R22: shrinkable + overflow:hidden so a squeezed pane clips, never overflows. */
.navbtns { display: flex; align-items: center; flex: 0 1 auto; min-width: 0;
           overflow: hidden; padding: 0 2px }
.navbtn { background: none; border: 0; color: var(--text-chrome); cursor: pointer;
          padding: 3px 5px; border-radius: 4px; display: flex; align-items: center }
.navbtn:disabled { color: var(--text-chrome-muted); cursor: default }
.navbtn:not(:disabled):hover { background: var(--border) }
.navbtn svg { width: 12px; height: 12px; display: block }
.modebtn { background: none; border: 0; color: var(--text-chrome-muted); cursor: pointer;
           padding: 0 10px; flex: 0 1 auto; min-width: 0; overflow: hidden }   /* R22: shrinkable, was flex-shrink:0 */
.modebtn:hover { color: var(--text-chrome) }
.modebtn svg { width: 16px; height: 16px; display: block }
/* graphhdr REQ-5: the ⋮ "More options" button — same muted glyph, right of the toggle */
.morebtn { background: none; border: 0; color: var(--text-chrome-muted); cursor: pointer;
           padding: 0 8px; flex: 0 1 auto; min-width: 0; overflow: hidden }
.morebtn:hover { color: var(--text-chrome) }
.morebtn svg { width: 16px; height: 16px; display: block }
.tab { width: 120px; min-width: 0; flex-shrink: 1; overflow: hidden; display: flex; align-items: center;
       gap: 6px; padding: 0 8px; background: var(--bg-surface); color: var(--text-chrome-muted);
       cursor: pointer; font-size: 12px; border-right: 2px solid var(--bg-ribbon) }   /* R22: overflow:hidden — the 16px close button + 16px padding cannot fit a 2px tab, so clip */
.tab.active { background: var(--tab-background-active); color: var(--text-chrome) }   /* themematch: active tab = the note surface */
.tab .t { flex: 1 1 auto; min-width: 0; overflow: hidden;
          text-overflow: ellipsis; white-space: nowrap }
/* ux-1: close button — flex-none so ellipsized titles can never crush it,
   inline svg cross (U+2715 was tofu under webkit2gtk's font set) */
.tab .x { flex: 0 0 auto; display: flex; align-items: center;
          justify-content: center; width: 16px; height: 16px;
          cursor: pointer; border-radius: 3px; color: var(--text-chrome-faint) }
.tab .x svg { width: 9px; height: 9px; display: block }
.tab .x:hover { background: var(--border-strong); color: var(--text-chrome) }
.tab.active .x { color: var(--text-chrome-muted) }
/* ux-5: tab drag — ghost chip under the cursor + drop-target highlights */
#tabghost { position: fixed; left: 0; top: 0; will-change: transform; z-index: 99; pointer-events: none;
            background: var(--border); color: var(--text-chrome); padding: 4px 12px;
            border-radius: 6px; font-size: 12px; opacity: .92;
            box-shadow: 0 4px 14px var(--shadow-popover) }
.tabs.drop-strip { outline: 2px solid var(--accent-blue); outline-offset: -2px }
/* R24.6: the explorer drop target is OUTLINED — the folder row the note lands
   in, or the whole tree when the destination is the vault root. Same accent
   and same inset as the tab strip's: one drop-target idiom, two surfaces. */
.trow.drop-into { outline: 2px solid var(--accent-blue); outline-offset: -2px;
                  background: var(--bg-surface) }
#tree.drop-into { outline: 2px solid var(--accent-blue); outline-offset: -2px }
.pane.drop-edge::after { content: ""; position: absolute; top: 32px; right: 0;
                         bottom: 0; width: 40px; pointer-events: none;
                         background: var(--accent-blue-veil) }
.content { flex: 1 1 0; display: flex; min-width: 0; min-height: 0; position: relative;
            container: notepane / inline-size }   /* R22: min-width:0. minimalmargin: the query container of the narrow-pane margin clamp (.lp/.preview padding) */
/* R8.8: one of .editor/.lp/.preview fills the pane (JS toggles display) —
   the old permanent editor|preview side-by-side layout is gone */
.editor { flex: 1 1 0; min-width: 0;   /* R22 */ background: var(--bg-base); color: var(--text-chrome); border: 0;
          padding: 16px; resize: none;
          font: 13px/1.6 monospace; outline: none }
.lp { flex: 1 1 0; min-width: 0; overflow-y: auto; overflow-x: hidden }   /* R22: min-width:0 + no h-scroll */   /* padding + type: R15 block below */
.lp a.wiki { color: var(--accent-blue) }
.lp a.wiki-unresolved { color: var(--link-unresolved); font-style: italic }
/* R8.2: one rendered row per line/block; the raw caret row blends in */
.lprow { min-height: 1.5em; cursor: text }
.lprow > :first-child { margin-top: 0 }
.lprow > :last-child { margin-bottom: 0 }
.lpraw { display: block; width: 100%; background: var(--bg-elevated); color: inherit;
         border: 0; border-radius: 3px; padding: 0 2px; resize: none;
         outline: none; font: inherit; line-height: inherit; overflow: hidden }
/* rvcursor: READING VIEW IS NOT EDITABLE, so the pointer must never be the
   I-beam here. The container declares the ordinary arrow (`default`) for all
   ordinary content — without this declaration the UA resolves the inherited
   `cursor: auto` against the hit test and hands back the text caret over
   selectable prose, which lies about what a click does. `default` is a cursor
   SHAPE only: selection is governed by `user-select`, which this change does
   not touch (proven by the drag-select phase). Declaring `default` here also
   makes every clickable's own `cursor: pointer` LOAD-BEARING, because the UA's
   "auto over a link = hand" fallback no longer applies to descendants — hence
   the explicit pointer on a.wiki below (a.ext, now :754, and a.tag :756/:758,
   already had one — the brief's :734/:736 were their numbers before this
   comment grew above them; a.wiki itself is :752).
   Live preview (.lp / .lprow, :725) keeps `cursor: text` and source mode
   is untouched: those views ARE editable. Divergence from Obsidian, which
   shows the I-beam over reading-view text, is recorded in
   docs/recon-rvcursor/; the operator ask is the requirement. */
.preview { flex: 1 1 0; min-width: 0; overflow-y: auto; overflow-x: hidden;
           cursor: default }   /* R22 */
/* modeswitch opt6: the view the edit <-> reading switch left stays LAID OUT (ui/main.js
   applyMode): out of flow over the same box, same width, never painted, never hit.
   opt8: while a view is kept BOTH views are out of flow over that box (.vk2), so a toggle
   changes no containing block, only which view hides.
   opt10: the kept view hides by being moved off the pane (transform, NOT inherited) instead of
   visibility:hidden + pointer-events:none — flipping an inherited property re-resolved the style
   of every descendant (o9 probe, 30k: ~550 ms forced style recalc per toggle). Off the pane it
   is neither painted nor hit; its layout and scroll offset stay. */
.content.vk2 > .lp, .content.vk2 > .preview { position: absolute; inset: 0 }
.content > .vkeep { transform: translateX(-200vw) }
/* one rule covers both wikilink states: the unresolved anchor carries
   `class="wiki wiki-unresolved"` (main.rs:1336-1343), never wiki-unresolved
   alone — :733 adds only colour + italic */
.preview a.wiki { color: var(--accent-blue); cursor: pointer }
.preview a.wiki-unresolved { color: var(--link-unresolved); font-style: italic }
.lp a.ext, .preview a.ext { color: var(--accent-blue); text-decoration: underline; cursor: pointer }  /* S1: external link, opens via xdg-open */
/* tags: inline #tag pills (small rounded accent-tinted chip) */
.lp a.tag, .preview a.tag { color: var(--tag-color, var(--accent-blue)); background: var(--tag-bg);
  border-radius: 999px; padding: 0 7px; font-size: .85em; text-decoration: none;
  cursor: pointer; white-space: nowrap }
.lp a.tag:hover, .preview a.tag:hover { background: var(--tag-bg-hover) }
.graph { flex: 1 1 0; min-width: 0; min-height: 0;   /* R22 */ background: var(--bg-ribbon); position: relative; z-index: 1 }   /* above .graphgl */
/* graph-webgl: WebGL canvas BEHIND .graph (nodes + edges); .graph goes transparent and keeps events + labels */
.graphgl { position: absolute; inset: 0; background: var(--bg-ribbon) }
.graphgl[hidden] { display: none }
.graph.gl-on { background: var(--transparent) }

/* [[ autocomplete */
.ac { position: absolute; z-index: 6; background: var(--bg-elevated);
     border: 1px solid var(--border); border-radius: 6px; min-width: 180px;
     max-height: 210px; overflow-y: auto; font-size: 13px; padding: 3px }
.ac[hidden] { display: none }
.ac div { padding: 4px 10px; cursor: pointer; white-space: nowrap;
          border-radius: 4px }
.ac div.sel, .ac div:hover { background: var(--border) }

/* status bar */
.status { position: absolute; right: 10px; bottom: 8px; display: flex;
          gap: 12px; background: var(--bg-sidebar); border: 1px solid var(--bg-surface);
          border-radius: 6px; padding: 3px 10px; font-size: 12px;
          /* R22: in a narrow pane the three counters wrap (bottom-anchored, so
             the box grows upward) and each one ellipsises rather than spilling
             out over the sidebar / past the window edge. */
          max-width: calc(100% - 20px); overflow: hidden;
          flex-wrap: wrap; justify-content: flex-end;
          color: var(--text-chrome-muted); z-index: 5; pointer-events: none }
.status > span { max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap }
.status[hidden] { display: none }

/* vault picker */
#picker { position: fixed; inset: 0; background: var(--bg-base); z-index: 10;
          display: flex; align-items: center; justify-content: center }
#picker[hidden] { display: none }
#pcard { width: 480px; max-width: calc(100% - 24px); background: var(--bg-elevated);   /* R22: never wider than the window */
         border-radius: 10px;
         padding: 28px; position: relative }
/* startscr: logo LEFT of the name, the pair centred as one row (operator 2026-10-10) */
#ptitle { display: flex; align-items: center; justify-content: center; gap: 12px;
          font-size: 26px; font-weight: 700; line-height: 36px;
          margin-bottom: 20px; letter-spacing: 0.5px }
#plogo { width: 36px; height: 36px; flex-shrink: 0; display: block }   /* ~2x the cap height */
#ptname { display: block; min-width: 0 }
/* startscr: #p-actions spans 10px into the card padding on both sides (padding gives the rows back
   the content edges) so #p-recent, which reaches into that band, never overflows its parent */
#p-actions { margin: 0 -10px; padding: 0 10px }
.prow { display: flex; align-items: center; gap: 12px; padding: 14px 0;
        border-top: 1px solid var(--bg-surface) }
.ptext { flex: 1; min-width: 0; display: flex; flex-direction: column; overflow-wrap: anywhere }   /* long text wraps, never squeezes the button */
.ptext span { color: var(--text-chrome-muted); font-size: 12px }
.prow button, #p-go, #p-gonew { background: var(--bg-button-primary); color: var(--text-on-accent); border: 0;
        border-radius: 5px; padding: 8px 6px; cursor: pointer }
/* startscr: every action button one fixed width -> right edges on one line, centred on the row */
.prow button { width: 100px; flex: 0 0 100px }
#p-btns button { width: 132px; flex: 0 0 132px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis }
#p-back { background: var(--bg-surface); color: var(--text-chrome); border: 0; border-radius: 5px;
          padding: 8px 6px; cursor: pointer }
#p-btns { display: flex; justify-content: flex-end; align-items: center; gap: 8px; margin-top: 8px }
#p-name, #p-path { width: 100%; background: var(--bg-sidebar); color: var(--text-chrome);
        border: 1px solid var(--border); border-radius: 5px; padding: 8px;
        margin: 6px 0; outline: none; font-size: 13px }
#p-dirs { list-style: none; height: 180px; overflow-y: auto;
          border: 1px solid var(--bg-surface); border-radius: 5px; margin: 6px 0 }
#p-dirs li { padding: 4px 10px; cursor: pointer; font-size: 13px }
#p-dirs li:hover { background: var(--bg-surface) }
#p-err { color: var(--accent-red); font-size: 12px; min-height: 18px }
/* R1.6 recent-vault rows: empty list collapses to just the top border */
/* startscr: the list box reaches 10px into the card padding so its rows' TEXT sits on the .prow text column */
#p-recent { list-style: none; margin: 0 -10px; padding: 5px 0 4px;
            /* the top rule stays on the card content edges, like the .prow rules */
            background: linear-gradient(var(--bg-surface), var(--bg-surface)) 10px 0 / calc(100% - 20px) 1px no-repeat;
            max-height: 150px; overflow-y: auto }
#p-recent li { padding: 6px 10px; cursor: pointer; border-radius: 5px; position: relative;
               display: flex; flex-direction: column; font-size: 13px }
#p-recent li:hover { background: var(--bg-surface) }
#p-recent li span { color: var(--text-chrome-muted); font-size: 11px }
/* vaultbleed: "open in new window" on a recent row (only in an open vault) */
#p-recent .p-newwin { position: absolute; right: 8px; top: 50%; transform: translateY(-50%);
            background: none; border: 0; color: var(--text-chrome-muted); cursor: pointer; font-size: 15px }
#p-recent .p-newwin:hover { color: var(--text-chrome) }
#p-close { position: absolute; top: 10px; right: 12px; background: none;
           border: 0; color: var(--text-chrome-muted); font-size: 15px; cursor: pointer }
#bar button svg, #bmbar button svg { width: 16px; height: 16px; display: block; margin: 0 auto }
#vswitch svg { width: 13px; height: 13px; vertical-align: -2px; margin-right: 6px }

/* tab context menu (M7 / R6.2): simple dark popup */
.ctxmenu { position: fixed; z-index: 60; background: var(--bg-base);
           border: 1px solid var(--border); border-radius: 6px; padding: 4px 0;
           min-width: 130px; box-shadow: 0 4px 14px var(--shadow-popover);
           /* R22: JS clamps the position to the MEASURED box (placeMenu);
              these cap a menu that is bigger than the whole window */
           max-width: 100vw; max-height: 100vh; overflow: auto }
.ctxmenu div { padding: 6px 14px; cursor: pointer; font-size: 13px;
               color: var(--text-chrome); white-space: nowrap }
.ctxmenu div:hover { background: var(--border) }
/* bmmenu (docs/recon-bmmenu/README.md): the bookmark-row menu has separators and
   rows this tree cannot back. .sep is a real child so the census reads "---" where
   Obsidian draws a rule; .dis is the settings rows' disabled treatment (R30 note in
   main.js) applied to a menu row: muted, inert,
   reason in the hover title. */
.ctxmenu div.sep { padding: 0; height: 1px; margin: 5px 0; background: var(--border); cursor: default; pointer-events: none }
.ctxmenu div.dis { color: var(--text-chrome-muted); cursor: default; pointer-events: none }   /* no :hover rule needed: pointer-events:none means the row never hovers (and lint-colors bans "transparent") */
/* R38 (docs/recon-tabmenu/README.md): the tab menu's radio rows. Obsidian has no
   `Live preview` row and no ✓ in any label, so R12.4's mark is an ELEMENT
   (span.chk) rather than text in the label — the rendered label set then equals
   Obsidian's character for character while the radio state stays visible, and
   [menu:] (textContent) reads Obsidian's wording. The gutter is reserved on EVERY
   row of these two so the labels do not shift when the mark moves. */
.ctxmenu div.chkrow { padding-left: 4px }
.ctxmenu .chk { display: inline-block; width: 12px; height: 12px; margin-right: 4px; vertical-align: -1px }
.ctxmenu .chk svg { width: 12px; height: 12px; display: block }
/* R4X.5 (navbtn): the history menu carries a file icon per row (recon-navbtn
   §4, 18-menu-crop.png) — an ELEMENT like .chk, so textContent and [menu:]
   stay the bare note labels. */
.ctxmenu .mi { display: inline-block; width: 12px; height: 12px; margin-right: 6px; vertical-align: -1px; color: var(--text-chrome-muted) }
.ctxmenu .mi svg { width: 12px; height: 12px; display: block }
/* R38.29: `Delete file` is the ONE non-default-coloured row in the menu (red, see docs/requirements.md R38.29) */
.ctxmenu div.del { color: var(--accent-red) }
/* graphctx GC29 (recon-graphctx §6.1): the graph node menu opens with the file basename as an inert label row (stock .menu-item.is-label) */
.ctxmenu div.mlbl { color: var(--text-chrome-muted); font-size: 12px; cursor: default; pointer-events: none }
/* R38.7: the pin glyph rides in FRONT of a pinned tab's label (shots/m3-pinned-tabbar.png) */
.tab .pin { flex: 0 0 auto; display: flex; align-items: center; color: var(--text-chrome-muted); margin-right: 3px }
.tab .pin svg { width: 11px; height: 11px; display: block }
/* "Reveal file in navigation": a focus ring on the explorer row, no selection (recon 10-reveal.png) */
.trow.revealed { outline: 2px solid var(--border-focus-ring); outline-offset: -2px }
/* goal graphparity: the global graph's Forces card (docs/recon-graphpanel). Open: 240 wide,
   12px in from the view's top-right corner; closed (.is-close): a gear column. R22: never
   wider/taller than the pane it floats in — the body scrolls instead. */
.graph-controls { position: absolute; top: 12px; right: 12px; z-index: 7; width: 240px;
  max-width: calc(100% - 24px); max-height: calc(100% - 24px); overflow-x: hidden; overflow-y: auto; box-sizing: border-box;
  background: var(--bg-elevated); border: 1px solid var(--border); border-radius: 8px;
  padding: 6px 0 8px; font-size: 13px; color: var(--text-chrome) }
.graph-controls[hidden] { display: none }
/* The card's shadow is switched on (.gf-lit, startGraph) once the sim first comes to rest, not
   with the card: the first paint of ANY box-shadow here stalls the main thread ~460 ms one-off on
   the box (WebKitGTK; no panel / no shadow: first motion 310 / 370 ms after ctrl+g, shadow
   0 4px 16px: 820, 0 2px 6px: 680, blur 0: 490, drop-shadow filter: 780, own layer: 780-880),
   so a card shadowed at open froze the sim's first 500 ms and S1 settle read +31..36% vs stock.
   Painted after rest, the same one-off stall lands on a still graph. */
.graph-controls.gf-lit { box-shadow: 0 4px 16px var(--shadow-drop) }
/* graphctx GC2: reset + close float in the FIRST section's header row (stock), not a row of their own */
.graph-controls .gf-btns { position: absolute; top: 6px; right: 6px; z-index: 1; display: flex; gap: 2px }
.graph-controls .gf-btn { width: 26px; height: 26px; padding: 5px; border: 0; border-radius: 4px; background: none;
  color: var(--text-muted); cursor: pointer; line-height: 0; box-sizing: border-box }
.graph-controls .gf-btn:hover { background: var(--border); color: var(--text-chrome) }
.graph-controls .gf-btn svg { width: 16px; height: 16px; display: block }
.graph-controls .mod-open { display: none }
.graph-controls.is-close { width: 44px; padding: 6px 0 }
.graph-controls.is-close .gf-btns, .graph-controls.is-close .graph-control-section { display: none }
.graph-controls.is-close .mod-open { display: block; width: 30px; margin: 0 auto }
.graph-controls .gf-head { display: flex; align-items: center; gap: 4px; padding: 4px 12px; cursor: pointer; user-select: none }
.graph-controls .collapse-icon { line-height: 0; color: var(--text-muted); transition: transform 0.1s; transform: rotate(90deg) }
.graph-controls .collapse-icon svg { width: 14px; height: 14px; display: block }
.graph-controls .graph-control-section.is-collapsed .collapse-icon { transform: none }
.graph-controls .graph-control-section-header { font-weight: 600 }
.graph-controls .graph-control-section.is-collapsed .gf-body { display: none }
.graph-controls:not(.is-local) .gc-local { display: none }   /* local-only Filters rows (Depth, link directions: R§5.1) */
.graph-controls.is-local .gc-global { display: none }   /* global-only Filters rows (Orphans: GC12) */
.graph-controls .mod-search-setting input { width: 100%; box-sizing: border-box; height: 28px; padding: 0 8px; font: inherit; font-size: 13px;
  color: var(--text-normal, inherit); background: var(--background-modifier-form-field, var(--bg-elevated)); border: 1px solid var(--border); border-radius: 4px }
.graph-controls .gf-body:empty { display: none }
.graph-controls .setting-item { padding: 6px 12px; min-width: 0 }
.graph-controls .setting-item-name { margin-bottom: 4px }
.graph-controls .gf-ctl { display: flex; align-items: center; gap: 8px; min-width: 0 }
.graph-controls .slider-value { flex: 0 0 auto; min-width: 28px; font-size: 12px; color: var(--text-muted); font-variant-numeric: tabular-nums }
/* R22: the range control paints its thumb past a 16px box — it clips its own internals */
.graph-controls input.slider { flex: 1 1 auto; min-width: 0; height: 16px; margin: 0; overflow: hidden; accent-color: var(--interactive-accent) }
/* graphctx: stock's toggle (div.checkbox-container, .is-enabled = on), right of the setting's name */
.graph-controls .mod-toggle { display: flex; align-items: center; justify-content: space-between; gap: 8px }
.graph-controls .mod-toggle .setting-item-name { margin-bottom: 0; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap }
.graph-controls .checkbox-container { flex: 0 0 auto; position: relative; width: 34px; height: 18px; border-radius: 9px;
  background: var(--border); cursor: pointer; transition: background 0.1s }
.graph-controls .checkbox-container::after { content: ""; position: absolute; top: 2px; left: 2px; width: 14px; height: 14px;
  border-radius: 50%; background: var(--text-chrome); transition: transform 0.1s }
.graph-controls .checkbox-container.is-enabled { background: var(--interactive-accent) }
.graph-controls .checkbox-container.is-enabled::after { transform: translateX(16px) }
/* graphctx Groups (R§3.1): one row per colour group — query input, round swatch, delete (x); "New group" below */
.graph-controls .graph-color-group { display: flex; align-items: center; gap: 6px; padding: 3px 12px }
.graph-controls .graph-color-group input[type=text] { flex: 1 1 auto; min-width: 0; height: 28px; padding: 0 8px; box-sizing: border-box; font: inherit; font-size: 13px;
  color: var(--text-normal, inherit); background: var(--background-modifier-form-field, var(--bg-elevated)); border: 1px solid var(--border); border-radius: 4px }
.graph-controls .graph-color-group input[type=color] { flex: 0 0 auto; width: 22px; height: 18px; padding: 0; border: 0; border-radius: 50%;
  background: none; cursor: grab; overflow: hidden }
.graph-controls .graph-color-group input[type=color]::-webkit-color-swatch-wrapper { padding: 0 }
.graph-controls .graph-color-group input[type=color]::-webkit-color-swatch { border: 0; border-radius: 50% }
.graph-controls .graph-color-group.gc-drag { opacity: 0.5 }
.graph-controls .graph-color-group input[type=color] { -webkit-user-drag: none; user-select: none }
.graph-controls .graph-color-group .clickable-icon { flex: 0 0 auto; width: 26px; height: 26px; padding: 5px; box-sizing: border-box; border-radius: 4px;
  color: var(--text-muted); cursor: pointer; line-height: 0 }
.graph-controls .graph-color-group .clickable-icon:hover { background: var(--border); color: var(--text-chrome) }
.graph-controls .graph-color-group .clickable-icon svg { width: 16px; height: 16px; display: block }
.graph-controls .graph-color-button-container { padding: 6px 12px }
.graph-controls .gc-newgroup { width: 100%; height: 30px; font: inherit; font-size: 13px; border: 0; border-radius: 4px; cursor: pointer;
  color: var(--text-on-accent); background: var(--interactive-accent) }
/* graphctx Display (R§4.5): Animate = a 78x30 button, right aligned, in a row without a name */
.graph-controls .gc-animate .setting-item-control { display: flex; justify-content: flex-end }
.graph-controls .gc-animate-btn { width: 78px; height: 30px; font: inherit; font-size: 13px; border: 0; border-radius: 4px; cursor: pointer;
  color: var(--text-normal, inherit); background: var(--interactive-normal, var(--border)) }

/* m5: shared fuzzy modal (quick switcher Ctrl+O / command palette Ctrl+P) */
/* R22: the overlay is the flex box, #mbox is its item — the 90px top offset is
   kept EXACTLY (same smoke click coordinates) as long as the natural card
   (~360px: input + 320px list) still fits under it, then it collapses toward 0
   and the card's own list is what shrinks. Nothing ever reaches past the
   bottom edge. */
#modal { position: fixed; inset: 0; z-index: 50; background: var(--scrim);
         display: flex; flex-direction: column; align-items: center; box-sizing: border-box;
         padding: max(0px, min(90px, 100vh - 420px)) 0 8px }
#modal[hidden] { display: none }
#mbox { width: 520px; max-width: 90%; margin: 0; background: var(--bg-elevated);
        max-height: 100%; min-height: 0; display: flex; flex-direction: column;
        border: 1px solid var(--border); border-radius: 8px; overflow: hidden;
        box-shadow: 0 8px 30px var(--shadow-modal) }
#minput { width: 100%; box-sizing: border-box; padding: 10px 14px; border: 0;
          flex: 0 0 auto;
          border-bottom: 1px solid var(--border); background: var(--bg-base);
          color: var(--text-chrome); font-size: 14px; outline: none }
#mlist { max-height: 320px; min-height: 0; flex: 0 1 auto; overflow-y: auto }
#mlist .mrow { display: flex; justify-content: space-between; align-items: center;
               padding: 6px 14px; cursor: pointer; font-size: 13px; color: var(--text-chrome) }
#mlist .mrow.sel, #mlist .mrow:hover { background: var(--background-modifier-hover) }   /* themematch: selected suggestion */
#mlist .mhint { color: var(--text-chrome-faint); font-size: 11px; margin-left: 12px; white-space: nowrap }
#mlist .mempty { padding: 8px 14px; color: var(--text-chrome-faint); font-size: 12px }
/* goal qscreate: stock's create hint is accent-coloured, and its static instructions
   row sits under the list (notes/stock.md r1 c00..c02), wrapping as needed */
#mlist .mhint.mcreate { color: var(--text-accent) }
#mfoot { flex: 0 0 auto; display: flex; flex-wrap: wrap; gap: 2px 14px; padding: 8px 14px;
         border-top: 1px solid var(--border); font-size: 11px; color: var(--text-chrome-faint) }
#mfoot[hidden] { display: none }
#mfoot b { font-weight: 600; color: var(--text-chrome) }

/* m5 F2 rename inline prompt */
#rnbox { position: fixed; top: max(0px, min(90px, 100vh - 60px)); left: 50%; transform: translateX(-50%);   /* R22: never below the bottom edge */
         width: 380px; max-width: 90%; background: var(--bg-elevated); z-index: 60;
         border: 1px solid var(--border); border-radius: 8px; overflow: hidden;
         box-shadow: 0 8px 30px var(--shadow-modal) }
#rninput { width: 100%; box-sizing: border-box; padding: 10px 14px; border: 0;
           background: var(--bg-base); color: var(--text-chrome); font-size: 14px; outline: none }
/* R34.4-R34.7 the "Update links" dialog. GEOMETRY IS FIXED ON PURPOSE: the
   card is a constant width, the button row is right-aligned and every button
   has an explicit width, so the focus ring lands at coordinates a probe can
   compute — and it does not have to: updateTitle() publishes the button
   centres as [ulx:...] (the R33.13 idiom — smoke READS the geometry, never
   hard-codes it).
   The fills keep the pixel discriminator that works on the recon
   screenshots and ours: the default button is the brightest fill on the row (--bg-button-default), its two
   neighbours a step darker (--bg-button-plain), with a --border-focus-ring row
   under it (docs/recon-title-rename/btn-analysis.log).
   The ring is what makes "which button is default" measurable in PIXELS
   instead of OCR-ed off a word list. */
#ulbox { position: fixed; inset: 0; z-index: 70; background: var(--scrim);
         display: flex; flex-direction: column; align-items: center; box-sizing: border-box;
         padding: max(0px, min(120px, 100vh - 260px)) 0 8px }
#ulbox[hidden] { display: none }
#ulcard { width: 480px; max-width: 92%; box-sizing: border-box; background: var(--bg-elevated);
          border: 1px solid var(--border); border-radius: 8px; padding: 18px 20px 16px;
          box-shadow: 0 8px 30px var(--shadow-modal); color: var(--text-chrome) }
#ultitle { font-size: 17px; font-weight: 600; margin-bottom: 10px }
#ulq { font-size: 13px; line-height: 1.5 }
#ulsay { font-size: 13px; line-height: 1.5; margin-top: 6px; color: var(--text-quiet) }
#ulbtns { display: flex; justify-content: flex-end; gap: 10px; margin-top: 18px }
#ulbtns button { box-sizing: border-box; height: 30px; border: 0; border-radius: 5px;
                 background: var(--bg-button-plain); color: var(--bg-base); font-size: 13px; font-weight: 500;
                 cursor: pointer; outline: none; padding: 0 }
#ul-always { width: 102px }
#ul-once   { width: 68px }
#ul-no     { width: 101px }
#ulbtns button:focus { background: var(--bg-button-default); box-shadow: 0 0 0 2px var(--border-focus-ring) }

/* R24.7 delete confirmation — deliberately the Update-links card's geometry
   (same scrim, same 480px card, same button row), so this app maintains ONE
   modal shape rather than two that drift. Two differences, both on purpose:
   the DEFAULT is Cancel (the destructive answer must not be one Enter away
   from a user who did not read the question), and Delete is tinted with the
   error colour so the irreversible button does not look like the safe one. */
#delbox { position: fixed; inset: 0; z-index: 70; background: var(--scrim);
          display: flex; flex-direction: column; align-items: center; box-sizing: border-box;
          padding: max(0px, min(120px, 100vh - 260px)) 0 8px }
#delbox[hidden] { display: none }
#delcard { width: 480px; max-width: 92%; box-sizing: border-box; background: var(--bg-elevated);
           border: 1px solid var(--border); border-radius: 8px; padding: 18px 20px 16px;
           box-shadow: 0 8px 30px var(--shadow-modal); color: var(--text-chrome) }
#deltitle { font-size: 17px; font-weight: 600; margin-bottom: 10px }
#delq { font-size: 13px; line-height: 1.5 }
#delsay { font-size: 13px; line-height: 1.5; margin-top: 6px; color: var(--text-quiet) }
#delbtns { display: flex; justify-content: flex-end; gap: 10px; margin-top: 18px }
#delbtns button { box-sizing: border-box; height: 30px; border: 0; border-radius: 5px;
                  background: var(--bg-button-plain); color: var(--bg-base); font-size: 13px; font-weight: 500;
                  cursor: pointer; outline: none; padding: 0 }
#del-no  { width: 101px }
#del-yes { width: 101px; background: var(--text-banner-error); color: var(--bg-base) }
#delbtns button:focus { box-shadow: 0 0 0 2px var(--border-focus-ring) }
#del-no:focus { background: var(--bg-button-default) }

/* R4X.7 Edit bookmark — the #delbox shape again (one modal geometry in this app,
   not three that drift): fixed overlay, 480px card, buttons bottom-right with
   Save last and accented. Ours
   inherits the house modal rather than growing a second one, and the phase
   clicks by the MEASURED [bmex:] centres, never by a constant. */
#bmebox { position: fixed; inset: 0; z-index: 70; background: var(--scrim);
          display: flex; flex-direction: column; align-items: center; box-sizing: border-box;
          padding: max(0px, min(120px, 100vh - 260px)) 0 8px }
#bmebox[hidden] { display: none }
#bmecard { width: 480px; max-width: 92%; box-sizing: border-box; background: var(--bg-elevated);
           border: 1px solid var(--border); border-radius: 8px; padding: 18px 20px 16px;
           box-shadow: 0 8px 30px var(--shadow-modal); color: var(--text-chrome) }
#bmetitle { font-size: 17px; font-weight: 600; margin-bottom: 10px }
.bmerow { display: flex; align-items: center; justify-content: space-between;
          gap: 12px; margin-top: 12px; font-size: 13px }
.bmelbl { color: var(--text-quiet) }
#bmecard input, #bmecard select { box-sizing: border-box; width: 240px; height: 28px;
          background: var(--bg-base); color: var(--text-chrome); font: inherit;
          border: 1px solid var(--border); border-radius: 4px; padding: 0 6px; outline: none }
#bme-path { color: var(--text-chrome-dim) }          /* read-only: the name IS the bookmark (R9.4) */
#bmecard select:focus { box-shadow: 0 0 0 2px var(--border-focus-ring) }
#bmebtns { display: flex; justify-content: flex-end; gap: 10px; margin-top: 18px }
#bmebtns button { box-sizing: border-box; height: 30px; width: 101px; border: 0; border-radius: 5px;
                  background: var(--bg-button-plain); color: var(--bg-base); font-size: 13px; font-weight: 500;
                  cursor: pointer; outline: none; padding: 0 }
#bme-yes { background: var(--accent-blue); color: var(--bg-base) }   /* the accented, non-destructive default, like .ullink */
#bmebtns button:focus { box-shadow: 0 0 0 2px var(--border-focus-ring) }
/* bmactive: the SAME card in "Add bookmark" mode (recon-bmactive F1, 03-modal.png):
   a Title row between Path and Bookmark group, and an X close top-right.
   Both are hidden in Edit mode so R4X.7's card is unchanged. */
#bmecard { position: relative }
.bmerow[hidden], #bme-x[hidden] { display: none }
#bme-x { position: absolute; top: 10px; right: 12px; width: 24px; height: 24px; border: 0; padding: 0;
         background: none; color: var(--text-quiet); font-size: 18px; line-height: 24px; cursor: pointer }
#bmecard #bme-title:focus { box-shadow: 0 0 0 2px var(--border-focus-ring) }

/* R34.12/R34.13 refusal notice — the dropmsg shape (bottom-right, transient),
   because it says the same kind of thing: what was refused and why. The census
   token it publishes does NOT expire with the banner (dropTok's lesson: "no
   refusal happened" and "a refusal whose banner faded" must not look alike). */
#notice { position: fixed; right: 12px; bottom: 12px; z-index: 71; max-width: 420px;
          padding: 8px 12px; border-radius: 6px; border: 1px solid var(--border-strong);
          background: var(--bg-surface); color: var(--text-chrome); font-size: 12px; line-height: 1.4;
          white-space: pre-line }
#notice[hidden] { display: none }
#saveerr:not([hidden]) ~ #notice, #dropmsg:not([hidden]) ~ #notice { bottom: 58px }

/* R10.3 block ids: grey superscript label in LP, hidden in reading view */
.lp .blockid { font-size: .7em; color: var(--text-chrome-dim); vertical-align: super; margin-left: 4px; user-select: none }
.preview .blockid { display: none }
/* R10.2 navigation target flash (~2 s fade, yellow) */
.navflash { background: var(--highlight-yellow) }
.navflash.fade { background: var(--transparent); transition: background 1.5s ease-out }
.ac .acb { float: right; margin-left: 14px; color: var(--text-chrome-dim); font-size: 11px; line-height: 20px }
/* R10.5 unlinked mentions: collapsible section head + Link button per hit */
.ulhead { cursor: pointer; display: flex; align-items: center; gap: 4px; margin-top: 6px;
          border-top: 1px solid var(--bg-surface); padding-top: 8px }
.ulhead .tc { display: flex; color: var(--text-chrome-dim) }
.ulhead .tc svg { width: 10px; height: 10px; transition: transform .1s }
.ulhead.open .tc svg { transform: rotate(90deg) }
.ullink { background: var(--accent-blue); color: var(--bg-base); border: 0; border-radius: 4px;
          padding: 1px 8px; font: 600 11px sans-serif; cursor: pointer }
/* R17: ONE engine for live preview and source mode (ui/editor.js). Every
   row carries its whole source line; the markers live in span.mk and are
   HIDDEN unless the SELECTION touches their TOKEN's raw range (.rv, flipped
   per token by Ed.reveal — R17.6/R17.7 M73-M77) or the pane is in source
   mode (.lp.src) — "source mode = live preview with every marker revealed".
   The row-wide rule is source mode ONLY: `.lprow.cur` still names the caret
   row for geometry/census, but revealing a whole row on every caret move is
   the R17.6 divergence this replaced.
   Because a hidden marker is display:none the caret can never land in it. */
.lp .lprow { white-space: pre-wrap; overflow-wrap: anywhere }
.lp .lprow.lzy { height: calc(var(--line-height-normal, 1.55) * 1em) }   /* audit #10/#12: a lazy row placeholder (Ed.lzy) holds one line of height */
.lp .mk { display: none }
.lp.src .mk, .lp .mk.rv { display: inline; color: var(--text-chrome-dim) }
.lp:focus, .lp .lprow:focus { outline: none }
.lp .tbox { vertical-align: middle }                            /* rendered task checkbox (x offsets: R15.11 below) */
.lp.src .tbox { display: none }                                 /* source mode shows [ ] instead */
.lp .h { font-weight: 700; color: var(--text-heading, currentColor) }   /* wasp: orange headings; currentColor = inherit for every theme that declares no --text-heading */
.lp .h1 { font-size: var(--h1-size) }
.lp .h2 { font-size: var(--h2-size) }
.lp .h3 { font-size: var(--h3-size) }
.lp .h4 { font-size: var(--h4-size) }
.lp .h5 { font-size: var(--h5-size) }
.lp .h6 { font-size: var(--h6-size) }
.lp .b { font-weight: var(--bold-weight) }
.lp .i { font-style: italic }
.lp .bi { font-weight: var(--bold-weight); font-style: italic }
.lp .s { text-decoration: line-through }
.lp .hl { background: var(--highlight-yellow) }
.lp .code { font-family: var(--font-monospace); font-size: var(--code-size); background: var(--code-background); border-radius: var(--code-radius) }
.lp .wl, .lp .lt, .lp .url { color: var(--accent-blue) }
.lp .wl:hover, .lp .lt:hover, .lp .url:hover { color: var(--text-accent-hover) }   /* themematch: link hover ink */
.lp a.wiki { text-decoration: none }
.lp.src .wl, .lp.src .lt, .lp.src .url { text-decoration: underline }
.lp .tag { color: var(--tag-color, var(--accent-blue)); background: var(--tag-bg); border-radius: 999px; padding: 0 4px }
.lp .task, .lp .hr, .lp .tbl { font-family: var(--font-monospace) }
.lp .fence { display: block; font-family: var(--font-monospace); font-size: 13px; background: var(--bg-surface);
             padding: 0 8px; white-space: pre; overflow-x: auto }
.lp .bq { color: inherit }   /* geometry (border + padding) is R15.13 below */
/* R14 settings modal (Ctrl+,) — setcenter R1/R2: centred on both axes by flex,
   sized by the viewport-relative rule below; recentres on resize with no JS.
   Phases never click it by literal coordinate: they read the census [sbox:] [snav:] ... */
#settings { position: fixed; inset: 0; z-index: 55; background: var(--scrim-strong);
  display: flex; align-items: center; justify-content: center; box-sizing: border-box }
#settings[hidden] { display: none }
#sbox { position: relative; width: min(90vw, 1100px); height: min(85vh, 1000px); box-sizing: border-box;
        min-width: 0; min-height: 0; max-width: 100%; max-height: 100%;
        background: var(--bg-elevated); border: 1px solid var(--border); border-radius: 8px; overflow: hidden;
        display: flex; flex-direction: column; box-shadow: 0 8px 30px var(--shadow-modal); font-size: 13px }
#shead { height: 36px; box-sizing: border-box; display: flex; align-items: center; justify-content: space-between;
         padding: 0 12px; background: var(--bg-base); border-bottom: 1px solid var(--border); font-weight: 600 }
#sclose { background: none; border: 0; color: var(--text-chrome-muted); cursor: pointer; font-size: 14px }
#sclose:hover { color: var(--text-chrome) }
#sbody { flex: 1; display: flex; min-height: 0 }
#snav { width: 180px; box-sizing: border-box; background: var(--bg-base); border-right: 1px solid var(--border);
        padding: 8px 0; overflow-y: auto }
.snavh { padding: 4px 14px; color: var(--text-chrome-faint); font-size: 11px; text-transform: uppercase }
.snavi { padding: 5px 14px; cursor: pointer; color: var(--text-chrome) }
.snavi:hover { background: var(--bg-surface) }
.snavi.sel { background: var(--border) }
#spage { flex: 1; min-width: 0; padding: 16px; box-sizing: border-box; display: flex; flex-direction: column }
/* R30 row panes: card inset, a fixed row height for a one-line description
   growing per extra line, a separator inset to the card, controls flush right
   so the rows line up with Obsidian's settings layout. #spage was already an
   allowed R22 scroll container; #snav declares overflow-y and is registered in
   OVF_SCROLL / R22.2 as well (R30.13). */
#spage.rows { display: block; overflow-y: auto; padding: 16px 20px }
.ssec { font-weight: 600; color: var(--text-chrome); margin: 24px 0 8px; font-size: 13px }
#spage.rows > .ssec:first-child { margin-top: 0 }
.scard { background: var(--bg-base); border: 1px solid var(--bg-surface); border-radius: 6px }
.srow { display: flex; align-items: center; gap: 16px; min-height: 76px; box-sizing: border-box;
        padding: 20px 17px; border-bottom: 1px solid var(--bg-surface) }
.scard > .srow:last-child { border-bottom: 0 }
.sinfo { flex: 1; min-width: 0 }
.slabel { font-size: 14px; line-height: 19px; color: var(--text-chrome) }
.sdesc { font-size: 13px; line-height: 16px; color: var(--text-chrome-faint); margin-top: 2px }
.slink { color: var(--accent-blue) }
/* the ONE disabled treatment (R30): not focusable (no control is a form element),
   not clickable, aria-disabled in the DOM — grey paint is the least of it. */
.srow.dis { cursor: default }
.srow.dis .slabel { color: var(--text-chrome-muted) }
.srow.dis .slabel.slink { color: var(--accent-blue) }   /* a link line stays a link line when disabled (Obsidian underlines it) */
.srow.linkrow .slabel { text-decoration: underline }
/* themematch: a disabled CONTROL keeps its normal paint (Obsidian has no
   disabled style, recon Q4) — the muted label, aria-disabled and the title carry it */
.srow.dis .sctl { pointer-events: none }
.srow.dis .sicons { opacity: .5; pointer-events: none }
.sctl { flex: none; display: flex; align-items: center; gap: 8px; color: var(--text-chrome); font-size: 13px }
.sctl.toggle { width: 40px; height: 22px; border-radius: 999px; background: var(--border); padding: 0; position: relative }
.sctl.toggle > i { position: absolute; top: 3px; left: 3px; width: 16px; height: 16px; border-radius: 50%; background: var(--text-chrome) }
.sctl.toggle.on { background: var(--interactive-accent) }
.sctl.toggle.on > i { left: 21px; background: var(--bg-base) }
.sctl.dropdown { width: 141px; height: 24px; box-sizing: border-box; padding: 0 8px; background: var(--bg-sidebar);
                 border: 1px solid var(--border); border-radius: 4px; white-space: nowrap; overflow: hidden }
/* .live = the one dropdown in this pane with a handler behind it (Appearance ▸
   Themes; the palette axis that used to sit beside it is gone — themeone item 7). Every other control here is a transcription of
   Obsidian's settings with no behaviour, rendered inert; this one gets the affordances that
   inertness withholds — a pointer, a hover, and a visible focus ring, because
   it IS a tab stop. */
.sctl.dropdown.live { cursor: pointer; background: var(--bg-surface) }
.sctl.dropdown.live:hover { border-color: var(--border-strong) }
.sctl.dropdown.live:focus-visible { outline: 2px solid var(--border-focus-ring); outline-offset: 1px }
.sctl.text { width: 141px; height: 24px; box-sizing: border-box; padding: 0 8px; background: var(--bg-sidebar);
             border: 1px solid var(--border); border-radius: 4px; color: var(--text-chrome-faint); white-space: nowrap; overflow: hidden }
.sctl.list { color: var(--text-chrome-faint); max-width: 200px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis }
.sctl.slider .trk { width: 150px; height: 4px; border-radius: 2px; background: var(--border); display: inline-block }
.sctl.color .sw { width: 22px; height: 22px; border-radius: 4px; background: var(--accent-mauve); display: inline-block }
.sctl.nav { color: var(--text-chrome-faint) }
/* goal fontset: the three live font rows (whole row opens the chooser), the
   live Font size slider, and the chooser sub-page (docs/fontset/recon.md
   REQ-1..7, REQ-16). Same tokens as the rest of the pane. */
.srow.live { cursor: pointer }
.srow.live:hover { background: var(--bg-surface) }
.sctl.nav.live .nv { margin-right: 4px; color: var(--text-chrome-muted) }
.sctl.slider.live { gap: 10px }
.sctl.slider.live .val { min-width: 18px; text-align: right; color: var(--text-chrome-muted) }
.frange { width: 150px; margin: 0; accent-color: var(--interactive-accent); cursor: pointer }
.fbtn { flex: none; display: inline-flex; align-items: center; justify-content: center; width: 26px; height: 26px;
        padding: 0; border: 0; border-radius: 4px; background: none; color: var(--text-chrome-faint); cursor: pointer }
.fbtn:hover { background: var(--bg-surface); color: var(--text-chrome) }
.fbtn[aria-disabled="true"] { opacity: .4; cursor: default; pointer-events: none }
.fgrip { cursor: grab }
#spage.fontpage .fhead { display: flex; align-items: center; gap: 8px; margin-bottom: 8px }
.fback { display: inline-flex; align-items: center; justify-content: center; width: 28px; height: 28px; padding: 0;
         border: 0; border-radius: 4px; background: none; color: var(--text-chrome); cursor: pointer }
.fback:hover { background: var(--bg-surface) }
.ftitle { font-size: 16px; font-weight: 600; color: var(--text-chrome) }
.fdesc { margin: 0 0 16px }
.fselcard { margin-bottom: 16px }
.fselcard[hidden] { display: none }
.frow { display: flex; align-items: center; gap: 8px; min-height: 44px; padding: 8px 17px; box-sizing: border-box;
        border-bottom: 1px solid var(--bg-surface) }
.scard > .frow:last-child { border-bottom: 0 }
.fcand { cursor: pointer }
.fcand:hover { background: var(--bg-surface) }
.fsearch { display: block; width: calc(100% - 24px); margin: 12px; height: 30px; box-sizing: border-box; padding: 0 8px;
           background: var(--bg-sidebar); color: var(--text-chrome); border: 1px solid var(--border); border-radius: 4px;
           font: inherit; outline: none }
.fsearch:focus { border-color: var(--border-focus-ring) }
.fcands { max-height: 360px; overflow-y: auto; border-top: 1px solid var(--bg-surface) }
.sbtn { padding: 4px 10px; background: var(--bg-surface); border: 1px solid var(--border); border-radius: 4px; white-space: nowrap }
.sbtn.acc { background: var(--interactive-accent); border-color: var(--interactive-accent); color: var(--bg-ribbon) }
.sicons { flex: none; display: flex; align-items: center; gap: 19px; color: var(--text-chrome-faint) }
.sico { width: 16px; height: 16px; display: flex; align-items: center; justify-content: center;
        text-align: center; font-size: 15px; line-height: 16px }
/* goal/noplugins: the Core plugins LIST pane (.list rows, .ssearch) and the
   Community plugins security card (.sblurb*, .sgrid, .scell) left with their panes. */
.srow.linkrow { min-height: 44px }
.sempty { color: var(--text-chrome-faint) }
#hkbar { display: flex; align-items: center; gap: 6px; height: 28px }
#hkfilter { flex: 1; height: 28px; box-sizing: border-box; background: var(--background-modifier-form-field, var(--bg-sidebar)); color: var(--text-chrome);
            border: 1px solid var(--border); border-radius: 4px; padding: 0 8px; outline: none; font-size: 13px }
#hkfilter:focus { border-color: var(--background-modifier-border-focus) }   /* themematch: focused search field border */
#hkclear { color: var(--text-chrome-faint); cursor: pointer; width: 16px; text-align: center }
#hkchips { display: flex; gap: 6px; height: 24px; margin: 8px 0; align-items: center }
.hkchip { padding: 2px 10px; border-radius: 999px; background: var(--bg-surface); color: var(--text-chrome); cursor: pointer; font-size: 12px; line-height: 16px }
.hkchip.sel { background: var(--accent-mauve); color: var(--bg-base) }
.hkchip.conf { background: var(--accent-red); color: var(--text-on-accent) }
.hkchip.conf.sel { outline: 2px solid var(--text-on-accent) }
#hklist { flex: 1; overflow-y: auto; min-height: 0 }
.hkrow { display: flex; align-items: center; height: 32px; box-sizing: border-box; gap: 6px;
         border-bottom: 1px solid var(--bg-surface) }
.hkname { flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis }
.hkkeys { display: flex; gap: 6px; align-items: center }
.hkkey { font-family: monospace; font-size: 12px; background: var(--bg-surface); color: var(--text-chrome); border-radius: 4px;
         padding: 2px 6px; display: inline-flex; align-items: center; gap: 4px; white-space: nowrap }
.hkkey.blank { color: var(--text-chrome-faint) }
.hkkey.rec { background: var(--accent-mauve); color: var(--bg-base) }
.hkkey.conf { background: var(--accent-red); color: var(--text-on-accent) }
.hkx { width: 14px; text-align: center; cursor: pointer; font-size: 11px; color: var(--text-chrome-muted) }
.hkkey.conf .hkx { color: var(--text-on-accent) }
.hkx:hover { color: var(--accent-red) }
.hkbtn { width: 24px; height: 24px; box-sizing: border-box; padding: 0; background: var(--bg-surface); color: var(--text-chrome);
         border: 0; border-radius: 4px; cursor: pointer; font-size: 14px; line-height: 24px }
.hkbtn:hover { background: var(--border) }
.hkbtn, .hkx, #sclose { display: inline-flex; align-items: center; justify-content: center }
.hkbtn svg, .hkx svg, #sclose svg { display: block }
/* ---------- R15 TYPOGRAPHY (docs/requirements.md §15): dark-theme tokens ---------- */
/* R15.2 bundled fonts (ui/fonts/, scripts/fetch-fonts.sh): same family names / weight ranges / font-display as Obsidian */
@font-face { font-family: 'Source Code Pro'; font-weight: normal; font-style: normal; font-display: swap; src: url(fonts/SourceCodePro-Regular.ttf) }
@font-face { font-family: 'Source Code Pro'; font-weight: normal; font-style: italic; font-display: swap; src: url(fonts/SourceCodePro-Italic.ttf) }
@font-face { font-family: 'Source Code Pro'; font-weight: bold; font-style: normal; font-display: swap; src: url(fonts/SourceCodePro-Bold.ttf) }
@font-face { font-family: 'Source Code Pro'; font-weight: bold; font-style: italic; font-display: swap; src: url(fonts/SourceCodePro-BoldItalic.ttf) }
@font-face { font-family: "Inter"; font-style: normal; font-weight: 100 900; font-display: swap; src: url(fonts/Inter-Variable.woff2) }
@font-face { font-family: "Inter"; font-style: italic; font-weight: 100 900; font-display: swap; src: url(fonts/Inter-Variable-Italic.woff2) }
:root {
  /* R15.1 literal stacks (system-ui resolves first on Linux -> Noto Sans, like Obsidian; code -> bundled Source Code Pro) */
  --font-default: ui-sans-serif, -apple-system, BlinkMacSystemFont, system-ui, "Segoe UI", "Google Sans Flex", Roboto, "Inter Variable", "Inter", "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", sans-serif;
  --font-monospace-default: ui-monospace, SFMono-Regular, "Cascadia Mono", "Roboto Mono", "DejaVu Sans Mono", "Liberation Mono", Menlo, Monaco, "Consolas", "Source Code Pro", monospace;
  --font-text: var(--font-default); --font-interface: var(--font-default); --font-monospace: var(--font-monospace-default);
  /* R15.4-6 */
  --font-text-size: 16px; --font-smaller: 0.9em; --line-height-normal: 1.55; --line-height-tight: 1.25;
  /* ONE spacing unit for the content column (TOKENS.md): every margin, indent, radius and width below is a multiple of it */
  --space-unit: 0.25rem;
  --font-weight: 400; --bold-modifier: 250; --bold-weight: min(900, calc(var(--font-weight) + var(--bold-modifier)));
  /* R15.7-9 headings */
  --h1-size: 1.974em; --h2-size: 1.762em; --h3-size: 1.574em; --h4-size: 1.405em; --h5-size: 1.254em; --h6-size: 1.12em;
  --h1-weight: 700; --h2-weight: 600; --h3-weight: 600; --h4-weight: 600; --h5-weight: 600; --h6-weight: 600;
  --h1-line-height: 1.15; --h2-line-height: 1.21; --h3-line-height: 1.27; --h4-line-height: 1.33;
  --h5-line-height: 1.39; --h6-line-height: 1.45;
  --h1-letter-spacing: -0.018em; --h2-letter-spacing: -0.015em; --h3-letter-spacing: -0.012em;
  --h4-letter-spacing: -0.009em; --h5-letter-spacing: -0.006em; --h6-letter-spacing: -0.003em;
  --p-spacing: calc(var(--space-unit) * 5); --p-spacing-empty: var(--space-unit); --heading-spacing: calc(var(--space-unit) * 8);
  /* R15.10-13 lists / checkbox / code / blockquote */
  --list-indent: calc(var(--space-unit) * 7);
  --list-indent-editing: calc(var(--space-unit) * 2); --list-spacing: calc(var(--space-unit) / 2); --list-bullet-size: calc(var(--space-unit) * 1.5); --list-bullet-radius: calc(var(--list-bullet-size) / 2);
  --checkbox-size: calc(var(--space-unit) * 3.5); --checkbox-radius: calc(var(--space-unit) * 0.75);
  --checkbox-margin-inline-start: calc(var(--space-unit) * 3); --checklist-done-decoration: line-through 0.06em;
  --code-size: 0.92em; --code-radius: calc(var(--space-unit) * 1.5);
  --blockquote-border-thickness: calc(var(--space-unit) * 0.75);
  /* THE SEVEN COLOUR ROWS OF THIS BLOCK LIVE ON `body`, NOT HERE — see the
     "R15.10-13 CONTENT COLOURS" comment in the body token block at the top of
     this file: --list-marker-color, --checkbox-border-color, --checkbox-color,
     --checkbox-marker-color, --checklist-done-color, --code-background,
     --blockquote-border-color. Each derives from a layer-2/3 name of ours, which
     is declared on body, and a var() naming a property this element does not
     have is guaranteed-invalid, not a fallback. Keep this block colour-free. */
  /* R15.14 dark tokens (content area only — app chrome keeps its own token layer): --text-normal / --text-muted / --text-faint / --background-primary(-alt) / --interactive-accent all live in the ONE token block at the top of this file */
  --radius-s: 4px; --size-4-6: 24px; --size-4-8: 32px; --file-line-width: 700px; --file-margins: var(--size-4-8);
  /* sidefont: ONE size for every sidebar list row — explorer .trow, bookmarks
     .bmrow, the right-strip .rlist rows, search .sgroup file rows. 13px is
     the recon value (docs/recon-sidefont/README.md Q1/Q3/Q5), and
     Obsidian's text-font slider never moves it (Q4), so it is a standalone UI
     size, NOT derived from --font-text-size. Snippet lines (.shit, .blline)
     are smaller and keep their own size. Published as [sfont:t<>b<>r<>]. */
  --sidebar-font-size: 13px;
}
@supports (font-variation-settings: normal) {   /* graded heading weights on variable fonts */
  :root { --h2-weight: 680; --h3-weight: 660; --h4-weight: 640; --h5-weight: 620; --h6-weight: 600 }
}
/* note column: 16px/1.5 text, 700px readable line length centred, 32px file margins
   goal/minimalmargin (docs/recon-minimalmargin, stock 1.13.7 measured black-box):
   - the column is bounded ONLY while the vault's app.json "readableLineLength"
     is on (default on). ui/main.js mirrors it as `.is-readable-line-width` on
     each .lp/.preview — the class stock puts on its view and the one themes key
     on. Off = the column fills the pane between the file margins.
   - bounded width = min(--line-width, --max-width) when a theme declares them
     (Minimal 9.0.2: 40rem, 88%), else --file-line-width (stock Default: 700px,
     measured; was 720px here). Minimal applies the same pair through stock-DOM
     selectors (.markdown-preview-sizer > div, .cm-content > div) that our DOM
     cannot match — so the pair is read here, on our own column.
   - padding is ONE var(--file-margins), any 1-4 value form, like stock. The old
     `min(var(--file-margins), 5%)` went invalid-at-computed-time on Minimal's
     `.is-readable-line-width { --file-margins: 1rem 0 0 0 }`.
   R22: padding does NOT shrink, so a literal 32px on each side makes the note
   pane 64px wide minimum and pushes the window past the viewport in a narrow
   split. Below a 640px pane (where 5% < 32px) OUR default margins clamp to 5%;
   it is declared at :where() specificity, so a theme's own --file-margins wins. */
.lp, .preview { font-family: var(--font-text); font-size: var(--font-text-size); line-height: var(--line-height-normal);
                color: var(--text-normal);
                padding: var(--file-margins) }
@container notepane (max-width: 639.98px) {
  :where(.lp, .preview) { --file-margins: var(--size-4-8) 5% }
}
.is-readable-line-width { --note-column: min(var(--line-width, var(--file-line-width)), var(--max-width, 100%)) }
.lp > *, .preview > * { max-width: var(--note-column, none); margin-inline: auto }
/* stock's note scroller carries a 12px classic scrollbar (track transparent,
   thumb inset 2px left / 3px right) — measured in the recon shots at x 884..895
   of a 896px window; the column centres in what is left of the pane. WebKitGTK
   would otherwise paint a zero-width overlay bar and centre 6px further right. */
.lp::-webkit-scrollbar, .preview::-webkit-scrollbar { width: 12px; background-color: var(--scrollbar-bg) }
.lp::-webkit-scrollbar-thumb, .preview::-webkit-scrollbar-thumb {
  background-color: var(--scrollbar-thumb-bg); background-clip: padding-box;
  border-style: solid; border-color: var(--scrollbar-bg); border-width: 0 3px 0 2px; border-radius: 6px; min-height: 45px }
.lp::-webkit-scrollbar-thumb:active, .preview::-webkit-scrollbar-thumb:active { background-color: var(--scrollbar-active-thumb-bg, var(--scrollbar-thumb-bg)) }
/* #20 INLINE TITLE (R32): the big title of a note is its FILENAME, rendered —
   it lives in no file. Measured against Obsidian (progress.md Q3): the
   title is pixel-identical to a body h1 (same ink, same trim box) and the
   block it adds above the content is one h1 line box + a 0.5em
   margin-bottom. NOTHING above it: the title
   takes the content column's own y-origin.
   It is a ::before, NOT a DOM child, and that is load-bearing: ui/editor.js
   indexes model rows POSITIONALLY (rowAt = g.lp.children[l], editor.js:491),
   so a real element prepended to .lp would shift every line index by one.
   A pseudo-element also cannot hold a caret. R32.5 called that "non-editable
   by construction" and accepted it as the delta from Obsidian; R34 reverses that
   half — see the R34.15 block below for the mechanism that gives the title a
   caret while the ::before, and therefore every row index, stays put.
   Fed by data-title (ui/main.js setInlineTitle); the ATTRIBUTE selector is
   deliberate — no attribute, no box, so a note-less pane keeps its y-origin. */
/* R34.18 — THE TITLE'S TYPE, DECLARED ONCE, FOR BOTH SURFACES.
   This selector list used to be two rules that a comment PROMISED were the same
   ("repeats the ::before's type declarations verbatim"). They were not, and a
   promise in a comment is not a mechanism: `scripts/smoke.sh fast titletype`
   measured the two surfaces apart on THREE axes — size (1.618em resolving
   against a 16px base on one and a 14px base on the other), family (the
   --font-text stack against the body's system-ui fallback), and colour
   (--text-normal against --text-chrome). The NUMBERS deliberately do not live
   here: this file is where the false claim lived, so the measurement stays in
   the phase's log, where every run re-takes it.
   Now there is ONE rule, so the two surfaces cannot drift by editing: every
   declaration here lands on the ::before that PAINTS the title and on the
   .titleedit that replaces it while renaming. What each needs on its own —
   content, margins and user-select for the ::before; a transparent box with no
   border for the editor — stays in its own rule below.
   NOTE the sizes are all `em`/token relative and MUST stay that way: a literal
   would lock the fix to --font-text-size: 16px, which is the same bug holding
   one lucky value. What makes em resolve identically on both is .titlewrap
   carrying the note column's own type context — see the rule below it. */
.lp[data-title]::before, .preview[data-title]::before, .titleedit {
  display: block; max-width: var(--note-column, none); margin-inline: auto;
  font-size: var(--h1-size); font-weight: var(--h1-weight);
  line-height: var(--h1-line-height); letter-spacing: var(--h1-letter-spacing);
  color: inherit; overflow-wrap: anywhere; white-space: pre-wrap; cursor: text;
}
.lp[data-title]::before, .preview[data-title]::before {
  content: attr(data-title);
  margin-top: 0; margin-bottom: 0.5em; user-select: none;
}
/* R34.15 (R32.4 restated as a NUMERIC condition) — HOW THE TITLE BECOMES
   EDITABLE WITHOUT BECOMING A ROW.
   The ::before above stays exactly as it is, always, including while the title
   is being edited: it is the LAYOUT. `visibility: hidden` (never `display:
   none`, never a swapped-in element) keeps its box — same height, same
   margin-bottom, same y-origin for row 0 — and only drops the ink, so no row
   moves and the scroller does not jump by 44px the moment a caret arrives.
   The caret surface is `.titlewrap > .titleedit`, appended to `.content`
   (ui/main.js openTitleEdit) — a SIBLING of the scroller, never a child of it.
   ui/editor.js indexes model rows positionally (rowAt = g.lp.children[l],
   editor.js:491); an editable node inside .lp — prepended, appended or
   absolutely positioned — is still `g.lp.children[i]` and shifts or extends
   that list. Outside it, `children` is byte-identical open or closed, which is
   what the census token [te:<text>/<rows>] publishes so a phase can assert it.
   .titlewrap is the POSITIONER (the scroller's content box, full width, fed
   pixel values by titleBox()); .titleedit is the text column. It no longer
   "repeats the ::before's type declarations verbatim" — that sentence stood
   here while the two surfaces differed in size, font family and text
   colour, which is R34.18's whole lesson: the type is now declared ONCE, in a
   selector list both surfaces are in, and the agreement is MEASURED by
   `scripts/smoke.sh fast titletype` (computed style off the live DOM + ink off
   a screenshot) rather than asserted here. Background stays transparent: there
   is nothing to cover, the ::before is already invisible underneath. */
.lp.titling::before, .preview.titling::before { visibility: hidden }
/* R34.18 — THE ROOT CAUSE, AND WHY THE FIX IS HERE AND NOT ON .titleedit.
   The shared rule above is written in em and tokens: `--h1-size: 1.974em` and
   `--h1-letter-spacing: -0.018em`. `em` resolves against the element's INHERITED
   font-size, and `color: inherit` / an undeclared font-family resolve against
   the inherited value — so identical declarations produce different type when
   the two elements sit in different contexts, which is exactly what happened:
     .lp[data-title]::before  inherits .lp  -> --font-text / --font-text-size
                              (style.css `.lp, .preview`) -> --h1-size x 16px
     .titleedit               inherits .content, which sets no type at all, so
                              it fell through to `body { font: 14px/1.5 system-ui }`
                              and `color: var(--text-chrome)` -> --h1-size x 14px,
                              a different family, and the CHROME text colour.
   .titleedit cannot be moved inside .lp to inherit it — an editable node in the
   scroller is `g.lp.children[i]` and shifts every positional row index
   (ui/editor.js:491, R34.15). So the CONTEXT is brought to the surface instead:
   .titlewrap, the positioner, restates the note column's own type context, the
   same tokens `.lp, .preview` reads. Nothing is hardcoded — move
   --font-text-size and both surfaces move together, which is what
   `fast titletype` re-measures at a non-default size. */
.titlewrap { position: absolute; z-index: 5; box-sizing: border-box;
             font-family: var(--font-text); font-size: var(--font-text-size);
             line-height: var(--line-height-normal); color: var(--text-normal) }
.titleedit {
  background: var(--transparent); border: 0; outline: none; user-select: text;
}
.lp .lprow { width: 100% }
.lp h1, .preview h1, .lp h2, .preview h2, .lp h3, .preview h3, .lp h4, .preview h4, .lp h5, .preview h5, .lp h6, .preview h6 {
  color: var(--text-heading, currentColor); font-style: normal; font-variant: normal; border: 0 }   /* wasp: see .lp .h */
.lp h1, .preview h1 { font-size: var(--h1-size); font-weight: var(--h1-weight); line-height: var(--h1-line-height); letter-spacing: var(--h1-letter-spacing) }
.lp h2, .preview h2 { font-size: var(--h2-size); font-weight: var(--h2-weight); line-height: var(--h2-line-height); letter-spacing: var(--h2-letter-spacing) }
.lp h3, .preview h3 { font-size: var(--h3-size); font-weight: var(--h3-weight); line-height: var(--h3-line-height); letter-spacing: var(--h3-letter-spacing) }
.lp h4, .preview h4 { font-size: var(--h4-size); font-weight: var(--h4-weight); line-height: var(--h4-line-height); letter-spacing: var(--h4-letter-spacing) }
.lp h5, .preview h5 { font-size: var(--h5-size); font-weight: var(--h5-weight); line-height: var(--h5-line-height); letter-spacing: var(--h5-letter-spacing) }
.lp h6, .preview h6 { font-size: var(--h6-size); font-weight: var(--h6-weight); line-height: var(--h6-line-height); letter-spacing: var(--h6-letter-spacing) }
.lp b, .lp strong, .preview b, .preview strong { font-weight: var(--bold-weight) }
.lp em, .preview em { font-style: italic }
/* R15.9 reading-mode block spacing; LP spacing comes from the blank-line rows */
.preview :is(p, ul, ol, table, pre, blockquote) { margin-block: var(--p-spacing) }
.preview :is(h1, h2, h3, h4, h5, h6) { margin-block: var(--p-spacing) }
.preview :is(p, pre, table, ul, ol) + :is(h1, h2, h3, h4, h5, h6) { margin-top: var(--heading-spacing) }
.preview > :first-child { margin-top: 0 }
/* R15.10 lists: no ul/ol padding, li indented 3ch, nested by --list-indent, faint markers, 0.3em disc bullet */
.lp ul, .lp ol, .preview ul, .preview ol { padding-inline-start: 0 }
.lp ul ul, .lp ul ol, .lp ol ul, .lp ol ol, .preview ul ul, .preview ul ol, .preview ol ul, .preview ol ol { margin-block: 0 }
.lp li, .preview li { padding-block: var(--list-spacing); position: relative }
.preview ul > li, .preview ol > li { margin-inline-start: 3ch }
.preview ul ul > li, .preview ol ol > li, .preview ul ol > li, .preview ol ul > li { margin-inline-start: var(--list-indent) }
.lp li::marker, .preview li::marker { color: var(--list-marker-color) }
/* markers are ::before pseudo-elements (not ::marker): drawing our own bullet / number means the x offsets are exact.
   ol keeps the decimal numbering via the built-in list-item counter (honours <ol start>); the box sits 2px left of the h1 "H" ink. */
.lp ul, .preview ul, .lp ol, .preview ol { list-style: none }
.lp ul > li::before, .preview ul > li::before { content: ""; position: absolute; width: var(--list-bullet-size); height: var(--list-bullet-size);
  border-radius: var(--list-bullet-radius); background: var(--list-marker-color); pointer-events: none;
  inset-inline-start: calc(14px - 3ch); top: calc(var(--list-spacing) + 0.6em) }
.lp ol > li::before, .preview ol > li::before { content: counter(list-item) "."; position: absolute; color: var(--list-marker-color);
  pointer-events: none; inset-inline-start: calc(12px - 3ch); top: var(--list-spacing) }
/* R15.11 task list: custom 16px box r4 1px faint border, accent fill + masked tick; no bullet; done = strike + muted */
.lp li:has(> input[type=checkbox])::before, .preview li:has(> input[type=checkbox])::before { display: none }
.lp li:has(> input[type=checkbox]), .preview li:has(> input[type=checkbox]) { list-style: none }
.lp li:has(> input[type=checkbox]:checked),
/* rvtask Q5: Obsidian strikes only "[x]"/"[X]" in a BULLET list — a custom status ("[/]" "[-]" …)
   and an ORDERED-list task are filled but not struck. data-task comes from the renderer. */
.preview ul > li:is([data-task="x"], [data-task="X"]) {
  text-decoration: var(--checklist-done-decoration); color: var(--checklist-done-color) }
.lp input[type=checkbox], .preview input[type=checkbox] { -webkit-appearance: none; appearance: none; border-radius: var(--checkbox-radius);
  border: 1px solid var(--checkbox-border-color); flex-shrink: 0; padding: 0; margin: 0; width: var(--checkbox-size); height: var(--checkbox-size);
  position: relative; top: 0.2em; margin-inline-end: 0.6em; cursor: pointer; vertical-align: baseline }
.preview ul > li > input[type=checkbox] { margin-inline-start: calc(var(--checkbox-size) * -1.5 - 1px) }   /* box at +3 (h1-ink relative) */
/* rvcursor + rvtask R1: the READING-VIEW task box is now ENABLED and toggles
   the source line (rvtask R3), but it keeps the ordinary ARROW — Obsidian
   shows cursor:default on it, hovered too (docs/recon-rvtask README Q10,
   spec K1). Declared, not inherited, because a UA sheet may set its own
   `cursor` on a form control; the census phase reads the live value either way.
   LP's box is NOT named here — live preview is editable and unchanged. */
.preview input[type=checkbox] { cursor: default }
/* goal/listlinks — reading view list/link geometry
   (docs/listlinks/recon.md REQ-n). .preview only; live preview untouched. */
.preview ul > li::before { inset-inline-start: calc(-12.8px - var(--list-bullet-size) / 2) }   /* REQ-1: dot centre = text x - 12.8 */
.preview li > p { margin-block: 0 }                                                         /* REQ-13: loose list == tight */
.preview ol > li::before { inset-inline-start: auto; inset-inline-end: calc(100% + 6px); font-variant-numeric: tabular-nums; white-space: nowrap }   /* REQ-11: right-aligned, tabular; "." ink right = text ink - 7 (native ::marker sits 5px further left in WebKitGTK) */
.preview ul > li > input[type=checkbox] { margin-inline-start: calc(var(--checkbox-size) * -1.5);   /* REQ-12: -1.5 x size, end 0.6em — within 1px of the recon; see docs/listlinks/recon.md AFTER */
  margin-inline-end: 0.6em; top: calc(0.2em - 1px) }
.preview a.ext { padding-inline-end: 0.9em; background-repeat: no-repeat; background-position: 100% 4px; background-size: 0.825em;   /* REQ-6 */
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%238a8a8a' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M15 3h6v6'/%3E%3Cpath d='M10 14 21 3'/%3E%3Cpath d='M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6'/%3E%3C/svg%3E") }
.preview a.wiki-unresolved { font-style: normal; text-decoration-color: var(--link-unresolved-underline) }   /* REQ-15 */
.preview li > :is(ul, ol) { position: relative }                                            /* REQ-9: indent guide at parent text x - 13 */
.preview li > :is(ul, ol)::before { content: ""; position: absolute; inset-inline-start: -13px; top: 0; bottom: 0; width: 1px;
  background: var(--indentation-guide-color); pointer-events: none }
/* LP: the task li has no list margin (the checkbox sits in the text flow after the hidden "- " widget); box at +7, text at +27 */
.lp li:has(> input[type=checkbox]) { margin-inline-start: 0 }
.lp ul > li > input[type=checkbox] { margin-inline-start: calc(var(--checkbox-margin-inline-start) - 2px); margin-inline-end: calc(0.6em - 4px) }
.lp input[type=checkbox]:hover, .preview input[type=checkbox]:hover { border-color: var(--text-muted) }
.lp input[type=checkbox]:checked, .preview input[type=checkbox]:checked { background-color: var(--checkbox-color); border-color: var(--checkbox-color) }
.lp input[type=checkbox]:checked:after, .preview input[type=checkbox]:checked:after { content: ""; top: -1px; inset-inline-start: -1px; position: absolute;
  width: var(--checkbox-size); height: var(--checkbox-size); display: block; background-color: var(--checkbox-marker-color);
  -webkit-mask-position: 52% 52%; -webkit-mask-size: 65%; -webkit-mask-repeat: no-repeat;
  -webkit-mask-image: url("data:image/svg+xml; utf8, <svg width=\"12px\" height=\"10px\" viewBox=\"0 0 12 8\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\"><g stroke=\"none\" stroke-width=\"1\" fill=\"none\" fill-rule=\"evenodd\"><g transform=\"translate%28-4.000000, -6.000000%29\" fill=\"%23000000\"><path d=\"M8.1043257,14.0367999 L4.52468714,10.5420499 C4.32525014,10.3497722 4.32525014,10.0368095 4.52468714,9.8424863 L5.24777413,9.1439454 C5.44721114,8.95166768 5.77142411,8.95166768 5.97086112,9.1439454 L8.46638057,11.5903727 L14.0291389,6.1442083 C14.2285759,5.95193057 14.5527889,5.95193057 14.7522259,6.1442083 L15.4753129,6.84377194 C15.6747499,7.03604967 15.6747499,7.35003511 15.4753129,7.54129009 L8.82741268,14.0367999 C8.62797568,14.2290777 8.3037627,14.2290777 8.1043257,14.0367999\"></path></g></g></svg>") }
/* R15.12 inline code: Source Code Pro 0.875em on the alt background, r4, 0.15em 0.3em */
.lp code, .preview code { font-family: var(--font-monospace); font-size: var(--code-size); background-color: var(--code-background);
  border-radius: var(--code-radius); padding: 0.15em 0.3em; -webkit-box-decoration-break: clone; box-decoration-break: clone; color: var(--text-normal) }
.lp pre, .preview pre { font-family: var(--font-monospace); font-size: var(--code-size); background-color: var(--code-background); border-radius: var(--code-radius); padding: 0.5em 1em; overflow-x: auto }
.lp pre code, .preview pre code { padding: 0; background: none; font-size: inherit }
/* R15.13 blockquote: 2px accent inline-start border, 24px padding, no margin (LP: 14px text offset) */
.lp blockquote, .preview blockquote { border-inline-start: var(--blockquote-border-thickness) solid var(--blockquote-border-color);
  padding: 0 0 0 var(--size-4-6); margin-inline: 0; font-style: normal; color: inherit; background: var(--transparent);
  position: relative; inset-inline-start: 2px }   /* the border sits on the h1 "H" ink x; the content box is 2px left of it */
.preview > blockquote { margin-inline: auto }    /* keep the 700px column centring (the rule above outranks .preview > *) */
.preview blockquote > :first-child { margin-top: 0 }
.preview blockquote > :last-child { margin-bottom: 0 }
.lp blockquote { padding-inline-start: 11px }
/* R15.10-13 in LIVE PREVIEW, on the R17 flat row DOM (ui/editor.js emits ONE
   div.lprow per source line — there is no ul/ol/li/blockquote to select, so
   the semantic rules above are reading-mode only). The reading-view offsets are
   reproduced on the row's own parts, never by absolute positioning against
   the row: the hidden marker span KEEPS ITS BOX (min-width = the
   marker advance) and draws the bullet inside it, so a nested item's literal
   indent — real text in the row — shifts marker and text together for free.
   Revealing the markers (.cur row / source mode) drops the box: the source
   line then measures exactly as it reads. */
.lp .li > .mk.lim { display: inline-block; min-width: 23px; position: relative; color: var(--transparent) }
.lp .li > .mk.lim::before { content: ""; position: absolute; inset-inline-start: 14px; top: 0.6em;
  width: var(--list-bullet-size); height: var(--list-bullet-size);
  border-radius: var(--list-bullet-radius); background: var(--list-marker-color) }
.lp .li.ord > .mk.lim { min-width: 32px; padding-inline-start: 15px; color: var(--list-marker-color) }
.lp .li.ord > .mk.lim::before { content: none }
.lp .tk > .mk.lim { display: none }                             /* a task row draws a checkbox, not a bullet */
.lp .tk > .mk.lim::before { content: none }
/* the selector must out-specify `.lp input[type=checkbox]` above (0,2,1): an
   attribute selector weighs as much as a class, so a bare `.lp .tbox` loses */
.lp input[type=checkbox].tbox { margin-inline-start: 9px; margin-inline-end: calc(0.6em - 4px) }   /* box at +7, text at +27 */
/* markers revealed: the box collapses, the line renders as its raw source.
   Per TOKEN (.rv) now, not per row — the list marker opens only when the
   selection actually touches "- " (R17.6 D6 vs E6). */
.lp.src .li > .mk.lim, .lp .li > .mk.lim.rv {
  display: inline; min-width: 0; padding: 0; color: var(--text-chrome-dim) }
.lp.src .li > .mk.lim::before, .lp .li > .mk.lim.rv::before { content: none }
/* R15.13 blockquote in LP: the span.bq is the line's block box — border ink on
   the h1 "H" ink x (+2 from the column), text 11px past it */
.lp .bq { display: block; margin-inline-start: 2px;
  border-inline-start: var(--blockquote-border-thickness) solid var(--blockquote-border-color);
  padding-inline-start: 11px }
.lp .bq .bq { margin-inline-start: 0 }                          /* nested "> >": only the outer one is offset */

/* perf (INTEGRATE): compositor floor probe target — a real 1px style+paint
   invalidation so floorProbe() measures a frame, not an elided class flip.
   Created lazily by floorProbe(); absent from the DOM in normal use. */
#floorpx { position: fixed; left: 0; bottom: 0; width: 1px; height: 1px; background: var(--bg-base); z-index: 999; pointer-events: none }
body.floorprobe #floorpx { background: var(--bg-sidebar) }
/* INTEGRATE: the 1px probe above measures the cheapest possible frame (damage =
   one pixel). pane_split/tab_drop damage the WHOLE window, so two more floors
   bracket them, driven through the same act()->paint path: `floorfull` flips the
   inherited colour by 1/255 (every painted glyph re-rastered, no layout) and
   `floorrelayout` flips #main's padding by 1px (both note columns reflow, then
   that same full raster) — the damage a pane split actually causes. */
body.floorfull #main, body.floorfull #side { color: var(--text-chrome-alt) }
body.floorrelayout #main { padding-right: 1px }

/* F1 dataloss: a save that did not land gets a persistent banner — the whole
   point of the finding is that a failed write used to be invisible. Bottom
   right, above the panes, non-blocking (the user keeps typing; the buffer is
   still dirty and every later save retries). */
#saveerr { position: fixed; right: 12px; bottom: 12px; z-index: 60; max-width: 420px;
           padding: 8px 12px; border-radius: 6px; border: 1px solid var(--accent-red);
           background: var(--bg-banner-error); color: var(--text-banner-error); font-size: 12px; line-height: 1.4 }
#saveerr[hidden] { display: none }

/* R31.5 drop-to-attach: the same corner, the same shape — a refusal the user
   can read ("remote images are disabled", "…is over the 32 MB limit"). Neutral
   border, because most of what it says is a decision, not a failure. It is
   transient (4 s): unlike a failed save nothing is left dirty behind it. */
#dropmsg { position: fixed; right: 12px; bottom: 12px; z-index: 60; max-width: 420px;
           padding: 8px 12px; border-radius: 6px; border: 1px solid var(--border-strong);
           background: var(--bg-surface); color: var(--text-chrome); font-size: 12px; line-height: 1.4;
           white-space: pre-line }
#dropmsg[hidden] { display: none }
#saveerr:not([hidden]) ~ #dropmsg { bottom: 58px }   /* never cover the dataloss banner */

/* R31.7 "Insert attachment" prompt — one line, centred over the note column,
   because it is the keyboard road to the SAME attach that a drop takes. */
#anew { position: fixed; left: 50%; top: 80px; transform: translateX(-50%); z-index: 61;
        width: 520px; max-width: 70vw; padding: 6px; border-radius: 6px;
        border: 1px solid var(--border-strong); background: var(--bg-base) }
#anew[hidden] { display: none }
#apath { width: 100%; box-sizing: border-box; background: var(--bg-sidebar); color: var(--text-chrome);
         border: 1px solid var(--border); border-radius: 4px; padding: 6px 8px; font-size: 13px }

/* ---------- R29 IMAGE EMBEDS ---------- */
/* Both engines emit the same two elements: img.vimg (bytes over the
   opensidian-img: scheme) and span.imgmiss (R29.4's banner). The CSS is shared
   deliberately — R29.7 says a divergence between reading view and live preview
   is a bug, and that includes the painted box. */
.lp .vimg, .preview .vimg { display: block; max-width: 100%; height: auto; margin: 8px 0 }
/* R29.10: NEITHER engine mints a remote <img> any more — an http(s) target
   takes the R29.4 banner below, so no img.rimg is ever created and no note can
   cause a network fetch. This rule stays as the PAINT backstop the way the CSP
   is the FETCH backstop: if some future path ever emits a bare remote <img>,
   it is sized like a vault image instead of reflowing the note. */
.lp .rimg, .preview img:not(.vimg):not(.rimg), .preview .rimg {
  display: block; max-width: 100%; height: auto; margin: 8px 0 }
/* R29.4: a full-width grey banner, NOT a broken-image icon. Reading view
   carries the wording as TEXT (the Rust renderer escapes the target into it);
   live preview carries the target in data-miss and CSS paints the identical
   sentence, because a row's textContent MUST stay equal to its source line —
   the token map walks text nodes, so banner prose inside the row would move
   every caret column after it. A REMOTE target's label is the constant
   "remote image" in both engines, never the URL (R29.10). */
.lp .imgmiss, .preview .imgmiss {
  display: block; padding: 12px 16px; margin: 8px 0; border-radius: 6px;
  background: var(--bg-surface); color: var(--text-chrome-muted); font-size: 14px; line-height: 1.4;
  user-select: none }
.lp .imgmiss::after { content: "\201C" attr(data-miss) "\201D could not be found." }
/* source mode (.lp.src) is raw markdown: markers are all revealed and the
   widget must not paint. With the caret merely ON the row the widget stays,
   exactly like the task checkbox of an edited "- [x] " line (R17.6). */
.lp.src .vimg, .lp.src .rimg, .lp.src .imgmiss { display: none }

/* ---------- R33 frameless window: the app draws its own frame ----------
   tauri.conf.json ships `decorations: false`: there is no WM title bar and no WM
   resize border, so every affordance the frame provided lives here.
   #wframe is the title bar's replacement. Obsidian does NOT stack a strip
   above its tabs — it puts the window controls inside the top chrome row
   (recon under openbox). This strip keeps the controls at the right edge
   (44px hit areas at right 4/48/92) and is 32px tall, because that is
   opensidian's tab-row height — a taller strip would shift every baseline in
   the suite.
   position:fixed, z-index above the picker / settings / modal overlays: a window
   you cannot close while a modal is open is a broken interaction. The row under
   the strip reserves its width (.wfinset, set by placeRToggle) instead of being
   covered — #rtoggle and .modebtn live at exactly that end of the row. */
#wframe { position: fixed; top: 0; right: 0; width: 140px; height: 32px;
          /* --titlebar-bg is DEFINED only by the item-8 R4 bridge (a vault
             theme declaring --titlebar-background-focused); without a bridge
             the var is undefined and the fallback keeps the theme's ribbon */
          z-index: 200; background: var(--titlebar-bg, var(--tab-container-background)); user-select: none }   /* themematch: the title area continues the tab strip */
#wframe button { position: absolute; top: 0; width: 44px; height: 32px; padding: 0;
                 background: none; border: 0; color: var(--text-chrome-muted); cursor: pointer;
                 display: flex; align-items: center; justify-content: center }
#wf-min { right: 92px }
#wf-max { right: 48px }
#wf-close { right: 4px }
#wframe button:hover { background: var(--bg-surface); color: var(--text-chrome) }
#wf-close:hover { background: var(--accent-red); color: var(--bg-ribbon) }
/* R33.4 keyboard: every control is a real <button>, so Tab reaches it and
   Enter/Space activates it — the focus ring has to be visible or "operable by
   keyboard" is a claim nobody can see. */
#wframe button:focus-visible, #wframe button:focus { outline: 2px solid var(--accent-blue);
          outline-offset: -3px; color: var(--text-chrome) }
#wframe button svg { width: 11px; height: 11px; display: block }
/* R33.10 the reservation must never make the row SPILL the window (R22). The
   140px is padding on a border-box, and a border-box width can never go below its
   own padding — so on a window narrower than the strip the row was floored at
   140px+ no matter what min-width said. Measured at 320x240 with the right sidebar
   open (sidebar = max-width 40% = 122px): #rtabs came out 338px wide and phase fuzz
   caught it ([ovf:0,0,1] [ovfe:div#rtabs.wfinset@r338>320]); adding min-width:0
   alone did NOT fix it (re-run, same 338). min(140px, …) reserves the strip's full
   width whenever the row can afford it and collapses the reservation when it
   cannot — at those sizes #rtoggle sits under the strip either way (with no
   reservation it is at the window's right edge, i.e. under the close button), so
   the clamp gives up nothing the reservation would have saved. With the clamp:
   fuzz PASSES 352 assertions over 44 sizes x 8 states, [ovf:0,0,0] everywhere. */
#rtabs.wfinset { padding-right: min(140px, calc(100% - 6px)); flex-wrap: nowrap; min-width: 0 }   /* nowrap: wrapping would DOUBLE the row height instead of clipping (R22) */
.tabbar.wfinset { padding-right: min(140px, 100%); min-width: 0 }
/* R33.3 resize: eight invisible grips. The layer is pointer-events:none so only
   the grips themselves take a press; they paint nothing, so no screenshot
   baseline and no pixel probe moves. The north grip stops at the control strip
   (right:140px) — Obsidian has no top-edge resize at all, and a 4px band over the
   buttons would eat clicks aimed at close. */
#wrz { position: fixed; inset: 0; z-index: 210; pointer-events: none }
#wrz i { position: absolute; display: block; pointer-events: auto }
#wrz i[data-d="n"] { top: 0; left: 0; right: 140px; height: 4px; cursor: n-resize }
#wrz i[data-d="s"] { bottom: 0; left: 0; right: 0; height: 4px; cursor: s-resize }
#wrz i[data-d="e"] { top: 0; bottom: 0; right: 0; width: 4px; cursor: e-resize }
#wrz i[data-d="w"] { top: 0; bottom: 0; left: 0; width: 4px; cursor: w-resize }
#wrz i[data-d="ne"] { top: 0; right: 0; width: 12px; height: 12px; cursor: ne-resize }
#wrz i[data-d="nw"] { top: 0; left: 0; width: 12px; height: 12px; cursor: nw-resize }
#wrz i[data-d="se"] { bottom: 0; right: 0; width: 12px; height: 12px; cursor: se-resize }
#wrz i[data-d="sw"] { bottom: 0; left: 0; width: 12px; height: 12px; cursor: sw-resize }

/* ---- R26 find bar (Ctrl+F), one per pane -----------------------------------
   Floats over the note column at the top right, the way Obsidian's does. It is a
   child of .content (position:relative), NOT of .lp: Ed's positional contract
   is "model line l IS g.lp.children[l], with nothing else in that list", and a
   bar inside the scroller would be row 0 for every caret computation.
   R22: max-width + overflow:hidden so the bar CLIPS in a narrow pane instead of
   reaching past the window edge (the fuzz phase counts any element that does). */
.findbar { position: absolute; top: 8px; right: 16px; z-index: 6;
           display: flex; flex-direction: column; gap: 4px;
           max-width: calc(100% - 24px); overflow: hidden;
           padding: 5px 6px; border: 1px solid var(--border); border-radius: 6px;
           background: var(--bg-elevated); box-shadow: 0 2px 8px var(--shadow-popover) }
.findbar[hidden] { display: none }
.findrow { display: flex; align-items: center; gap: 4px; min-width: 0 }
.findq { width: 160px; min-width: 0; padding: 2px 6px; border-radius: 4px;
         border: 1px solid var(--border); background: var(--bg-base);
         color: var(--text-chrome); font: 12px/1.5 var(--font-interface); outline: none }
.findq:focus { border-color: var(--border-focus-ring) }
.findcount { min-width: 46px; text-align: center; white-space: nowrap;
             color: var(--text-chrome-muted); font-size: 12px }
.findbar button { padding: 1px 5px; border: 0; border-radius: 4px; cursor: pointer;
                  background: var(--transparent); color: var(--text-chrome-muted); font-size: 12px }
.findbar button:hover { background: var(--bg-surface); color: var(--text-chrome) }
/* R26.21: the reduced bar of reading view. The arrows are hidden, not removed —
   `hidden` is what the census counts ([fvels:]), and a control that is not
   displayed offers nothing. R26.24 is why they are gone: nothing to navigate. */
.findbar button[hidden] { display: none }
/* R26.8: every match is marked; the current one is the one you are standing on. */
span.fmk { background: var(--highlight-yellow); border-radius: 2px }
span.fmk.fcur { background: var(--tag-bg-hover); outline: 1px solid var(--accent-yellow) }
/* R25.13d: the search-result flash. Obsidian marks it with the class
   `is-flashing`, a translucent yellow wash, NO animation and NO timer (C4);
   no colour literal is copied here — a hardcoded colour is a colour no theme can
   reach (scripts/lint-colors.sh). The class name is the measurement's; the
   value is this app's yellow highlight token, the same one .fmk and .navflash
   use, so the flash follows every theme. */
span.is-flashing { background: var(--highlight-yellow) }
/* W4 (goal wsrestore): a leaf this app has no view for (canvas, pdf, a plugin's
   view) is kept in the layout as a placeholder tab, like Obsidian's
   unavailable-plugin view. The message fills the pane the editor would. */
.unkview { flex: 1 1 0; display: flex; flex-direction: column; align-items: center;
           justify-content: center; gap: 6px; color: var(--text-muted); padding: 24px;
           text-align: center; user-select: text }
.unkview[hidden] { display: none }
.unkview .unkt { font-size: 1.1em; color: var(--text-normal, inherit) }
/* themematch: text selection paints from --text-selection (the accent
   at .2 light / .33 dark, see the tokens); without this the engine default blue showed */
::selection { background-color: var(--text-selection) }

/* ---------- pdfembed: PDF EMBED FRAME (both engines, one element) ----------
   Stock geometry from recon (harness docs/recon-pdfembed/README.md): full
   column width, 1 px border, 39 px toolbar, height set inline by Ed.pdfSize
   (w x aspect(page 1) + 20, or #height=N + 38). Text in the frame is painted
   from attributes: an LP row must hold no text node its source does not. */
.lp .pdf-embed, .preview .pdf-embed {
  display: flex; flex-direction: column; width: 100%; box-sizing: border-box; margin: 8px 0;
  border: 1px solid var(--border); border-radius: 4px; overflow: hidden; background: var(--bg-surface);
  user-select: none; text-indent: 0; white-space: normal }
.pdf-embed .pdf-toolbar {
  flex: 0 0 39px; height: 39px; box-sizing: border-box; display: flex; align-items: center; gap: 6px;
  padding: 0 8px; border-bottom: 1px solid var(--border); color: var(--text-chrome-muted); font-size: 13px }
.pdf-embed .pdf-counter { flex: 1 1 auto }
.pdf-embed .pdf-counter::before { content: attr(data-counter) }
.pdf-embed .pdf-toolbar button {
  width: 26px; height: 26px; padding: 0; border: 0; border-radius: 4px; background: none;
  color: inherit; font-size: 16px; line-height: 26px; cursor: pointer }
.pdf-embed .pdf-toolbar button:hover { background: var(--bg-base) }
.pdf-embed .pdf-zoom-out::before { content: "\2212" }
.pdf-embed .pdf-zoom-in::before { content: "+" }
.pdf-embed .pdf-container {
  position: relative; flex: 1 1 auto; min-height: 0; overflow: auto; overscroll-behavior: contain;
  display: flex; flex-direction: column; align-items: flex-start; gap: 8px; padding: 0 }
.pdf-embed .pdf-page { display: block; flex: 0 0 auto; width: 100%; height: auto; max-width: none;
  margin: 0 auto; background: none }
.pdf-embed.mod-broken .pdf-container::before {
  content: "This PDF could not be rendered."; margin: auto; color: var(--text-chrome-muted); font-size: 14px }
/* stock's missing-file banner is 37 px tall; the R29.4 banner shrunk to match */
.lp .imgmiss.pdfmiss, .preview .imgmiss.pdfmiss { padding: 8px 16px }
.lp.src .pdf-embed { display: none }
