Items 2-5 of #3072. Item 1 (the per-row sweep inserts) is separate. 2. .fc-bad was not merely duplicated — it is .fc-weak under a second name. Both local definitions were `color: rgb(var(--v-theme-error))`, identical to the global .fc-weak, and GpuAgentCard was already using .fc-weak to colour exactly what GpuActivityPanel coloured .fc-bad (an errored count, red when non-zero). So rather than promoting a synonym to app.css, both call sites now use .fc-weak and the local defs are gone. app.css's status-colour comment records why there is no .fc-bad, next to the existing note on why .fc-ok is deliberately NOT global. 3. GalleryItem.vue's obsidian literals now use --v-theme-background, which IS obsidian (vuetify-theme.js maps background -> surfaces. obsidian). Preferred over --fc-chrome-rgb: same value, but that variable is named for the nav fade, not for the palette entry. The ticket said these were the only three real uses in the tree. They are not — GalleryItem itself had two more in the artist-label gradient (fixed here, so the file is now consistent), and ~13 more live in SeriesView, SeriesReaderView, ImageViewer, ArtistHeader, ExploreView and GalleryFilterBar. Those are a separate sweep, filed rather than folded in here. 4. The attachment download path had two hand-formatted copies. One definition now, `attachment_download_url`, next to the model both serializers already import. The test pins it by MATCHING the built path against the app's real URL map rather than comparing to a literal — a string-equality test would still pass after someone renamed the route, which is the drift the helper exists to prevent. 5. Extension API key now compares with hmac.compare_digest. Compared as BYTES, not str: compare_digest's str form raises TypeError on non-ASCII, and this value comes straight from an attacker-controlled header, so the str form would turn a junk key into a 500 instead of a 403. Low stakes either way — the API is unauthenticated-by-design on a LAN — but it costs nothing. Refs #3072
135 lines
7.5 KiB
CSS
135 lines
7.5 KiB
CSS
/* App-global overrides. Imported in main.js after 'vuetify/styles', but
|
||
Vite's production bundler reorders CSS chunks so source order is not a
|
||
reliable winner against node_modules CSS at equal specificity. Rules
|
||
here either out-specify Vuetify's default or use !important on the
|
||
exact properties that need to win. */
|
||
|
||
/* Tooltip readability fix (operator-flagged 2026-05-28).
|
||
Vuetify's default v-tooltip pairs `on-surface-variant` TEXT with an
|
||
`surface-variant` BACKGROUND. FC's theme deliberately maps
|
||
`on-surface-variant` to vellum (#C2BFB4 — a light cream, correct for
|
||
muted captions/hints on the dark page) but never defines
|
||
`surface-variant`, so Vuetify auto-generates a light-ish background:
|
||
light text on light bg → near-white-on-near-white, unreadable.
|
||
|
||
Tooltips want the inverse of muted body text — a dark, slightly
|
||
elevated panel with the HIGH-contrast parchment text. Fixing it here
|
||
(not by changing on-surface-variant) keeps captions/empty-states
|
||
correct while making every tooltip in the app legible.
|
||
|
||
`!important` on the contrast properties: this rule ties Vuetify's
|
||
default at specificity (.v-tooltip > .v-overlay__content), and in a
|
||
Vite production build the node_modules CSS can land after app.css in
|
||
the final stylesheet regardless of import order, so source-order wins
|
||
aren't reliable. !important removes that fragility for the two
|
||
properties whose drift made tooltips unreadable; the cosmetic border
|
||
and shadow don't need it (Vuetify doesn't set them). Operator
|
||
re-flagged 2026-05-29 on the deployed :latest after PR #33. */
|
||
.v-tooltip > .v-overlay__content {
|
||
background: rgb(var(--v-theme-surface-bright)) !important; /* slate #2C313A */
|
||
color: rgb(var(--v-theme-on-surface)) !important; /* parchment #E8E4D8 */
|
||
border: 1px solid rgb(var(--v-theme-on-surface-variant) / 0.25);
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);
|
||
}
|
||
|
||
/* Canonical muted/secondary text token (DRY pattern sweep 2026-06-09).
|
||
Was redefined identically in 12 component <style scoped> blocks; now one
|
||
global utility. Muted text uses the explicit on-surface-variant (vellum)
|
||
token, NOT opacity — Vuetify's text-medium-emphasis is opacity-based and is
|
||
deliberately not used here. `.fc-muted` is a custom class Vuetify never
|
||
emits, so no specificity/reorder fight — no !important needed. */
|
||
.fc-muted { color: rgb(var(--v-theme-on-surface-variant)); }
|
||
|
||
/* Section sub-heading in settings cards (DRY pass #161): was redefined
|
||
identically in 4 cards, and TranslationCard used the class with NO local def
|
||
so its section headers rendered unstyled. Now one global utility. */
|
||
.fc-section-h {
|
||
font-size: 13px; font-weight: 700; letter-spacing: 0.03em;
|
||
text-transform: uppercase; color: rgb(var(--v-theme-on-surface));
|
||
}
|
||
|
||
/* Status text colours (DRY pass #161): fc-good = success, fc-weak = error,
|
||
consolidated from the GPU / heads cards. fc-ok is intentionally NOT global —
|
||
it means on-surface in HeadsCard but success in QueuesTable.
|
||
|
||
No `.fc-bad` (#3072): it was defined locally and identically in the Downloads
|
||
and GPU activity panels, and it is fc-weak under a second name — GpuAgentCard
|
||
and GpuActivityPanel were colouring the same "errored" count with different
|
||
class names. Both now use fc-weak. Reach for fc-weak, not a new synonym. */
|
||
.fc-good { color: rgb(var(--v-theme-success)); }
|
||
.fc-weak { color: rgb(var(--v-theme-error)); }
|
||
|
||
/* Vuetify 4 dropped its global CSS reset (normalisation moved into each
|
||
component). FC's layouts assumed the reset zeroed margins on text elements, so
|
||
restore just that — the "minimal reset" from the v4 upgrade guide — inside
|
||
Vuetify's own reset layer, which is low precedence so component + app styles
|
||
still win over it. Batch-4 Vuetify 3→4 (#1449). */
|
||
@layer vuetify-core.reset {
|
||
ul, ol, figure, details, summary { padding: 0; margin: 0; }
|
||
h1, h2, h3, h4, h5, h6, p { margin: 0; }
|
||
}
|
||
|
||
/* Active-tab indicator (operator-flagged 2026-07-13 in the Vuetify-4 review): v4's
|
||
MD3 v-tab "slider" underline renders wider than the tab and floats below it. The
|
||
active tab's TEXT is already accent-coloured (color="accent"), so drop the slider
|
||
and mark the active tab with a subtle accent fill + rounded top — a clean,
|
||
unambiguous highlight app-wide (Subscriptions / Browse / Settings / Series). */
|
||
.v-tab__slider { display: none !important; }
|
||
.v-tab[aria-selected="true"],
|
||
.v-tab.v-tab--selected {
|
||
background: rgb(var(--v-theme-accent) / 0.12);
|
||
border-radius: 8px 8px 0 0;
|
||
}
|
||
|
||
/* --- Continuous chrome fade (operator-asked 2026-07-13: "group the sub-nav as
|
||
part of the nav and use a single gradient in them"). ------------------------
|
||
The TopNav and any sticky sub-header pinned directly beneath it (Gallery's
|
||
filter bar, the Browse/Series/Settings/Subscriptions tabs bars) used to each
|
||
paint their OWN dark-to-transparent gradient (or a solid band), so the fade
|
||
read as happening TWICE — dark, fade out, then dark again. Instead the two
|
||
share ONE obsidian fade: the nav paints the TOP half (opaque → the seam
|
||
alpha) and the sub-header paints the CONTINUATION (seam alpha → transparent)
|
||
over its own height. Both reference --fc-chrome-seam, so the alphas meet
|
||
exactly at the 64px boundary — no re-darkening, no doubling, one gradient.
|
||
|
||
--fc-chrome-seam is the single knob: raise it for a heavier sub-header (more
|
||
legible tabs/controls over scrolling content), lower it for a lighter fade. */
|
||
:root {
|
||
--fc-chrome-rgb: 20, 23, 26; /* obsidian #14171A — matches the TopNav */
|
||
/* Alpha where the nav hands off to the sub-header — also the "hold" level of
|
||
the fade. The chrome stays fairly opaque (0.92 → this) through the bulk of
|
||
its height, then drops to transparent in a small eased section at the very
|
||
bottom (see the multi-stop gradients), so it reads as a slow falloff that
|
||
tails off softly rather than a straight line to a hard edge (operator
|
||
2026-07-13). Raise for heavier/more-legible chrome, lower for a lighter fade. */
|
||
--fc-chrome-seam: 0.68;
|
||
/* Actual TopNav height, measured live (ResizeObserver in TopNav.vue) and used
|
||
by full-height workspaces (Explore/Subscriptions: calc(100vh - var)) and by
|
||
every sticky sub-header pinned beneath the nav (top: var). This was a
|
||
hardcoded 64px in ~6 places; Vuetify 4's MD3 sizing made the real nav a
|
||
different height, so the Explore workspace overflowed and its breadcrumb
|
||
tucked under the nav (#1481). This fallback is only used pre-measure. */
|
||
--fc-nav-h: 64px;
|
||
}
|
||
/* Applied to a sticky sub-header so it continues the nav's fade instead of
|
||
restarting it. Percentage stops so the fade always spans the element's height
|
||
(survives the filter bar's expanding refine panel). The blur keeps tabs and
|
||
controls legible as the fill thins toward transparent — the solid-surface
|
||
bars it replaces had none, so it must live here. */
|
||
.fc-chrome-continues {
|
||
/* Continues the nav's fade: HOLDS near the seam alpha through the first ~55%
|
||
(subtle), then eases down to transparent over the last ~45% with an
|
||
intermediate stop so the tail is soft — no hard line at the bottom edge
|
||
(operator 2026-07-13). Percentage stops keep the shape spanning the
|
||
element's height (survives the filter bar's expanding refine panel). */
|
||
background: linear-gradient(
|
||
to bottom,
|
||
rgba(var(--fc-chrome-rgb), var(--fc-chrome-seam)) 0%,
|
||
rgba(var(--fc-chrome-rgb), 0.60) 55%,
|
||
rgba(var(--fc-chrome-rgb), 0.28) 82%,
|
||
rgba(var(--fc-chrome-rgb), 0) 100%
|
||
);
|
||
backdrop-filter: blur(2px);
|
||
-webkit-backdrop-filter: blur(2px);
|
||
}
|