From 355593c4f758bb732521f217e709563308d2cc8c Mon Sep 17 00:00:00 2001 From: Giorgio Gilestro Date: Thu, 28 May 2026 12:31:29 +0200 Subject: [PATCH] css: split cassandra.css into per-section files Splits the 2571-line cassandra.css into ten focused stylesheets: tokens (palette + fonts), layout (chrome), panels, dashboard, portfolio, log-chat, auth, settings, news, public. base.html and public_base.html load only what they need; auth pages (login, verify, unsubscribe confirm) load tokens + layout + auth. Brand drift-detection test repointed at tokens.css (where the palette now lives). 291 tests still pass. --- app/branding.py | 8 +- app/routers/email.py | 4 +- app/services/glossary.py | 4 +- app/static/css/auth.css | 132 ++ app/static/css/cassandra.css | 2571 ---------------------------- app/static/css/dashboard.css | 228 +++ app/static/css/layout.css | 185 ++ app/static/css/log-chat.css | 282 +++ app/static/css/news.css | 86 + app/static/css/panels.css | 92 + app/static/css/portfolio.css | 376 ++++ app/static/css/public.css | 717 ++++++++ app/static/css/settings.css | 381 +++++ app/static/css/tokens.css | 44 + app/templates/base.html | 10 +- app/templates/login.html | 4 +- app/templates/public_base.html | 7 +- app/templates/verify.html | 4 +- tests/test_branding_consistency.py | 6 +- 19 files changed, 2556 insertions(+), 2585 deletions(-) create mode 100644 app/static/css/auth.css delete mode 100644 app/static/css/cassandra.css create mode 100644 app/static/css/dashboard.css create mode 100644 app/static/css/layout.css create mode 100644 app/static/css/log-chat.css create mode 100644 app/static/css/news.css create mode 100644 app/static/css/panels.css create mode 100644 app/static/css/portfolio.css create mode 100644 app/static/css/public.css create mode 100644 app/static/css/settings.css create mode 100644 app/static/css/tokens.css diff --git a/app/branding.py b/app/branding.py index 1bd8f48..dd7370c 100644 --- a/app/branding.py +++ b/app/branding.py @@ -7,13 +7,13 @@ into user-visible chrome (page titles, email headers, OpenRouter referer) must read `BRAND_NAME` from here; do not hard-code the string. Internal identifiers (`cassandra_session` cookie, pyproject package name, -SQLAlchemy GET_LOCK keys, file `cassandra.css`, env var `CASSANDRA_TOKEN`) -keep the legacy name on purpose — renaming them would invalidate live -sessions / advisory locks / configs for zero brand benefit. +SQLAlchemy GET_LOCK keys, env var `CASSANDRA_TOKEN`) keep the legacy +name on purpose — renaming them would invalidate live sessions / +advisory locks / configs for zero brand benefit. The colour palette below is hand-authored in CSS as well; a drift- detection test (`tests/test_branding_consistency.py`) parses -`cassandra.css` and asserts every variable matches. Update both or +`tokens.css` and asserts every variable matches. Update both or neither. The light theme is the *default* everywhere — dashboard `:root` block, diff --git a/app/routers/email.py b/app/routers/email.py index 429101b..b7df411 100644 --- a/app/routers/email.py +++ b/app/routers/email.py @@ -63,7 +63,9 @@ _CONFIRM_PAGE = """\ Unsubscribed — {brand} - + + +
diff --git a/app/services/glossary.py b/app/services/glossary.py index c994995..40aa938 100644 --- a/app/services/glossary.py +++ b/app/services/glossary.py @@ -10,8 +10,8 @@ The wrap markup is: VIX `title` gives a native fallback on touch devices that don't fire :hover. -The CSS tooltip (see `.glossary:hover::after` in cassandra.css) uses -`data-def` for richer formatting. Wrapping happens at most once per term +The CSS tooltip (see `.glossary` / `#glossary-tooltip` in dashboard.css) +uses `data-def` for richer formatting. Wrapping happens at most once per term per HTML fragment — repeated occurrences stay plain. """ from __future__ import annotations diff --git a/app/static/css/auth.css b/app/static/css/auth.css new file mode 100644 index 0000000..70da6cd --- /dev/null +++ b/app/static/css/auth.css @@ -0,0 +1,132 @@ +/* Cassandra — auth pages: login, sign-up, OTP verify (standalone, no app chrome). */ + +/* --- Auth pages (login / signup, standalone — no app chrome) -------- */ + +.auth-shell { + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + background: var(--bg); + padding: 20px; +} +.auth-card { + width: 360px; + max-width: 100%; + background: var(--surface); + border: 1px solid var(--border); + padding: 28px 26px; +} +.auth-card__brand { + font-family: var(--font-mono); + color: var(--accent); + font-size: 18px; + letter-spacing: 0.12em; + text-transform: uppercase; + font-weight: 700; +} +.auth-card__brand::before { content: "▰ "; opacity: 0.6; } +.auth-card__hint { + font-family: var(--font-mono); + color: var(--muted); + font-size: 10px; + text-transform: uppercase; + letter-spacing: 0.08em; + margin: 2px 0 18px; +} +.auth-card form { display: flex; flex-direction: column; gap: 12px; } +.auth-card label { + display: flex; + flex-direction: column; + font-family: var(--font-mono); + color: var(--muted); + font-size: 10px; + text-transform: uppercase; + letter-spacing: 0.06em; + gap: 4px; +} +.auth-card input[type="email"], +.auth-card input[type="password"], +.auth-card input[type="text"] { + background: var(--bg); + border: 1px solid var(--border); + color: var(--text); + font-family: var(--font-mono); + font-size: 16px; + padding: 12px 14px; + outline: none; + border-radius: 3px; +} +/* The 6-digit OTP input wants to be visually loud — it's the only + thing the user is doing on that page. Bigger, more spacing, taller. */ +.auth-card input[name="code"] { + font-size: 24px; + padding: 16px 14px; + letter-spacing: 0.5em; + text-align: center; +} +.auth-card input:focus { border-color: var(--accent); } +.auth-card button { + margin-top: 8px; + background: transparent; + border: 1px solid var(--accent); + color: var(--accent); + font-family: var(--font-mono); + font-size: 11px; + padding: 9px 12px; + text-transform: uppercase; + letter-spacing: 0.1em; + cursor: pointer; +} +.auth-card button:hover { background: var(--accent); color: var(--bg); } +.auth-card__alt { + margin-top: 18px; + font-size: 12px; + color: var(--muted); + text-align: center; +} +.auth-error { + border-left: 3px solid var(--negative); + background: color-mix(in srgb, var(--negative) 6%, transparent); + color: var(--negative); + padding: 8px 10px; + font-size: 12px; + margin-bottom: 14px; + font-family: var(--font-mono); +} +.auth-info { + border-left: 3px solid var(--accent); + background: color-mix(in srgb, var(--accent) 6%, transparent); + color: var(--accent); + padding: 8px 10px; + font-size: 12px; + margin-bottom: 14px; + font-family: var(--font-mono); +} +.auth-info--invited { + /* Slightly warmer / friendlier shading for the referral banner. */ + border-left-color: var(--positive); + background: color-mix(in srgb, var(--positive) 7%, transparent); + color: var(--text); + font-family: var(--font-sans); + font-size: 13px; + line-height: 1.5; +} +.auth-info--invited strong { color: var(--positive); font-weight: 600; } +.auth-card__lede { + font-size: 12.5px; + color: var(--muted); + margin: 0 0 16px; + line-height: 1.5; +} +.auth-card__lede strong { color: var(--text); font-weight: normal; } +.auth-card__resend { + background: transparent !important; + color: var(--muted) !important; + border: 1px dashed var(--border) !important; + font-size: 11px !important; +} +.auth-card__resend:hover { + color: var(--accent) !important; + border-color: var(--accent) !important; +} diff --git a/app/static/css/cassandra.css b/app/static/css/cassandra.css deleted file mode 100644 index fdc8729..0000000 --- a/app/static/css/cassandra.css +++ /dev/null @@ -1,2571 +0,0 @@ -/* Cassandra — geopolitical-terminal aesthetic with two themes. - * Mono for data, headers, terminal feel; sans for prose surfaces (log + chat). */ - -:root { - /* Light theme (default) */ - --bg: #f5f3ec; /* warm off-white, easier on the eyes than pure white */ - --surface: #ffffff; - --surface-2: #efece3; - --border: #d6d3cb; - --text: #1c1f25; - --muted: #545b69; - --dim: #8a8f9a; - --accent: #0e7490; /* deep teal — still terminal-feel on light */ - --positive: #166534; - --negative: #b91c1c; - --alert: #c2410c; - --warning: #a16207; - --user-bubble-bg: rgba(14, 116, 144, 0.07); -} - -[data-theme="dark"] { - --bg: #0a0e14; - --surface: #11151c; - --surface-2: #161b25; - --border: #2a3142; - --text: #d4dae8; /* lifted from #c0caf5 for readability */ - --muted: #8189a1; /* lifted from #565f89 — was unreadably dim */ - --dim: #565f89; - --accent: #00d9ff; - --positive: #50fa7b; - --negative: #ff5b5b; - --alert: #ff8a4a; - --warning: #f1fa8c; - --user-bubble-bg: rgba(0, 217, 255, 0.08); -} - -/* Font stacks. Mono for terminal feel; sans for reading. */ -:root { - --font-mono: 'JetBrains Mono', 'IBM Plex Mono', 'Fira Code', ui-monospace, monospace; - --font-sans: -apple-system, BlinkMacSystemFont, 'Inter', 'Segoe UI', Roboto, - 'Helvetica Neue', system-ui, sans-serif; -} - -* { box-sizing: border-box; } - -html, body { - margin: 0; - padding: 0; - background: var(--bg); - color: var(--text); - font-family: var(--font-mono); - font-size: 13px; - line-height: 1.5; - font-variant-numeric: tabular-nums; -} - -a { color: var(--accent); text-decoration: none; } -a:hover { text-decoration: underline; } - -/* --- Layout ---------------------------------------------------------- */ - -.app { - display: grid; - grid-template-columns: 1fr; - grid-template-rows: auto 1fr auto; - min-height: 100vh; -} - -.app-header { - display: flex; - align-items: center; - justify-content: space-between; - border-bottom: 1px solid var(--border); - padding: 10px 18px; - background: var(--surface); - letter-spacing: 0.08em; - text-transform: uppercase; - position: sticky; - top: 0; - z-index: 50; -} -.app-header .brand { - color: var(--accent); - font-weight: 700; - text-decoration: none; -} -.app-header .brand:hover { color: var(--text); } -.app-header .brand::before { content: "▰ "; opacity: 0.6; } -.app-header nav a { - margin-left: 18px; - color: var(--muted); -} -.app-header nav a.active { color: var(--text); } -.app-header .meta { color: var(--muted); font-size: 11px; } - -.app-header .header-right { display: flex; align-items: center; gap: 14px; } -.theme-toggle { - background: transparent; - border: 1px solid var(--border); - color: var(--muted); - padding: 3px 8px; - font-family: var(--font-mono); - font-size: 10px; - letter-spacing: 0.08em; - cursor: pointer; - text-transform: lowercase; -} -.theme-toggle:hover { color: var(--accent); border-color: var(--accent); } -.theme-toggle__label::before { content: "◐ light"; } -[data-theme="dark"] .theme-toggle__label::before { content: "◐ dark"; } - -/* Tone toggle (segmented control: Novice | Intermediate) */ -.tone-toggle { - display: inline-flex; - border: 1px solid var(--border); - font-family: var(--font-mono); - font-size: 10.5px; - letter-spacing: 0.06em; - text-transform: uppercase; -} -.tone-toggle button { - background: transparent; - color: var(--muted); - border: 0; - padding: 4px 10px; - cursor: pointer; - font: inherit; - letter-spacing: inherit; - text-transform: inherit; -} -.tone-toggle button + button { border-left: 1px solid var(--border); } -.tone-toggle button:hover { color: var(--accent); } -.tone-toggle[data-tone="NOVICE"] button[data-value="NOVICE"], -.tone-toggle[data-tone="INTERMEDIATE"] button[data-value="INTERMEDIATE"] { - background: var(--accent); - color: var(--bg); -} - -/* Language toggle in the topbar — same visual rhythm as the tone - * toggle so the two controls read as a pair. Only EN and IT are - * visible here; the WIP languages (ES/FR/DE) live in /settings. */ -.lang-toggle { - display: inline-flex; - border: 1px solid var(--border); - font-family: var(--font-mono); - font-size: 10.5px; - letter-spacing: 0.06em; - text-transform: uppercase; -} -.lang-toggle button { - background: transparent; - color: var(--muted); - border: 0; - padding: 4px 8px; - cursor: pointer; - font: inherit; - letter-spacing: inherit; - text-transform: inherit; -} -.lang-toggle button + button { border-left: 1px solid var(--border); } -.lang-toggle button:hover { color: var(--accent); } -.lang-toggle[data-lang="en"] button[data-value="en"], -.lang-toggle[data-lang="it"] button[data-value="it"] { - background: var(--accent); - color: var(--bg); -} - -.app-main { - padding: 14px; - display: grid; - grid-template-columns: minmax(0, 2fr) minmax(0, 1fr); - grid-template-rows: auto auto auto auto; - grid-template-areas: - "header header" - "indicators log" - "portfolio log" - "news news"; - gap: 14px; -} -@media (max-width: 1100px) { - .app-main { - grid-template-columns: 1fr; - grid-template-areas: "header" "indicators" "portfolio" "log" "news"; - } -} - -#dash-header-container { grid-area: header; } -#indicators-panel { grid-area: indicators; } -#portfolio-panel { grid-area: portfolio; } -#log-panel { - grid-area: log; - /* Don't stretch to fill both grid rows; if the log is shorter than - the portfolio next to it, the surplus below would render as a big - empty white box. Aligning to the start makes the panel shrink to - its content and the dashboard background fills any gap. */ - align-self: start; -} -#news-panel { grid-area: news; } - - -/* Sticky bottom markets bar — uses the same .mkt chip styling as the - old dashboard header, extended with each market's headline index. */ -.markets-bar { - position: sticky; - bottom: 0; - z-index: 50; - background: var(--surface); - border-top: 1px solid var(--border); -} -.markets-bar__inner { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); - gap: 1px; - background: var(--border); - border: 0; -} -.markets-bar .mkt { - border: 0; - border-radius: 0; -} - -/* --- Panels ----------------------------------------------------------- */ - -.panel { - background: var(--surface); - border: 1px solid var(--border); - position: relative; -} -.panel-header { - border-bottom: 1px solid var(--border); - padding: 8px 12px; - display: flex; - align-items: center; - justify-content: space-between; - text-transform: uppercase; - letter-spacing: 0.1em; - color: var(--muted); - font-size: 11px; - background: linear-gradient(180deg, var(--surface-2), var(--surface)); -} -.panel-header .title { color: var(--text); font-weight: 700; } -.panel-header .title::before { content: "■ "; color: var(--accent); } -.panel-header .meta { color: var(--dim); } -.panel-body { padding: 6px 0; } -.panel-body--scroll { max-height: 70vh; overflow-y: auto; } - -/* --- Tables ----------------------------------------------------------- */ - -table.dense { - width: 100%; - border-collapse: collapse; -} -table.dense th, table.dense td { - padding: 4px 12px; - font-size: 12px; - border-bottom: 1px solid var(--surface-2); - white-space: nowrap; -} -table.dense th { - text-align: left; - color: var(--muted); - font-weight: 400; - text-transform: uppercase; - letter-spacing: 0.06em; - font-size: 10px; - background: var(--surface-2); -} -table.dense th.num, -table.dense td.num { text-align: right; } -table.dense td.label { color: var(--text); } -table.dense td.label.has-tip, -table.dense td[title] { - cursor: help; - border-bottom: 1px dotted color-mix(in srgb, var(--accent) 40%, transparent); - border-bottom-width: 1px; -} -.pf-name.has-tip { - cursor: help; - border-bottom: 1px dotted color-mix(in srgb, var(--accent) 50%, transparent); -} -table.dense tr:hover td { background: color-mix(in srgb, var(--accent) 5%, transparent); } - -.pos { color: var(--positive); } -.neg { color: var(--negative); } -.neu { color: var(--muted); } -.note { color: var(--dim); font-size: 11px; } - -/* Stale indicator rows — last observation > 90 days old */ -table.dense tr.row-stale td { color: var(--dim); } -.stale-tag { - display: inline-block; - font-size: 8.5px; - letter-spacing: 0.08em; - color: var(--alert); - border: 1px solid var(--alert); - padding: 0 4px; - margin-left: 4px; - vertical-align: middle; - text-transform: uppercase; - cursor: help; -} - -/* --- Status LEDs ------------------------------------------------------ */ - -.led { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 4px; vertical-align: middle; } -.led.ok { background: var(--positive); box-shadow: 0 0 6px var(--positive); } -.led.warn { background: var(--warning); box-shadow: 0 0 6px var(--warning); } -.led.err { background: var(--negative); box-shadow: 0 0 6px var(--negative); } -.led.idle { background: var(--dim); } - -/* --- Dashboard top header (markets + aggregate read) ----------------- */ - -.dash-header { - display: grid; - grid-template-columns: 1fr; - gap: 12px; - margin-bottom: 0; -} -.dash-header__markets { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); - gap: 1px; - background: var(--border); - border: 1px solid var(--border); -} -.mkt { - background: var(--surface); - padding: 6px 10px; - font-family: var(--font-mono); - font-size: 11px; - display: grid; - grid-template-columns: auto 1fr auto; - grid-template-rows: auto auto; - align-items: center; - gap: 2px 6px; -} -.mkt__dot { - width: 8px; height: 8px; border-radius: 50%; - grid-row: 1 / span 2; grid-column: 1; - align-self: center; -} -.mkt--open .mkt__dot { background: var(--positive); box-shadow: 0 0 6px var(--positive); } -.mkt--closed .mkt__dot { background: var(--dim); } -.mkt__name { - grid-row: 1; grid-column: 2; - color: var(--text); font-weight: 700; - text-transform: uppercase; letter-spacing: 0.08em; -} -.mkt__state { - grid-row: 1; grid-column: 3; - font-size: 9.5px; letter-spacing: 0.08em; - text-transform: lowercase; -} -.mkt--open .mkt__state { color: var(--positive); } -.mkt--closed .mkt__state { color: var(--dim); } -.mkt__index { - grid-row: 2; grid-column: 2; - font-size: 10.5px; - font-variant-numeric: tabular-nums; - display: inline-flex; - align-items: baseline; - gap: 5px; - white-space: nowrap; -} -.mkt__index-label { color: var(--dim); } -.mkt__index-price { color: var(--text); } -.mkt__index-change.pos { color: var(--positive); } -.mkt__index-change.neg { color: var(--negative); } -.mkt__index-change.neu { color: var(--muted); } -.mkt__index--empty { color: var(--dim); font-size: 10px; } -.mkt__when { - grid-row: 2; grid-column: 3; - color: var(--muted); font-size: 10px; - font-variant-numeric: tabular-nums; - text-align: right; -} -.mkt__when-label { color: var(--dim); } - -.dash-header__read { - border: 1px solid var(--border); - border-left: 3px solid var(--accent); - background: color-mix(in srgb, var(--accent) 4%, transparent); - padding: 10px 14px; -} -.dash-header__read-meta { - display: flex; - justify-content: space-between; - align-items: baseline; - margin-bottom: 4px; -} -.dash-header__read-body { - margin: 0; - font-family: var(--font-sans); - font-size: 14px; - line-height: 1.55; - color: var(--text); -} -.dash-header__read--pending { color: var(--dim); font-style: italic; } -.dash-header__read--pending .dash-header__read-body { color: var(--dim); font-size: 12px; } - -/* --- Indicator group summary (above the table) ----------------------- */ - -.ind-summary { - font-family: var(--font-sans); - padding: 10px 16px; - border-bottom: 1px solid var(--surface-2); - border-left: 3px solid var(--accent); - background: color-mix(in srgb, var(--accent) 4%, transparent); -} -.ind-summary__head { - display: flex; - align-items: baseline; - justify-content: space-between; - margin-bottom: 4px; -} -.ind-summary__label { - font-family: var(--font-mono); - font-size: 10px; - color: var(--accent); - text-transform: uppercase; - letter-spacing: 0.1em; - font-weight: 700; -} -.ind-summary__label::before { content: "▸ "; } -.ind-summary__when { - font-family: var(--font-mono); - font-size: 10px; - color: var(--dim); - font-variant-numeric: tabular-nums; -} -.ind-summary__body { - margin: 0; - font-size: 13.5px; - line-height: 1.55; - color: var(--text); -} -.ind-summary--pending { color: var(--dim); font-style: italic; } -.ind-summary--pending .ind-summary__body { color: var(--dim); font-size: 12px; } - -/* --- Glossary tooltips (Novice mode) --------------------------------- */ -/* The term gets a dotted underline. The actual tooltip is a single shared - element (#glossary-tooltip) positioned by JS so it can flip on viewport - edges and never clip behind sticky bars (which sit at z-index 50). */ - -.glossary { - border-bottom: 1px dotted var(--accent); - cursor: help; - /* Same colour as surrounding text — only the underline signals "tooltip - available", keeping the paragraph visually quiet. */ -} -.glossary:focus { outline: 1px dotted var(--accent); outline-offset: 2px; } - -#glossary-tooltip { - position: fixed; - z-index: 200; /* Above sticky bars (z-index 50). */ - max-width: 300px; - padding: 9px 12px; - background: var(--surface); - color: var(--text); - border: 1px solid var(--accent); - font-family: var(--font-sans); - font-size: 12.5px; - line-height: 1.5; - letter-spacing: 0; - text-transform: none; - font-weight: normal; - box-shadow: 0 6px 18px rgba(0,0,0,0.35); - pointer-events: none; - opacity: 0; - transition: opacity 90ms ease; -} -#glossary-tooltip[data-visible="1"] { opacity: 1; } -#glossary-tooltip[hidden] { display: none; } - -/* --- Group tabs ------------------------------------------------------- */ - -.group-tabs { - display: flex; - border-bottom: 1px solid var(--border); - overflow-x: auto; -} -.group-tabs button { - background: transparent; - border: 0; - border-right: 1px solid var(--border); - color: var(--muted); - font-family: inherit; - font-size: 11px; - padding: 6px 12px; - text-transform: uppercase; - letter-spacing: 0.06em; - cursor: pointer; -} -.group-tabs button:hover { color: var(--text); } -.group-tabs button.active { - color: var(--accent); - background: var(--bg); - box-shadow: inset 0 -2px 0 var(--accent); -} - -/* --- Portfolio overall ----------------------------------------------- */ - -.pf-overall { - border-bottom: 1px solid var(--border); - padding: 10px 14px 12px; - background: linear-gradient(180deg, var(--surface-2), var(--surface)); -} -.pf-overall__head { - display: flex; - justify-content: space-between; - align-items: baseline; - margin-bottom: 8px; -} -.pf-name { - color: var(--accent); - text-transform: uppercase; - letter-spacing: 0.1em; - font-weight: 700; - font-size: 11px; -} -.pf-name::before { content: "◆ "; opacity: 0.6; } -.pf-as-of { color: var(--dim); font-size: 11px; } -.pf-overall__grid { - display: grid; - grid-template-columns: repeat(3, 1fr); - gap: 6px 24px; -} -@media (max-width: 640px) { - .pf-overall__grid { grid-template-columns: repeat(2, 1fr); } -} -.pf-stat-label { - font-size: 10px; - color: var(--muted); - text-transform: uppercase; - letter-spacing: 0.08em; -} -.pf-stat-value { - font-size: 16px; - color: var(--text); - font-variant-numeric: tabular-nums; - margin-top: 2px; -} -.pf-stat-value.pos { color: var(--positive); } -.pf-stat-value.neg { color: var(--negative); } -.pf-stat-value.neu { color: var(--muted); } -.pf-ccy { color: var(--dim); font-size: 11px; margin-left: 2px; } -.pf-pct { color: var(--dim); font-size: 11px; margin-left: 4px; } -.pf-pills { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 4px; } -.pf-pill { - font-size: 10.5px; - font-family: var(--font-mono); - color: var(--muted); - background: var(--surface-2); - border: 1px solid var(--border); - padding: 2px 6px; - letter-spacing: 0.04em; -} -.pf-warn { - border-left: 3px solid var(--alert); - background: color-mix(in srgb, var(--alert) 6%, transparent); - color: var(--alert); - padding: 8px 10px; - font-size: 12px; - margin: 10px 0; -} -.pf-actions { - display: flex; - gap: 8px; - margin-top: 12px; -} -.pf-actions button { - font-family: var(--font-mono); - font-size: 11px; - letter-spacing: 0.06em; - text-transform: uppercase; - background: var(--surface-2); - color: var(--accent); - border: 1px solid var(--border); - padding: 7px 14px; - cursor: pointer; -} -.pf-actions button:hover { border-color: var(--accent); } -.pf-actions button:disabled { opacity: 0.5; cursor: not-allowed; } -.pf-secondary { color: var(--muted); } -.pf-secondary:hover { color: var(--negative); border-color: var(--negative); } - -/* Settings-page action button — same visual language as .pf-actions - button so buttons across /settings (Manage subscription, future - actions) read as one family. Standalone class (not nested under a - parent) so it can be dropped onto any button anywhere on the page. */ -.settings-btn { - font-family: var(--font-mono); - font-size: 11px; - letter-spacing: 0.06em; - text-transform: uppercase; - background: var(--surface-2); - color: var(--accent); - border: 1px solid var(--border); - padding: 7px 14px; - cursor: pointer; - border-radius: 2px; - text-decoration: none; - display: inline-block; -} -.settings-btn:hover { border-color: var(--accent); } -.settings-btn:disabled { opacity: 0.5; cursor: not-allowed; } - -/* Icon-button variant for inline row actions (e.g. Manage subscription - gear in the Tier row). Square hit area, accent on hover, tooltip via - title attribute. */ -.settings-icon-btn { - background: transparent; - border: 1px solid transparent; - color: var(--muted); - width: 32px; - height: 32px; - padding: 0; - display: inline-flex; - align-items: center; - justify-content: center; - cursor: pointer; - border-radius: 3px; - flex-shrink: 0; - transition: color 80ms linear, border-color 80ms linear, background 80ms linear; -} -.settings-icon-btn:hover { - color: var(--accent); - border-color: var(--border); - background: var(--surface-2); -} -.settings-icon-btn:disabled { opacity: 0.5; cursor: not-allowed; } -.settings-icon-btn svg { display: block; } -.pf-analysis { - margin-top: 14px; - background: var(--surface-2); - border: 1px solid var(--border); -} -.pf-analysis__details { padding: 0; } -.pf-analysis__head { - display: flex; - justify-content: space-between; - align-items: center; - font-size: 11px; - color: var(--muted); - letter-spacing: 0.06em; - text-transform: uppercase; - padding: 10px 16px; - cursor: pointer; - user-select: none; - list-style: none; /* hide native marker in Firefox */ -} -.pf-analysis__head::-webkit-details-marker { display: none; } -.pf-analysis__head-left::before { - content: "▸ "; - display: inline-block; - width: 1em; - color: var(--accent); - transition: transform 120ms ease; -} -details[open] .pf-analysis__head-left::before { content: "▾ "; } -.pf-analysis__head:hover { color: var(--accent); } -.pf-analysis__head:hover .pf-analysis__head-left::before { color: var(--accent); } -.pf-analysis__details[open] .pf-analysis__head { - border-bottom: 1px solid var(--border); -} -.pf-analysis__body { - font-family: var(--font-sans); - font-size: 14px; - line-height: 1.65; - color: var(--text); - white-space: pre-wrap; - margin: 0; - padding: 14px 16px 16px; -} - -/* --- Log panel -------------------------------------------------------- */ - -.log-content { - font-family: var(--font-sans); - padding: 28px clamp(20px, 4vw, 56px) 32px; - font-size: 15.5px; - line-height: 1.72; - color: var(--text); - max-width: 76ch; - margin: 0 auto; - max-height: calc(100vh - 240px); - overflow-y: auto; -} -.log-content p { margin: 0 0 1.1em; } -.log-content h1, .log-content h2, .log-content h3, .log-content h4 { - font-family: var(--font-mono); - color: var(--accent); - text-transform: uppercase; - letter-spacing: 0.08em; - font-size: 12px; - margin-top: 1.8em; - margin-bottom: 0.5em; - font-weight: 700; -} -.log-content h1:first-child, -.log-content h2:first-child, -.log-content h3:first-child { margin-top: 0; } - -/* TL;DR callout — model is instructed to put it first, so style the first - * heading + paragraph block as a callout. */ -.log-content h3:first-of-type { - font-size: 11px; - color: var(--accent); - border-left: 3px solid var(--accent); - padding-left: 10px; - margin-bottom: 0; -} -.log-content h3:first-of-type + p { - font-size: 16.5px; - line-height: 1.6; - color: var(--text); - border-left: 3px solid var(--accent); - padding: 4px 14px 12px; - margin: 0 0 1.8em; - background: color-mix(in srgb, var(--accent) 5%, transparent); - font-weight: 500; -} -.log-content strong { color: var(--text); font-weight: 700; } -.log-content em { color: var(--muted); font-style: italic; } -.log-content ul, .log-content ol { padding-left: 1.4em; margin: 0 0 1.1em; } -.log-content li { margin-bottom: 0.4em; } -.log-content hr { - border: 0; - border-top: 1px solid var(--border); - margin: 1.6em 0; -} - -/* --- Log page (calendar + log + chat sidebar) ------------------------- */ - -.log-page__body { - display: grid; - grid-template-columns: 220px 1fr 320px; - gap: 1px; - background: var(--border); -} -@media (max-width: 1100px) { - .log-page__body { grid-template-columns: 1fr; } -} -.log-page__cal, .log-page__content, .log-page__chat { background: var(--surface); } -.log-page__cal { padding: 10px; } -.log-page__content { min-height: 60vh; } -.log-page__chat { padding: 8px; min-height: 60vh; display: flex; flex-direction: column; } -.log-page__chat--locked { opacity: 0.92; } -.chat-locked { - flex: 1; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - text-align: center; - gap: 16px; - padding: 24px 18px; - color: var(--muted); - font-size: 13px; - line-height: 1.55; - border: 1px dashed var(--border); - border-radius: 4px; - margin: 8px 4px; -} -.chat-locked p { margin: 0; max-width: 280px; } -.chat-locked strong { color: var(--text); display: block; margin-bottom: 6px; } - -/* --- Calendar widget --------------------------------------------------- */ - -.cal__nav { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 8px; - font-size: 11px; - letter-spacing: 0.08em; - text-transform: uppercase; -} -.cal__title { color: var(--accent); font-weight: 700; } -.cal__btn { - background: transparent; - color: var(--muted); - border: 1px solid var(--border); - padding: 2px 8px; - cursor: pointer; - font-family: inherit; - font-size: 13px; -} -.cal__btn:hover { color: var(--accent); border-color: var(--accent); } -.cal__grid { - display: grid; - grid-template-columns: repeat(7, 1fr); - gap: 1px; - background: var(--border); - border: 1px solid var(--border); -} -.cal__h { - text-align: center; - font-size: 9px; - color: var(--dim); - background: var(--surface-2); - padding: 3px 0; - text-transform: uppercase; -} -.cal__d { - background: var(--surface); - border: 0; - color: var(--muted); - font-family: inherit; - font-size: 11px; - padding: 6px 0; - text-align: center; - cursor: not-allowed; -} -.cal__d--empty { background: var(--bg); cursor: default; } -.cal__d--has-log { - color: var(--text); - cursor: pointer; - position: relative; -} -.cal__d--has-log::after { - content: ""; - position: absolute; - bottom: 3px; - left: 50%; - transform: translateX(-50%); - width: 3px; height: 3px; - border-radius: 50%; - background: var(--accent); -} -.cal__d--has-log:hover { background: color-mix(in srgb, var(--accent) 10%, transparent); } -.cal__d--today { color: var(--warning); } -.cal__d--selected { - background: var(--accent); - color: var(--bg); - font-weight: 700; -} -.cal__d--selected::after { background: var(--bg); } - -/* --- Badges (tone / analysis indicators) ------------------------------ */ - -.badge { - display: inline-block; - font-family: var(--font-mono); - font-size: 9.5px; - letter-spacing: 0.06em; - text-transform: uppercase; - padding: 1px 6px; - border: 1px solid currentColor; - margin-right: 4px; - background: transparent; - vertical-align: middle; -} -/* Tone axis — green→accent→amber as audience density rises */ -.badge--tone-novice { color: var(--positive); } -.badge--tone-intermediate { color: var(--accent); } -.badge--tone-pro { color: var(--alert); } - -/* Analysis axis — dry is muted, speculative is accent */ -.badge--analysis-dry { color: var(--muted); } -.badge--analysis-speculative { color: var(--accent); } - -.badge--ver { color: var(--dim); } -.badge--ok { color: var(--positive); border-color: var(--positive); } - -.meta__hint { color: var(--dim); font-size: 10px; margin-right: 4px; } - -/* --- Log metadata footer ---------------------------------------------- */ - -.log-meta { - padding: 4px clamp(20px, 4vw, 56px) 6px; - max-width: 76ch; - margin: 0 auto; - border-top: 1px dashed var(--border); - color: var(--dim); - font-size: 10.5px; - font-family: var(--font-mono); - letter-spacing: 0.04em; -} - -/* --- Auth pages (login / signup, standalone — no app chrome) -------- */ - -.auth-shell { - min-height: 100vh; - display: flex; - align-items: center; - justify-content: center; - background: var(--bg); - padding: 20px; -} -.auth-card { - width: 360px; - max-width: 100%; - background: var(--surface); - border: 1px solid var(--border); - padding: 28px 26px; -} -.auth-card__brand { - font-family: var(--font-mono); - color: var(--accent); - font-size: 18px; - letter-spacing: 0.12em; - text-transform: uppercase; - font-weight: 700; -} -.auth-card__brand::before { content: "▰ "; opacity: 0.6; } -.auth-card__hint { - font-family: var(--font-mono); - color: var(--muted); - font-size: 10px; - text-transform: uppercase; - letter-spacing: 0.08em; - margin: 2px 0 18px; -} -.auth-card form { display: flex; flex-direction: column; gap: 12px; } -.auth-card label { - display: flex; - flex-direction: column; - font-family: var(--font-mono); - color: var(--muted); - font-size: 10px; - text-transform: uppercase; - letter-spacing: 0.06em; - gap: 4px; -} -.auth-card input[type="email"], -.auth-card input[type="password"], -.auth-card input[type="text"] { - background: var(--bg); - border: 1px solid var(--border); - color: var(--text); - font-family: var(--font-mono); - font-size: 16px; - padding: 12px 14px; - outline: none; - border-radius: 3px; -} -/* The 6-digit OTP input wants to be visually loud — it's the only - thing the user is doing on that page. Bigger, more spacing, taller. */ -.auth-card input[name="code"] { - font-size: 24px; - padding: 16px 14px; - letter-spacing: 0.5em; - text-align: center; -} -.auth-card input:focus { border-color: var(--accent); } - -/* --- Modal text inputs (cloud-sync PIN modal, etc.) ---------------- */ -/* Same visual treatment as auth-card so prompts read as a coherent - family. Replaces the inline `style="padding:8px"` that left these - inputs feeling cramped. */ -.modal-input { - width: 100%; - background: var(--bg); - border: 1px solid var(--border); - color: var(--text); - font-family: var(--font-mono); - font-size: 16px; - padding: 12px 14px; - margin-bottom: 12px; - outline: none; - border-radius: 3px; - box-sizing: border-box; -} -.modal-input:focus { border-color: var(--accent); } -.auth-card button { - margin-top: 8px; - background: transparent; - border: 1px solid var(--accent); - color: var(--accent); - font-family: var(--font-mono); - font-size: 11px; - padding: 9px 12px; - text-transform: uppercase; - letter-spacing: 0.1em; - cursor: pointer; -} -.auth-card button:hover { background: var(--accent); color: var(--bg); } -.auth-card__alt { - margin-top: 18px; - font-size: 12px; - color: var(--muted); - text-align: center; -} -.auth-error { - border-left: 3px solid var(--negative); - background: color-mix(in srgb, var(--negative) 6%, transparent); - color: var(--negative); - padding: 8px 10px; - font-size: 12px; - margin-bottom: 14px; - font-family: var(--font-mono); -} -.auth-info { - border-left: 3px solid var(--accent); - background: color-mix(in srgb, var(--accent) 6%, transparent); - color: var(--accent); - padding: 8px 10px; - font-size: 12px; - margin-bottom: 14px; - font-family: var(--font-mono); -} -.auth-info--invited { - /* Slightly warmer / friendlier shading for the referral banner. */ - border-left-color: var(--positive); - background: color-mix(in srgb, var(--positive) 7%, transparent); - color: var(--text); - font-family: var(--font-sans); - font-size: 13px; - line-height: 1.5; -} -.auth-info--invited strong { color: var(--positive); font-weight: 600; } - -/* --- Settings page --------------------------------------------------- */ - -.settings-row { - display: flex; - align-items: baseline; - gap: 14px; - padding: 8px 0; - border-bottom: 1px solid var(--surface-2); - font-size: 13px; -} -.settings-row__label { - width: 110px; - flex-shrink: 0; - color: var(--muted); - text-transform: uppercase; - letter-spacing: 0.06em; - font-size: 10.5px; - font-family: var(--font-mono); -} -.settings-row__value { color: var(--text); } -.settings-row__hint { - color: var(--dim); - font-size: 11px; - margin-left: 8px; -} - -/* Terminal-aesthetic used in the Settings page. Native + * browser chrome stripped; we render a small chevron via crossed + * linear-gradients so the control matches the rest of the panel. */ +.settings-select { + appearance: none; + -webkit-appearance: none; + -moz-appearance: none; + background: transparent; + border: 1px solid var(--border); + color: var(--text); + padding: 4px 28px 4px 8px; + font-family: var(--font-mono); + font-size: 12px; + border-radius: 2px; + cursor: pointer; + background-image: + linear-gradient(45deg, transparent 50%, var(--dim) 50%), + linear-gradient(-45deg, transparent 50%, var(--dim) 50%); + background-position: calc(100% - 13px) 50%, calc(100% - 9px) 50%; + background-size: 5px 5px, 5px 5px; + background-repeat: no-repeat; + transition: border-color 120ms ease-out, color 120ms ease-out; +} +.settings-select:hover, +.settings-select:focus { + outline: none; + border-color: var(--accent); + color: var(--text); +} +.settings-select option { color: var(--text); background: var(--surface); } +.settings-select option:disabled { color: var(--dim); } + +.settings-status { + font-family: var(--font-mono); + font-size: 11px; + color: var(--muted); + letter-spacing: 0.04em; +} +.settings-status:empty { display: none; } + +/* Sections are
elements — collapsed by default to keep the + settings page scannable. Click the summary to expand. */ +.settings-section { + margin-top: 14px; + border-top: 1px solid var(--surface-2); + padding-top: 14px; +} +.settings-section__head { + font-family: var(--font-mono); + font-size: 11px; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--accent); + margin-bottom: 6px; + cursor: pointer; + list-style: none; + user-select: none; + display: flex; + align-items: center; + gap: 8px; + padding: 4px 0; +} +/* Suppress the native disclosure marker (Webkit + Firefox). */ +.settings-section__head::-webkit-details-marker { display: none; } +.settings-section__head::marker { content: ""; } +.settings-section__head::before { + content: "▸"; + color: var(--accent); + display: inline-block; + transition: transform 120ms ease-out; + font-size: 10px; +} +.settings-section[open] > .settings-section__head::before { + transform: rotate(90deg); +} +.settings-section[open] > .settings-section__head { margin-bottom: 10px; } +.settings-section__head:hover { color: var(--text); } +.settings-section__head:hover::before { color: var(--text); } +.settings-section__lede { + color: var(--muted); + font-size: 12.5px; + line-height: 1.55; + margin: 0 0 14px; +} +.settings-section__lede strong { color: var(--positive); font-weight: 600; } + +.invite-block { + background: var(--surface-2); + border: 1px solid var(--border); + padding: 14px 16px; +} +.invite-block__label { + display: block; + font-family: var(--font-mono); + font-size: 10px; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--muted); + margin-bottom: 4px; +} +.invite-block__label:not(:first-child) { margin-top: 12px; } +.invite-block__code { + font-family: var(--font-mono); + font-size: 22px; + letter-spacing: 0.32em; + color: var(--accent); + background: var(--surface); + padding: 10px 14px; + border: 1px solid var(--accent); + text-align: center; + user-select: all; +} +.invite-block__link { + display: flex; + gap: 6px; +} +.invite-block__link input { + flex: 1; + background: var(--surface); + color: var(--text); + border: 1px solid var(--border); + padding: 7px 10px; + font-family: var(--font-mono); + font-size: 12px; +} +.invite-block__link button { + background: var(--accent); + color: var(--bg); + border: 0; + padding: 0 14px; + font-family: var(--font-mono); + font-size: 11px; + letter-spacing: 0.06em; + text-transform: uppercase; + cursor: pointer; +} +.invite-block__link button:hover { opacity: 0.85; } + +.invite-stats { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 1px; + background: var(--border); + border: 1px solid var(--border); + margin-top: 16px; +} +.invite-stats > div { + background: var(--surface); + padding: 10px 14px; +} +.invite-stats__label { + font-family: var(--font-mono); + font-size: 10px; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--muted); +} +.invite-stats__value { + font-family: var(--font-mono); + font-size: 18px; + color: var(--text); + font-variant-numeric: tabular-nums; + margin-top: 4px; +} + +/* Import preview action row — two stacked buttons with an explainer. */ +.import-actions { + display: flex; + flex-wrap: wrap; + gap: 12px; + margin-top: 14px; +} +.import-choice { flex: 1 1 240px; min-width: 220px; } +.import-choice button { width: 100%; } +.import-choice .settings-row__hint { + display: block; + margin-top: 6px; + line-height: 1.5; +} + +/* User chip in header — now a button that toggles a dropdown menu. */ +.user-menu { position: relative; margin-left: 8px; } +.user-chip { + font-family: var(--font-mono); + font-size: 10.5px; + color: var(--muted); + letter-spacing: 0.04em; + background: none; + border: 0; + padding: 0; + cursor: pointer; +} +.user-chip:hover { color: var(--accent); } +.user-menu__caret { margin-left: 4px; opacity: 0.6; } +.user-menu__panel { + position: absolute; + top: calc(100% + 6px); + right: 0; + min-width: 160px; + background: var(--surface); + border: 1px solid var(--border); + border-radius: 6px; + box-shadow: 0 6px 18px rgba(0, 0, 0, 0.18); + z-index: 200; + padding: 4px 0; +} +.user-menu__item { + display: block; + padding: 8px 14px; + color: var(--text); + text-decoration: none; + font-size: 12px; +} +.user-menu__item:hover { background: var(--surface-2); color: var(--accent); } + +/* --- Upload / import drag-drop zone (settings page) ------------------ */ + +.dz { + border: 2px dashed var(--border); + background: var(--surface-2); + padding: 36px 20px; + text-align: center; + cursor: pointer; + transition: border-color 0.15s, background 0.15s; +} +.dz:hover, .dz--over { + border-color: var(--accent); + background: color-mix(in srgb, var(--accent) 6%, var(--surface-2)); +} +.dz__icon { + font-family: var(--font-mono); + font-size: 28px; + color: var(--accent); + letter-spacing: -2px; + margin-bottom: 6px; +} +.dz__label { + font-family: var(--font-mono); + font-size: 13px; + color: var(--text); + text-transform: uppercase; + letter-spacing: 0.08em; +} +.dz__hint { color: var(--muted); font-size: 11.5px; margin-top: 4px; } +.dz__hint a { color: var(--accent); } +.dz__filename { margin-top: 10px; color: var(--accent); font-size: 12px; font-family: var(--font-mono); min-height: 1em; } + + +.result { + margin-top: 20px; + padding: 14px; + border: 1px solid var(--border); + border-left: 3px solid var(--accent); + background: color-mix(in srgb, var(--accent) 4%, transparent); + font-family: var(--font-sans); + font-size: 13px; +} +.result--err { border-left-color: var(--negative); background: color-mix(in srgb, var(--negative) 5%, transparent); } +.result__head { + font-family: var(--font-mono); + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--accent); + margin-bottom: 10px; +} +.result--err .result__head { color: var(--negative); } +.result__grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 10px 18px; + margin-bottom: 10px; +} +.result__grid .k { + font-family: var(--font-mono); + font-size: 9.5px; + color: var(--muted); + text-transform: uppercase; + letter-spacing: 0.08em; +} +.result__grid .v { font-size: 17px; color: var(--text); font-variant-numeric: tabular-nums; margin-top: 2px; } +.result__grid .v.pos { color: var(--positive); } +.result__grid .v.neg { color: var(--negative); } +.result__row { color: var(--muted); font-size: 12px; margin-top: 6px; } +.result__warn { color: var(--alert); font-size: 12px; margin-top: 4px; } +.result__warn code { background: rgba(0,0,0,0.15); padding: 1px 4px; font-family: var(--font-mono); } + +/* --- Modal text inputs (cloud-sync PIN modal, etc.) ---------------- */ +/* Same visual treatment as auth-card so prompts read as a coherent + family. Replaces the inline `style="padding:8px"` that left these + inputs feeling cramped. */ +.modal-input { + width: 100%; + background: var(--bg); + border: 1px solid var(--border); + color: var(--text); + font-family: var(--font-mono); + font-size: 16px; + padding: 12px 14px; + margin-bottom: 12px; + outline: none; + border-radius: 3px; + box-sizing: border-box; +} +.modal-input:focus { border-color: var(--accent); } diff --git a/app/static/css/tokens.css b/app/static/css/tokens.css new file mode 100644 index 0000000..a3551b0 --- /dev/null +++ b/app/static/css/tokens.css @@ -0,0 +1,44 @@ +/* Cassandra — design tokens: palette, dark-theme overrides, font stacks. + * Must load first so all other files can var(--foo). */ + +:root { + /* Light theme (default) */ + --bg: #f5f3ec; /* warm off-white, easier on the eyes than pure white */ + --surface: #ffffff; + --surface-2: #efece3; + --border: #d6d3cb; + --text: #1c1f25; + --muted: #545b69; + --dim: #8a8f9a; + --accent: #0e7490; /* deep teal — still terminal-feel on light */ + --positive: #166534; + --negative: #b91c1c; + --alert: #c2410c; + --warning: #a16207; + --user-bubble-bg: rgba(14, 116, 144, 0.07); +} + +[data-theme="dark"] { + --bg: #0a0e14; + --surface: #11151c; + --surface-2: #161b25; + --border: #2a3142; + --text: #d4dae8; /* lifted from #c0caf5 for readability */ + --muted: #8189a1; /* lifted from #565f89 — was unreadably dim */ + --dim: #565f89; + --accent: #00d9ff; + --positive: #50fa7b; + --negative: #ff5b5b; + --alert: #ff8a4a; + --warning: #f1fa8c; + --user-bubble-bg: rgba(0, 217, 255, 0.08); +} + +/* Font stacks. Mono for terminal feel; sans for reading. */ +:root { + --font-mono: 'JetBrains Mono', 'IBM Plex Mono', 'Fira Code', ui-monospace, monospace; + --font-sans: -apple-system, BlinkMacSystemFont, 'Inter', 'Segoe UI', Roboto, + 'Helvetica Neue', system-ui, sans-serif; +} + +* { box-sizing: border-box; } diff --git a/app/templates/base.html b/app/templates/base.html index fbf52e0..fd15361 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -36,7 +36,15 @@ } catch (e) { document.documentElement.dataset.theme = 'light'; } })(); - + + + + + + + + + - + + +
diff --git a/app/templates/public_base.html b/app/templates/public_base.html index 77e4186..427fc36 100644 --- a/app/templates/public_base.html +++ b/app/templates/public_base.html @@ -14,7 +14,12 @@ } catch (e) { document.documentElement.dataset.theme = 'light'; } })(); - + + + + + +
diff --git a/app/templates/verify.html b/app/templates/verify.html index 1399fe5..43637d4 100644 --- a/app/templates/verify.html +++ b/app/templates/verify.html @@ -10,7 +10,9 @@ catch (e) { document.documentElement.dataset.theme = 'light'; } })(); - + + +
diff --git a/tests/test_branding_consistency.py b/tests/test_branding_consistency.py index a10c3a4..e51fd2e 100644 --- a/tests/test_branding_consistency.py +++ b/tests/test_branding_consistency.py @@ -1,6 +1,6 @@ """Drift-detection: brand palette in `app/branding.py` must match the CSS. -Both the website (cassandra.css) and the email templates use the same +Both the website (tokens.css) and the email templates use the same palette. The CSS hand-authors the values in :root and [data-theme="light"] blocks; this test parses those blocks and asserts every variable matches its counterpart in branding.py. If a colour changes, both must change. @@ -15,7 +15,7 @@ import pytest from app import branding -CSS_PATH = Path(__file__).resolve().parent.parent / "app" / "static" / "css" / "cassandra.css" +CSS_PATH = Path(__file__).resolve().parent.parent / "app" / "static" / "css" / "tokens.css" def _extract_vars(css: str, selector: str) -> dict[str, str]: @@ -23,7 +23,7 @@ def _extract_vars(css: str, selector: str) -> dict[str, str]: selector block. Strips whitespace; lowercases hex values.""" # Match the selector followed by its block. Non-greedy on the body to # stop at the first closing brace at the same depth (these blocks - # don't nest in cassandra.css). + # don't nest in tokens.css). pattern = re.escape(selector) + r"\s*\{([^}]*)\}" m = re.search(pattern, css) if not m: