diff --git a/client/src/index.css b/client/src/index.css index b8394a0d5f..9971924a64 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -128,6 +128,27 @@ --port-chart-3: 245 158 11; --port-chart-4: 236 72 153; --port-chart-grid: 64 64 64; + /* Surface & control tokens — same "tokens over per-theme selector blocks" + pattern as the heading tokens above. Card border/alpha default to the + generic border tokens so an unset theme renders identically to the plain + `.border-port-border` rule; the rest default to a true no-op (`none` / + `auto`) so an unset theme paints nothing extra. */ + --port-card-border-color: var(--port-border); + --port-card-border-alpha: var(--port-border-alpha); + --port-card-surface-image: none; + --port-card-surface-size: auto; + --port-focus-shadow: 0 0 0 4px rgb(var(--port-focus-ring) / 0.2); + /* Only read by the theme-scoped focus rule below (Lumen Glass, Lumen Glass + Day, Kestrel Neon) — deliberately separate from --port-focus-shadow so a + theme's stronger glow can't leak onto the [role="button"]/[tabindex="0"] + elements the generic rule also covers. */ + --port-focus-glow: none; + --port-nav-active-rule: none; + /* `inherit`, not `none` — `text-shadow` is naturally inherited, and Black + ICE's body-wide glow used to reach this element through that inheritance. + Declaring `none` here would silently cancel it for every theme. */ + --port-nav-active-glow: inherit; + --port-code-glow: none; } /* Self-hosted UI/mono webfonts. @@ -327,6 +348,15 @@ html[data-port-theme] kbd { font-family: var(--port-font-mono); } +/* Code/input glow + caret, tokenized (#6248) — was Kestrel-only (a + `data-port-theme-family` rule plus a per-mode text-shadow); the caret now + tracks the accent everywhere, and `--port-code-glow` (default `none`) lets + any theme add the text-shadow Black ICE (both modes) and Kestrel Neon use. */ +html[data-port-theme] :is(pre, code, textarea, input) { + caret-color: rgb(var(--port-accent)); + text-shadow: var(--port-code-glow); +} + html[data-port-theme] h1, html[data-port-theme] h2, html[data-port-theme] h3 { @@ -862,6 +892,29 @@ html[data-port-theme] :is( box-shadow: var(--port-shadow-card); } +/* Card top wash, tokenized (#6248) — was 8 near-identical per-theme selector + blocks differing only in a border alpha and a background-image. The image + half is safe to share unconditionally (default `none`/`auto` is a true + no-op). The border half is NOT shared here — see the theme-scoped rule + below: an unconditional `border-color` here would out-specificity a + component's own semantic border class (`border-port-accent` for a + "selected" card, `border-port-error`, …) on every theme, not just the ones + that used to override it. */ +html[data-port-theme] :is(.bg-port-card, [class~="bg-port-card/50"]):is(.border, .rounded, .rounded-lg, .rounded-xl, .rounded-2xl) { + background-image: var(--port-card-surface-image); + background-size: var(--port-card-surface-size); +} + +/* Card border tint, tokenized (#6248) — scoped to the exact themes that + overrode it before (specificity matters here: this selector shape is what + let the theme tint win over `.border-port-border` while still losing to a + component's own semantic border class at equal specificity elsewhere). A + theme not listed here never applied this override and still doesn't. */ +html:is([data-port-theme="lumen-glass"], [data-port-theme="lumen-glass-day"], [data-port-theme="black-ice-terminal"], [data-port-theme="black-ice-terminal-day"], [data-port-theme-family="kestrel"]) + :is(.bg-port-card, [class~="bg-port-card/50"]):is(.border, .rounded, .rounded-lg, .rounded-xl, .rounded-2xl) { + border-color: rgb(var(--port-card-border-color) / var(--port-card-border-alpha)) !important; +} + html[data-port-theme] :is(.shadow-lg, .shadow-xl, .shadow-2xl) { box-shadow: var(--port-shadow-elevated) !important; } @@ -894,6 +947,15 @@ html[data-port-theme] aside nav [aria-current="page"] { background: var(--port-active-bg) !important; } +/* Active-nav accent rule + glow, tokenized (#6248) — was 4 near-identical + per-theme `aside nav [aria-current="page"]` blocks (a border-left color and, + for Kestrel Neon only, a text-shadow). Doesn't touch `background` — that + stays on the `!important` rule above. */ +html[data-port-theme] aside nav [aria-current="page"] { + border-left: var(--port-nav-active-rule); + text-shadow: var(--port-nav-active-glow); +} + /* Tonal semantic surfaces keep their alpha/background treatment, but legacy literal white/black ink must follow the semantic foreground role. This is the safe pairing for /5 through /30 chips; higher-opacity fills use the @@ -1023,44 +1085,14 @@ html[data-port-theme="lumen-glass"] main { linear-gradient(180deg, rgb(255 255 255 / 0.035), transparent 240px); } -html[data-port-theme="lumen-glass"] :is(.bg-port-card, [class~="bg-port-card/50"]):is(.border, .rounded, .rounded-lg, .rounded-xl, .rounded-2xl) { - border-color: rgb(var(--port-border) / 0.28) !important; -} - -html[data-port-theme="lumen-glass"] :is(input, textarea, select):focus, -html[data-port-theme="lumen-glass"] button:focus-visible, -html[data-port-theme="lumen-glass"] a:focus-visible { - box-shadow: 0 0 0 4px rgb(var(--port-focus-ring) / 0.18), 0 0 36px rgb(var(--port-focus-ring) / 0.12); -} - html[data-port-theme="black-ice-terminal"] body { text-shadow: 0 0 12px rgb(var(--port-accent) / 0.08); } -html[data-port-theme="black-ice-terminal"] :is(.bg-port-card, [class~="bg-port-card/50"]):is(.border, .rounded, .rounded-lg, .rounded-xl, .rounded-2xl) { - border-color: rgb(var(--port-border) / 0.34) !important; - background-image: linear-gradient(180deg, rgb(var(--port-accent) / 0.035), transparent 38px); -} - html[data-port-theme="black-ice-terminal"] :is(h1, h2, h3, h4, .text-lg, .text-xl, .text-2xl, .text-3xl) { color: rgb(var(--port-text)); } -html[data-port-theme="black-ice-terminal"] :is(pre, code, textarea, input) { - text-shadow: 0 0 10px rgb(var(--port-accent) / 0.1); -} - -html[data-port-theme="black-ice-terminal"] aside nav [aria-current="page"] { - border-left: 2px solid rgb(var(--port-accent)); -} - -html[data-port-theme="blueprint-ops"] :is(.bg-port-card, [class~="bg-port-card/50"]):is(.border, .rounded, .rounded-lg, .rounded-xl, .rounded-2xl) { - background-image: - linear-gradient(90deg, rgb(var(--port-border) / 0.045) 1px, transparent 1px), - linear-gradient(180deg, rgb(var(--port-border) / 0.035) 1px, transparent 1px); - background-size: 20px 20px; -} - html[data-port-theme="blueprint-ops"] :is(h1, h2, h3) { border-left: 3px solid rgb(var(--port-accent) / 0.72); padding-left: 0.55rem; @@ -1367,32 +1399,23 @@ html[data-port-theme="classic-noon"] .bg-port-card { } /* === Lumen Glass Day === */ -/* Same card set as the base elevation rule above — every fill opacity, not just - `/50` — so this theme's softened border lands on all of its cards. */ +/* This theme's card-border coverage predates the shared card-border rule + above and was always wider — every `bg-port-card/NN` fill opacity, not just + `.bg-port-card`/`/50` — so it stays as its own selector rather than + widening the shared rule (and changing the other 7 themes' rarer alpha + variants along with it). Still token-driven now: no literal alpha left + here. `box-shadow` isn't restated — the wide-list elevation rule above + already applies `--port-shadow-card` to every one of these classes. */ html[data-port-theme="lumen-glass-day"] :is( - .bg-port-card, [class~="bg-port-card/30"], [class~="bg-port-card/40"], - [class~="bg-port-card/50"], [class~="bg-port-card/60"], [class~="bg-port-card/70"], [class~="bg-port-card/80"], [class~="bg-port-card/90"], [class~="bg-port-card/95"] ):is(.border, .rounded, .rounded-lg, .rounded-xl, .rounded-2xl) { - border-color: rgb(var(--port-border) / 0.22) !important; - box-shadow: var(--port-shadow-card); -} - -html[data-port-theme="lumen-glass-day"] :is(input, textarea, select):focus, -html[data-port-theme="lumen-glass-day"] button:focus-visible, -html[data-port-theme="lumen-glass-day"] a:focus-visible { - box-shadow: 0 0 0 3px rgb(var(--port-focus-ring) / 0.22), 0 0 24px rgb(var(--port-focus-ring) / 0.16); -} - -html[data-port-theme="lumen-glass-day"] aside nav [aria-current="page"] { - background: rgb(var(--port-accent) / 0.16); - color: rgb(var(--port-accent)); + border-color: rgb(var(--port-card-border-color) / var(--port-card-border-alpha)) !important; } html[data-port-theme="lumen-glass-day"] body::before { @@ -1436,27 +1459,7 @@ html[data-port-theme="black-ice-terminal-day"] body { text-shadow: 0 0 8px rgb(var(--port-accent) / 0.04); } -html[data-port-theme="black-ice-terminal-day"] :is(.bg-port-card, [class~="bg-port-card/50"]):is(.border, .rounded, .rounded-lg, .rounded-xl, .rounded-2xl) { - border-color: rgb(var(--port-border) / 0.32) !important; - background-image: linear-gradient(180deg, rgb(255 255 255 / 0.5), transparent 38px); -} - -html[data-port-theme="black-ice-terminal-day"] :is(pre, code, textarea, input) { - text-shadow: 0 0 6px rgb(var(--port-accent) / 0.06); -} - -html[data-port-theme="black-ice-terminal-day"] aside nav [aria-current="page"] { - border-left: 2px solid rgb(var(--port-accent)); -} - /* === Drafting Paper (Blueprint Day) === */ -html[data-port-theme="blueprint-ops-day"] :is(.bg-port-card, [class~="bg-port-card/50"]):is(.border, .rounded, .rounded-lg, .rounded-xl, .rounded-2xl) { - background-image: - linear-gradient(90deg, rgb(var(--port-border) / 0.05) 1px, transparent 1px), - linear-gradient(180deg, rgb(var(--port-border) / 0.04) 1px, transparent 1px); - background-size: 20px 20px; -} - html[data-port-theme="blueprint-ops-day"] :is(h1, h2, h3) { border-left: 3px solid rgb(var(--port-accent) / 0.72); padding-left: 0.55rem; @@ -1593,47 +1596,23 @@ html[data-port-theme-effects~="glitch"] h1 { } /* === Kestrel (CRT boot terminal) — shared by Neon and Dawn === */ -html[data-port-theme-family="kestrel"] :is(.bg-port-card, [class~="bg-port-card/50"]):is(.border, .rounded, .rounded-lg, .rounded-xl, .rounded-2xl) { - border-color: rgb(var(--port-accent-2) / var(--port-kestrel-card-border-alpha)) !important; - background-image: linear-gradient(180deg, rgb(var(--port-accent) / 0.06), transparent 32px); -} - -html[data-port-theme-family="kestrel"] :is(pre, code, textarea, input) { - caret-color: rgb(var(--port-accent)); -} - -html[data-port-theme-family="kestrel"] aside nav [aria-current="page"] { - border-left: 2px solid rgb(var(--port-accent-2)); +/* Card border color/image and the nav-active rule are identical for both + Kestrel themes, so they live once in kestrelShellTokens (portosThemes.js) + rather than as a `data-port-theme-family` selector — the shared rules above + already read them. Only the border ALPHA differs night vs. dawn, so it's + the one thing still set per-theme here (was --port-kestrel-card-border-alpha). */ +html[data-port-theme="kestrel-neon"] { + --port-card-border-alpha: 0.3; } -/* === Kestrel Neon (night): the glow === */ -html[data-port-theme="kestrel-neon"] { - --port-kestrel-card-border-alpha: 0.3; +html[data-port-theme="kestrel-neon-day"] { + --port-card-border-alpha: 0.26; } html[data-port-theme="kestrel-neon"] body { text-shadow: 0 0 10px rgb(var(--port-accent) / 0.08); } -html[data-port-theme="kestrel-neon"] :is(pre, code, textarea, input) { - text-shadow: 0 0 8px rgb(var(--port-accent) / 0.25); -} - -html[data-port-theme="kestrel-neon"] :is(input, textarea, select):focus, -html[data-port-theme="kestrel-neon"] button:focus-visible, -html[data-port-theme="kestrel-neon"] a:focus-visible { - box-shadow: 0 0 0 1px rgb(var(--port-focus-ring) / 0.7), 0 0 18px rgb(var(--port-focus-ring) / 0.35); -} - -html[data-port-theme="kestrel-neon"] aside nav [aria-current="page"] { - text-shadow: 0 0 8px rgb(var(--port-accent) / 0.6); -} - -/* === Kestrel Dawn (day) === */ -html[data-port-theme="kestrel-neon-day"] { - --port-kestrel-card-border-alpha: 0.26; -} - /* Accessibility: Global focus styles for keyboard navigation */ *:focus-visible { outline: 2px solid rgb(var(--port-focus-ring)); @@ -1663,7 +1642,25 @@ a:focus-visible, [tabindex="0"]:focus-visible { outline: 2px solid rgb(var(--port-focus-ring)); outline-offset: 2px; - box-shadow: 0 0 0 4px rgb(var(--port-focus-ring) / 0.2); + box-shadow: var(--port-focus-shadow); +} + +/* Field + button/link focus glow, tokenized (#6248) — was 3 per-theme copies + of this exact `:is(input, textarea, select):focus, button:focus-visible, + a:focus-visible` block. Kept theme-scoped rather than folded into the bare + `button:focus-visible` rule above: that rule's specificity is lower than + `.bg-port-accent`'s `box-shadow: var(--port-shadow-interactive)`, so an + accent-colored button would lose its focus glow to the interactive shadow. + Restoring the original per-theme selector shape (specificity, not the + values) keeps these three themes' stronger glow winning, same as before. + A DIFFERENT token than the generic rule's `--port-focus-shadow` on purpose + — that rule also covers `[role="button"]`/`[tabindex="0"]`, which this + selector (matching the original) never did, so reusing the same token + would leak the stronger glow onto elements it was never meant to reach. */ +html:is([data-port-theme="lumen-glass"], [data-port-theme="lumen-glass-day"], [data-port-theme="kestrel-neon"]) :is(input, textarea, select):focus, +html:is([data-port-theme="lumen-glass"], [data-port-theme="lumen-glass-day"], [data-port-theme="kestrel-neon"]) button:focus-visible, +html:is([data-port-theme="lumen-glass"], [data-port-theme="lumen-glass-day"], [data-port-theme="kestrel-neon"]) a:focus-visible { + box-shadow: var(--port-focus-glow); } /* DRY pointer cursor for interactive elements. Tailwind v4 dropped the diff --git a/client/src/themes/portosThemes.js b/client/src/themes/portosThemes.js index 12a4ba5adb..6212b69f2c 100644 --- a/client/src/themes/portosThemes.js +++ b/client/src/themes/portosThemes.js @@ -74,6 +74,11 @@ const kestrelShellTokens = { '--port-body-overlay-opacity': '1', '--port-app-backdrop': 'transparent', '--port-control-border': 'rgb(var(--port-border) / 0.3)', + // Card border channel + top wash + nav-active rule are identical night vs. + // dawn (#6248) — only the border ALPHA differs, set per-theme below. + '--port-card-border-color': 'var(--port-accent-2)', + '--port-card-surface-image': 'linear-gradient(180deg, rgb(var(--port-accent) / 0.06), transparent 32px)', + '--port-nav-active-rule': '2px solid rgb(var(--port-accent-2))', '--port-fx-scanline-period': '3px', '--port-fx-overlay-blend': 'multiply', }; @@ -287,6 +292,8 @@ export const THEMES = { '--port-font-ui': "Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif", '--port-font-display': 'var(--port-font-ui)', '--port-font-mono': "'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace", + '--port-card-border-alpha': '0.28', + '--port-focus-glow': '0 0 0 4px rgb(var(--port-focus-ring) / 0.18), 0 0 36px rgb(var(--port-focus-ring) / 0.12)', '--port-shadow-card': '0 22px 55px rgb(0 0 0 / 0.28), inset 0 1px 0 rgb(255 255 255 / 0.08)', '--port-shadow-elevated': '0 28px 80px rgb(0 0 0 / 0.42), inset 0 1px 0 rgb(255 255 255 / 0.12)', '--port-shadow-interactive': '0 0 0 1px rgb(var(--port-accent) / 0.18), 0 0 28px rgb(var(--port-accent) / 0.12)', @@ -369,6 +376,8 @@ export const THEMES = { '--port-font-ui': "Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif", '--port-font-display': 'var(--port-font-ui)', '--port-font-mono': "'IBM Plex Mono', 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace", + '--port-card-border-alpha': '0.22', + '--port-focus-glow': '0 0 0 3px rgb(var(--port-focus-ring) / 0.22), 0 0 24px rgb(var(--port-focus-ring) / 0.16)', '--port-shadow-card': 'inset 0 1px 0 0 rgb(255 255 255 / 0.6), 0 10px 30px -12px rgb(8 80 110 / 0.16), 0 2px 6px -2px rgb(8 80 110 / 0.08)', '--port-shadow-elevated': 'inset 0 1px 0 0 rgb(255 255 255 / 0.7), 0 22px 56px -16px rgb(8 80 110 / 0.24), 0 6px 14px -4px rgb(8 80 110 / 0.12)', '--port-shadow-interactive': '0 0 0 1px rgb(var(--port-accent) / 0.3), 0 0 28px rgb(var(--port-accent) / 0.16)', @@ -458,6 +467,10 @@ export const THEMES = { // The shared heading rule sets text-shadow explicitly, so restate the // body glow here or headings would lose what they used to inherit. '--port-heading-glow': '0 0 12px rgb(var(--port-accent) / 0.08)', + '--port-card-border-alpha': '0.34', + '--port-card-surface-image': 'linear-gradient(180deg, rgb(var(--port-accent) / 0.035), transparent 38px)', + '--port-nav-active-rule': '2px solid rgb(var(--port-accent))', + '--port-code-glow': '0 0 10px rgb(var(--port-accent) / 0.1)', '--port-shadow-card': '0 0 0 1px rgb(var(--port-border) / 0.18), 0 0 26px rgb(var(--port-accent) / 0.08)', '--port-shadow-elevated': '0 0 0 1px rgb(var(--port-border) / 0.3), 0 0 48px rgb(var(--port-accent) / 0.16), 0 18px 60px rgb(0 0 0 / 0.62)', '--port-shadow-interactive': '0 0 0 1px rgb(var(--port-accent) / 0.45), 0 0 20px rgb(var(--port-accent) / 0.22)', @@ -544,6 +557,10 @@ export const THEMES = { '--port-font-display': "'GeistPixelGrid', 'SFMono-Regular', Consolas, monospace", '--port-font-mono': "'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace", '--port-heading-glow': '0 0 8px rgb(var(--port-accent) / 0.04)', + '--port-card-border-alpha': '0.32', + '--port-card-surface-image': 'linear-gradient(180deg, rgb(255 255 255 / 0.5), transparent 38px)', + '--port-nav-active-rule': '2px solid rgb(var(--port-accent))', + '--port-code-glow': '0 0 6px rgb(var(--port-accent) / 0.06)', '--port-shadow-card': '0 0 0 1px rgb(var(--port-border) / 0.18), 0 4px 14px -6px rgb(40 60 30 / 0.18)', '--port-shadow-elevated': '0 0 0 1px rgb(var(--port-border) / 0.28), 0 12px 36px -10px rgb(40 60 30 / 0.22)', '--port-shadow-interactive': '0 0 0 1px rgb(var(--port-accent) / 0.5)', @@ -629,6 +646,8 @@ export const THEMES = { '--port-font-ui': "'Avenir Next', Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif", '--port-font-display': 'var(--port-font-ui)', '--port-font-mono': "'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace", + '--port-card-surface-image': 'linear-gradient(90deg, rgb(var(--port-border) / 0.045) 1px, transparent 1px), linear-gradient(180deg, rgb(var(--port-border) / 0.035) 1px, transparent 1px)', + '--port-card-surface-size': '20px 20px', '--port-shadow-card': '0 0 0 1px rgb(var(--port-border) / 0.1), 0 12px 28px rgb(0 0 0 / 0.2)', '--port-shadow-elevated': '0 0 0 1px rgb(var(--port-border) / 0.18), 0 22px 54px rgb(0 0 0 / 0.38)', '--port-shadow-interactive': '0 0 0 1px rgb(var(--port-accent) / 0.35), 0 0 0 4px rgb(var(--port-accent) / 0.08)', @@ -711,6 +730,8 @@ export const THEMES = { '--port-font-ui': "'Avenir Next', Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif", '--port-font-display': 'var(--port-font-ui)', '--port-font-mono': "'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace", + '--port-card-surface-image': 'linear-gradient(90deg, rgb(var(--port-border) / 0.05) 1px, transparent 1px), linear-gradient(180deg, rgb(var(--port-border) / 0.04) 1px, transparent 1px)', + '--port-card-surface-size': '20px 20px', '--port-shadow-card': '0 0 0 1px rgb(var(--port-border) / 0.14), 0 8px 22px -8px rgb(15 23 42 / 0.12)', '--port-shadow-elevated': '0 0 0 1px rgb(var(--port-border) / 0.22), 0 22px 54px -16px rgb(15 23 42 / 0.22)', '--port-shadow-interactive': '0 0 0 1px rgb(var(--port-accent) / 0.45), 0 0 0 4px rgb(var(--port-accent) / 0.1)', @@ -783,6 +804,9 @@ export const THEMES = { '--port-card-min-alpha': '0.88', '--port-overlay-alpha': '0.8', '--port-heading-glow': '0 0 12px rgb(var(--port-accent) / 0.5)', + '--port-focus-glow': '0 0 0 1px rgb(var(--port-focus-ring) / 0.7), 0 0 18px rgb(var(--port-focus-ring) / 0.35)', + '--port-code-glow': '0 0 8px rgb(var(--port-accent) / 0.25)', + '--port-nav-active-glow': '0 0 8px rgb(var(--port-accent) / 0.6)', '--port-shadow-card': '0 0 0 1px rgb(var(--port-accent-2) / 0.14), 0 0 34px rgb(var(--port-accent-2) / 0.08), inset 0 0 48px rgb(0 0 0 / 0.45)', '--port-shadow-elevated': '0 0 0 1px rgb(var(--port-accent) / 0.28), 0 0 44px rgb(var(--port-accent) / 0.14), 0 18px 60px rgb(0 0 0 / 0.7)', '--port-shadow-interactive': '0 0 0 1px rgb(var(--port-accent) / 0.45), 0 0 18px rgb(var(--port-accent) / 0.22)', diff --git a/docs/themes/README.md b/docs/themes/README.md index 48ec46b816..47e82e1483 100644 --- a/docs/themes/README.md +++ b/docs/themes/README.md @@ -77,6 +77,16 @@ Headings read `--port-heading-tracking` and `--port-heading-glow` (h1–h3) plus Black ICE Terminal (both modes) uses `scanlines`; Kestrel Neon uses all five, and Kestrel Dawn `scanlines` + `grid-floor`. +## Surface & control tokens + +The same "tokens over per-theme selector blocks" pattern covers the shared card/focus/nav/code rules — with one twist: `border-color` and `box-shadow` are properties a component can also set for its own reasons (a card's "selected"/"error" border class, an accent button's interactive shadow), so a theme's override has to keep winning against those *without* also winning for every theme that never opted in. Where that matters, the token-driven rule stays scoped to the exact theme(s) that use it (`html:is([data-port-theme="…"], …) …`) rather than becoming unconditional — same values-not-selectors win, without a specificity regression. + +`--port-card-surface-image` / `--port-card-surface-size` (a card's `background-image`/`background-size`, both a no-op by default) are safe to share unconditionally. `--port-card-border-color` / `--port-card-border-alpha` (a card's border, `rgb(var(--port-card-border-color) / var(--port-card-border-alpha))`) are read by a rule scoped to the themes that override them (Lumen Glass, Lumen Glass Day, Black ICE Terminal both modes, Kestrel both modes — Lumen Glass Day's wider `bg-port-card/NN` coverage beyond `.bg-port-card`/`/50` is its own separate selector, predating this rule). + +`--port-focus-shadow` is the generic `button:focus-visible`/`a:focus-visible`/`[role="button"]`/`[tabindex="0"]` rule's default box-shadow and is never overridden per theme. Lumen Glass, Lumen Glass Day, and Kestrel Neon's stronger glow is a *different* token, `--port-focus-glow` (`none` by default, read only by their own theme-scoped `:is(input, textarea, select):focus, button:focus-visible, a:focus-visible` rule) — reusing `--port-focus-shadow` there would leak the glow onto `[role="button"]`/`[tabindex="0"]` elements the original per-theme rule never touched, and unscoping it entirely would lose to `.bg-port-accent`'s interactive shadow on an accent-colored button (a `.bg-port-accent` class carries higher specificity than the bare `button:focus-visible` rule). + +`--port-nav-active-rule` (the active sidebar link's `border-left`) and `--port-nav-active-glow` (its `text-shadow`, defaulted to `inherit` rather than `none` since `text-shadow` is naturally inherited — Black ICE's body-wide glow reaches the active link through that) are shared unconditionally; `border-left` and `text-shadow` aren't properties any component sets on that element today. `--port-code-glow` (a `text-shadow` on `pre`/`code`/`textarea`/`input`, `none` by default) is likewise unconditional. `caret-color: rgb(var(--port-accent))` on that same selector is unconditional too — every theme gets an accent-colored text cursor, not just Kestrel. + ## New Feature Checklist Before merging UI work: