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 = """\
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;
-}
-.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;
-}
-
-/* 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 page (drag-drop CSV) ------------------------------------- */
-
-.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__tag {
- display: inline-block;
- margin-left: 6px;
- font-size: 9px;
- padding: 1px 5px;
- border: 1px solid var(--accent);
- color: var(--accent);
-}
-.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); }
-
-/* --- Chat sidebar ----------------------------------------------------- */
-
-.chat-header {
- border-bottom: 1px solid var(--border);
- padding: 6px 4px 8px;
- margin-bottom: 6px;
- display: flex;
- flex-direction: column;
-}
-.chat-title {
- color: var(--accent);
- font-weight: 700;
- text-transform: uppercase;
- letter-spacing: 0.1em;
- font-size: 11px;
-}
-.chat-title::before { content: "▸ "; }
-.chat-hint { color: var(--dim); font-size: 10px; margin-top: 2px; }
-
-.chat-thread {
- flex: 1 1 auto;
- overflow-y: auto;
- padding: 4px 2px;
- display: flex;
- flex-direction: column;
- gap: 8px;
- min-height: 0;
-}
-.chat-msg {
- font-family: var(--font-sans);
- font-size: 13.5px;
- padding: 9px 11px;
- border: 1px solid var(--border);
- line-height: 1.6;
- word-wrap: break-word;
-}
-.chat-msg--system {
- color: var(--muted);
- font-size: 12px;
- background: transparent;
- border-style: dashed;
- font-family: var(--font-mono);
-}
-.chat-msg--user {
- background: var(--user-bubble-bg);
- border-color: var(--accent);
- color: var(--text);
- align-self: flex-end;
- max-width: 92%;
- white-space: pre-wrap;
-}
-.chat-msg--user::before {
- content: "you › ";
- font-family: var(--font-mono);
- color: var(--accent);
- opacity: 0.7;
- font-size: 10px;
-}
-.chat-msg--assistant { background: var(--surface-2); color: var(--text); }
-.chat-msg--assistant::before {
- content: "cassandra › ";
- font-family: var(--font-mono);
- color: var(--accent);
- opacity: 0.7;
- font-size: 10px;
-}
-.chat-msg--pending { color: var(--dim); font-style: italic; }
-.chat-msg--error { color: var(--negative); border-color: var(--negative); }
-
-.chat-msg p { margin: 0.4em 0; }
-.chat-msg p:first-child { margin-top: 0; }
-.chat-msg p:last-child { margin-bottom: 0; }
-.chat-msg h2, .chat-msg h3, .chat-msg h4 {
- font-family: var(--font-mono);
- color: var(--accent);
- font-size: 11px;
- margin: 0.8em 0 0.3em;
- text-transform: uppercase;
- letter-spacing: 0.06em;
-}
-.chat-msg strong { color: var(--text); font-weight: 700; }
-.chat-msg em { color: var(--muted); font-style: italic; }
-
-.chat-form {
- border-top: 1px solid var(--border);
- padding-top: 6px;
- display: flex;
- gap: 6px;
- align-items: flex-end;
-}
-.chat-form textarea {
- flex: 1;
- background: var(--bg);
- border: 1px solid var(--border);
- color: var(--text);
- font-family: inherit;
- font-size: 12px;
- padding: 6px 8px;
- resize: vertical;
- min-height: 36px;
- outline: none;
-}
-.chat-form textarea:focus { border-color: var(--accent); }
-.chat-form button {
- background: transparent;
- border: 1px solid var(--accent);
- color: var(--accent);
- font-family: inherit;
- font-size: 11px;
- padding: 6px 12px;
- text-transform: uppercase;
- letter-spacing: 0.08em;
- cursor: pointer;
-}
-.chat-form button:hover:not(:disabled) { background: var(--accent); color: var(--bg); }
-.chat-form button:disabled { opacity: 0.4; cursor: not-allowed; }
-
-/* --- News ------------------------------------------------------------- */
-
-.news-row {
- padding: 4px 12px;
- display: grid;
- /* age | source | title | tags-on-right | utc-time */
- grid-template-columns: 50px 130px minmax(0, 1fr) minmax(0, auto) 110px;
- gap: 12px;
- font-size: 12px;
- border-bottom: 1px solid var(--surface-2);
- align-items: center;
-}
-@media (max-width: 720px) {
- .news-row { grid-template-columns: 50px 100px 1fr; }
- .news-row .local,
- .news-row__tags { display: none; }
-}
-.news-row:hover { background: color-mix(in srgb, var(--accent) 5%, transparent); }
-.news-row .age { color: var(--dim); text-align: right; }
-.news-row .source { color: var(--muted); font-size: 11px; }
-.news-row .title { color: var(--text); }
-.news-row .title:hover { color: var(--accent); }
-.news-row .local {
- color: var(--muted);
- font-size: 11px;
- text-align: right;
- font-variant-numeric: tabular-nums;
- white-space: nowrap;
-}
-
-/* News tag chips on each row + the top-bar pill toggles */
-.news-row__tags {
- display: inline-flex;
- flex-wrap: nowrap;
- gap: 3px;
- justify-content: flex-end;
- overflow: hidden;
- max-width: 100%;
-}
-.tag-chip {
- font-family: var(--font-mono);
- font-size: 9px;
- letter-spacing: 0.04em;
- color: var(--muted);
- background: var(--surface-2);
- border: 1px solid var(--border);
- padding: 0 4px;
- white-space: nowrap;
- text-transform: uppercase;
- line-height: 1.5;
-}
-
-.news-tags {
- display: flex;
- flex-wrap: wrap;
- gap: 4px;
- padding: 8px 12px;
- border-bottom: 1px solid var(--border);
- background: var(--surface-2);
-}
-.news-tag {
- font-family: var(--font-mono);
- font-size: 10.5px;
- letter-spacing: 0.04em;
- text-transform: uppercase;
- color: var(--muted);
- background: transparent;
- border: 1px solid var(--border);
- padding: 3px 8px;
- cursor: pointer;
-}
-.news-tag:hover { color: var(--accent); border-color: var(--accent); }
-.news-tag[data-state="include"] {
- background: var(--accent);
- color: var(--bg);
- border-color: var(--accent);
-}
-.news-tag[data-state="exclude"] {
- color: var(--negative);
- border-color: var(--negative);
- text-decoration: line-through;
-}
-.news-tag--clear { color: var(--dim); border-style: dashed; }
-.news-tag--clear:hover { color: var(--negative); border-color: var(--negative); }
-
-/* --- Empty / loading state ------------------------------------------- */
-
-.empty {
- padding: 24px;
- text-align: center;
- color: var(--muted);
- font-size: 11px;
- letter-spacing: 0.08em;
- text-transform: uppercase;
-}
-.htmx-indicator {
- display: inline-block;
- color: var(--dim);
- opacity: 0;
- transition: opacity 0.2s;
-}
-.htmx-request .htmx-indicator { opacity: 1; }
-
-/* --- Scrollbar -------------------------------------------------------- */
-
-::-webkit-scrollbar { width: 8px; height: 8px; }
-::-webkit-scrollbar-track { background: var(--bg); }
-::-webkit-scrollbar-thumb { background: var(--dim); border-radius: 0; }
-::-webkit-scrollbar-thumb:hover { background: var(--muted); }
-
-/* ============================================================
- * Public pages — landing, pricing, about, terms, privacy, disclaimer.
- * Shared by all templates extending public_base.html. Visual language
- * matches the app shell (same palette, monospace brand, restrained
- * typography) but without dashboard chrome.
- * ============================================================ */
-
-.public-page { background: var(--bg); }
-
-.public-shell {
- display: flex;
- flex-direction: column;
- min-height: 100vh;
- max-width: 1080px;
- margin: 0 auto;
- padding: 0 24px;
-}
-
-.public-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 22px 0 16px;
- border-bottom: 1px solid var(--border);
-}
-.public-header__brand {
- color: var(--accent);
- font-weight: 700;
- text-decoration: none;
- font-family: var(--font-mono);
- font-size: 15px;
- letter-spacing: 0.01em;
-}
-.public-header__brand::before { content: "▰ "; opacity: 0.6; }
-.public-header__brand:hover { color: var(--text); }
-.public-header__nav { display: flex; align-items: center; gap: 22px; }
-.public-header__nav a {
- color: var(--muted);
- font-size: 13px;
- text-decoration: none;
-}
-.public-header__nav a:hover,
-.public-header__nav a.active { color: var(--text); }
-.public-header__cta {
- color: var(--accent) !important;
- border: 1px solid var(--accent);
- padding: 6px 14px;
- border-radius: 3px;
-}
-.public-header__cta:hover { background: var(--accent); color: var(--bg) !important; }
-
-.public-main {
- flex: 1;
- padding: 48px 0 64px;
-}
-
-.public-footer {
- border-top: 1px solid var(--border);
- padding: 28px 0 36px;
- margin-top: 24px;
- font-size: 12px;
- color: var(--muted);
-}
-.public-footer__inner {
- display: flex;
- flex-direction: column;
- gap: 14px;
-}
-.public-footer__brand strong { color: var(--text); margin-right: 10px; }
-.public-footer__tagline { color: var(--muted); }
-.public-footer__links { display: flex; flex-wrap: wrap; gap: 16px; }
-.public-footer__links a { color: var(--muted); text-decoration: none; }
-.public-footer__links a:hover { color: var(--accent); }
-.public-footer__meta { color: var(--dim); font-size: 11px; }
-
-/* --- Hero (landing) -------------------------------------------------- */
-
-.hero {
- padding: 32px 0 48px;
- border-bottom: 1px solid var(--border);
- margin-bottom: 48px;
-}
-.hero__brand {
- color: var(--muted);
- font-family: var(--font-mono);
- font-size: 12px;
- letter-spacing: 0.08em;
- text-transform: uppercase;
-}
-.hero__headline {
- font-size: clamp(28px, 5vw, 44px);
- font-weight: 700;
- line-height: 1.15;
- color: var(--text);
- margin: 12px 0 14px;
- letter-spacing: -0.01em;
-}
-.hero__subhead {
- font-size: 16px;
- color: var(--muted);
- max-width: 640px;
- line-height: 1.55;
- margin: 0 0 24px;
-}
-.hero__ctas { display: flex; flex-wrap: wrap; gap: 12px; }
-/* Shared button shape — was previously scoped to .hero__ctas, which made
- the pricing-card CTAs render as bare anchors. */
-.btn-primary,
-.btn-secondary {
- display: inline-block;
- padding: 10px 22px;
- border-radius: 3px;
- font-size: 13.5px;
- font-weight: 500;
- line-height: 1.4;
- text-decoration: none;
- text-align: center;
- cursor: pointer;
-}
-/* Block variant: full-width within parent, slightly taller — used inside
- tier cards so each CTA spans the card and reads as the obvious action. */
-.btn-block { display: block; width: 100%; padding: 12px 22px; font-size: 14px; }
-
-/* Qualify with `a` so we beat `a { color: var(--accent) }` and any
- :link/:visited UA defaults. Without `a.btn-primary` the cascade can
- resolve in favour of the visited-link color on some browsers and the
- label disappears against the accent background. */
-a.btn-primary,
-a.btn-primary:link,
-a.btn-primary:visited {
- background: var(--accent);
- color: var(--bg);
- border: 1px solid var(--accent);
-}
-a.btn-primary:hover { background: transparent; color: var(--accent); }
-a.btn-secondary,
-a.btn-secondary:link,
-a.btn-secondary:visited {
- background: transparent;
- color: var(--text);
- border: 1px solid var(--border);
-}
-a.btn-secondary:hover { color: var(--accent); border-color: var(--accent); }
-
-/* --- Feature blocks (landing) --------------------------------------- */
-
-.feature-grid {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
- gap: 24px;
- margin: 8px 0 56px;
-}
-.feature-card {
- border: 1px solid var(--border);
- border-radius: 4px;
- padding: 22px 22px 24px;
- background: var(--surface);
- /* Flex column so the screenshot thumbnail can dock to the bottom via
- margin-top:auto — that's what lines the three thumbnails up across
- cards regardless of body-text length. */
- display: flex;
- flex-direction: column;
-}
-.feature-card__tag {
- font-family: var(--font-mono);
- font-size: 10.5px;
- color: var(--accent);
- letter-spacing: 0.08em;
- text-transform: uppercase;
- margin-bottom: 10px;
-}
-.feature-card__title {
- font-size: 17px;
- font-weight: 600;
- color: var(--text);
- margin: 0 0 10px;
-}
-.feature-card__body {
- font-size: 13.5px;
- line-height: 1.6;
- color: var(--muted);
- margin: 0;
- /* Grow to fill the flex column so the thumbnail below docks to the
- bottom of the card. With grid-stretched equal-height cards, this is
- what aligns the thumbnails across the three cards. */
- flex-grow: 1;
-}
-
-/* --- Section primitives reused across pricing/about/legal ---------- */
-
-.public-section {
- margin: 0 0 56px;
-}
-.public-section__head {
- font-size: 20px;
- font-weight: 600;
- color: var(--text);
- margin: 0 0 16px;
- padding-bottom: 8px;
- border-bottom: 1px solid var(--border);
-}
-.public-section h3 {
- font-size: 15px;
- font-weight: 600;
- color: var(--text);
- margin: 24px 0 8px;
-}
-.public-section p,
-.public-section li {
- font-size: 14px;
- line-height: 1.65;
- color: var(--text);
-}
-.public-section p { margin: 0 0 14px; }
-.public-section ul {
- margin: 0 0 16px;
- padding-left: 22px;
-}
-.public-section li { margin-bottom: 6px; }
-.public-section a { color: var(--accent); }
-
-.public-section--callout {
- border-left: 3px solid var(--accent);
- padding: 16px 22px;
- background: var(--surface);
- border-radius: 0 4px 4px 0;
- margin: 0 0 32px;
-}
-.public-section--warning {
- border-left-color: var(--negative);
- background: color-mix(in srgb, var(--negative) 6%, var(--bg));
-}
-.public-section--warning a { color: var(--text); }
-
-/* --- "What this is not" strip on landing --------------------------- */
-
-.not-strip {
- border: 1px dashed var(--border);
- padding: 18px 22px;
- border-radius: 4px;
- margin: 0 0 56px;
- background: var(--surface);
-}
-.not-strip strong { color: var(--text); }
-.not-strip ul { display: flex; flex-wrap: wrap; gap: 18px 28px; margin: 8px 0 0; padding: 0; list-style: none; }
-.not-strip li { color: var(--muted); font-size: 13px; }
-.not-strip li::before { content: "✕ "; color: var(--negative); font-weight: 700; margin-right: 4px; }
-
-/* --- Pricing comparison -------------------------------------------- */
-
-.tier-grid {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
- gap: 20px;
- margin: 8px 0 40px;
-}
-.tier-card {
- position: relative;
- border: 1px solid var(--border);
- border-radius: 6px;
- padding: 28px 26px 28px;
- background: var(--surface);
- display: flex;
- flex-direction: column;
-}
-.tier-card--featured {
- border-color: var(--accent);
- box-shadow: 0 0 0 1px var(--accent) inset,
- 0 12px 32px rgba(15, 23, 42, 0.10);
-}
-[data-theme="dark"] .tier-card--featured {
- box-shadow: 0 0 0 1px var(--accent) inset,
- 0 12px 32px rgba(0, 0, 0, 0.45);
-}
-.tier-card__badge {
- position: absolute;
- top: -11px;
- left: 24px;
- background: var(--accent);
- color: var(--bg);
- font-family: var(--font-mono);
- font-size: 10px;
- letter-spacing: 0.10em;
- text-transform: uppercase;
- font-weight: 600;
- padding: 4px 10px;
- border-radius: 3px;
-}
-/* Tier name — the actual heading, not the small uppercase chip it used
- to be. Pairs with .tier-card__tagline for a one-line value framing. */
-.tier-card__name {
- font-size: 26px;
- font-weight: 700;
- letter-spacing: -0.01em;
- color: var(--text);
- margin: 0 0 4px;
- line-height: 1.1;
-}
-.tier-card__tagline {
- font-size: 13px;
- color: var(--muted);
- line-height: 1.5;
- margin-bottom: 22px;
-}
-.tier-card__price {
- font-size: 40px;
- font-weight: 700;
- color: var(--text);
- line-height: 1;
- margin-bottom: 8px;
- letter-spacing: -0.02em;
-}
-.tier-card__price-unit {
- font-size: 15px;
- color: var(--muted);
- font-weight: 400;
- letter-spacing: 0;
-}
-.tier-card__price-hint {
- font-size: 12px;
- color: var(--muted);
- line-height: 1.55;
- margin-bottom: 20px;
-}
-.tier-card__divider {
- height: 1px;
- background: var(--border);
- margin: 0 0 18px;
-}
-.tier-card__list-head {
- font-family: var(--font-mono);
- font-size: 10.5px;
- color: var(--muted);
- letter-spacing: 0.10em;
- text-transform: uppercase;
- margin-bottom: 12px;
-}
-.tier-card ul {
- list-style: none;
- padding: 0;
- margin: 0 0 24px;
- flex: 1;
-}
-.tier-card li {
- font-size: 13.5px;
- color: var(--text);
- line-height: 1.55;
- padding: 8px 0 8px 22px;
- position: relative;
- border-bottom: 1px solid var(--border);
-}
-.tier-card li:last-child { border-bottom: 0; }
-.tier-card li::before {
- content: "✓";
- position: absolute;
- left: 0;
- top: 8px;
- color: var(--positive);
- font-weight: 700;
-}
-.tier-card__cta { margin-top: 18px; }
-/* Consent block above the Subscribe buttons (paid card, logged-in
- free user). The Subscribe buttons render disabled; ticking the box
- is what enables them. Wording covers ToS agreement (both cadences)
- + the Reg 36 CCR 2013 waiver (monthly only). */
-.tier-card__consent {
- display: flex;
- gap: 10px;
- align-items: flex-start;
- margin-bottom: 14px;
- padding: 12px 14px;
- background: var(--surface-2);
- border: 1px solid var(--border);
- border-radius: 4px;
- font-size: 12px;
- line-height: 1.55;
- color: var(--muted);
- cursor: pointer;
-}
-.tier-card__consent input[type="checkbox"] {
- flex-shrink: 0;
- margin-top: 2px;
- cursor: pointer;
-}
-.tier-card__consent a {
- color: var(--accent);
- text-decoration: underline;
-}
-.tier-card__consent strong { color: var(--text); }
-
-.tier-card__more {
- margin-top: 14px;
- padding-top: 14px;
- border-top: 1px dashed var(--border);
- font-size: 12px;
- color: var(--muted);
- line-height: 1.55;
-}
-
-/* Side-by-side feature comparison table. Lives below the cards and
- makes the deltas readable at a glance — the cards sell, the table
- confirms. */
-.compare-table {
- width: 100%;
- border-collapse: collapse;
- margin: 0 0 16px;
- font-size: 13.5px;
-}
-.compare-table th,
-.compare-table td {
- text-align: left;
- padding: 12px 14px;
- border-bottom: 1px solid var(--border);
- vertical-align: top;
- line-height: 1.5;
-}
-.compare-table thead th {
- font-family: var(--font-mono);
- font-size: 10.5px;
- letter-spacing: 0.10em;
- text-transform: uppercase;
- color: var(--muted);
- font-weight: 600;
- border-bottom: 1px solid var(--border);
-}
-.compare-table th[scope="row"] {
- font-weight: 500;
- color: var(--text);
- width: 38%;
-}
-.compare-table td.compare-table__free { color: var(--muted); }
-.compare-table td.compare-table__paid { color: var(--text); font-weight: 500; }
-.compare-table td.compare-table__paid strong { color: var(--accent); font-weight: 600; }
-.compare-table td.compare-table__none { color: var(--dim); }
-@media (max-width: 520px) {
- .compare-table th[scope="row"] { width: 50%; }
- .compare-table th, .compare-table td { padding: 10px 8px; font-size: 13px; }
-}
-
-/* BETA indicator pill in the app header — see app/templates/base.html. */
-.beta-chip {
- display: inline-block;
- margin-left: 8px;
- padding: 2px 7px;
- font-size: 10px;
- font-weight: 700;
- letter-spacing: 0.14em;
- font-family: var(--font-mono);
- color: var(--bg);
- background: var(--accent);
- border-radius: 2px;
- vertical-align: middle;
- user-select: none;
-}
-
-
-/* -----------------------------------------------------------------------------
- Landing-page screenshots: hero shot, thumbnails inside feature cards, gallery
- strip, and a -based lightbox. See app/templates/landing.html. */
-
-/* All clickable screenshots are s — reset the default chrome so they
- read as image cards, not form controls. The shadow is the main "this is a
- screenshot, not part of the page" signal; the border alone blends in. */
-.shot {
- appearance: none;
- -webkit-appearance: none;
- background: var(--surface);
- border: 1px solid var(--border);
- padding: 0;
- margin: 0;
- display: block;
- width: 100%;
- cursor: zoom-in;
- border-radius: 4px;
- overflow: hidden;
- transition: border-color 120ms ease, transform 120ms ease,
- box-shadow 160ms ease;
- position: relative;
- box-shadow: 0 6px 22px rgba(15, 23, 42, 0.18),
- 0 2px 6px rgba(15, 23, 42, 0.10);
-}
-.shot:hover {
- border-color: var(--accent);
- transform: translateY(-1px);
- box-shadow: 0 10px 28px rgba(15, 23, 42, 0.22),
- 0 4px 10px rgba(15, 23, 42, 0.14);
-}
-.shot:focus-visible {
- outline: none;
- border-color: var(--accent);
- box-shadow: 0 0 0 2px var(--accent),
- 0 6px 22px rgba(15, 23, 42, 0.18);
-}
-/* Dark mode: the soft slate shadow disappears against the near-black bg.
- Use a deeper, slightly accent-tinted glow so the cards still lift. */
-[data-theme="dark"] .shot {
- box-shadow: 0 8px 30px rgba(0, 0, 0, 0.55),
- 0 2px 8px rgba(0, 0, 0, 0.35);
-}
-[data-theme="dark"] .shot:hover {
- box-shadow: 0 14px 36px rgba(0, 0, 0, 0.65),
- 0 0 0 1px rgba(0, 217, 255, 0.20);
-}
-
-.shot img {
- display: block;
- width: 100%;
- height: auto;
-}
-
-/* Hero screenshot — sits just below the headline CTAs, full landing width. */
-.shot-hero {
- max-width: 960px;
- margin: 0 auto 56px;
- padding: 0 24px;
-}
-.shot--hero .shot__zoom {
- position: absolute;
- bottom: 10px;
- right: 12px;
- font-family: var(--font-mono);
- font-size: 11px;
- letter-spacing: 0.12em;
- text-transform: uppercase;
- color: var(--bg);
- background: var(--accent);
- padding: 4px 9px;
- border-radius: 3px;
- opacity: 0.85;
- pointer-events: none;
-}
-
-/* Thumbnail at the bottom of each feature card. Vertical alignment across
- the three cards is achieved by `.feature-card__body { flex-grow: 1 }`
- above, which lets the body fill all available space inside the
- equal-height grid cell — the thumbnail then sits at the same y across
- cards. The fixed margin-top keeps a predictable gap above. */
-.feature-card__shot {
- margin-top: 18px;
-}
-.feature-card__shot img {
- max-height: 200px;
- object-fit: cover;
- object-position: top left;
-}
-
-/* "More views" strip — flex so we can drop in 2-3 extra shots later. */
-.shots-section {
- margin-top: 8px;
-}
-.shots-grid {
- display: grid;
- gap: 18px;
- grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
- margin-top: 12px;
-}
-.shot__caption {
- padding: 10px 12px;
- font-size: 12.5px;
- line-height: 1.5;
- color: var(--muted);
- background: var(--surface);
- border-top: 1px solid var(--border);
- text-align: left;
-}
-.shot__caption strong {
- display: block;
- font-size: 13px;
- color: var(--text);
- margin-bottom: 2px;
- font-family: var(--font-mono);
- letter-spacing: 0.04em;
-}
-
-/* Lightbox. handles the modal mechanics (focus trap, ESC-to-close,
- inert background); we just style the surface. */
-.shot-modal {
- border: 1px solid var(--border);
- background: var(--surface);
- color: var(--text);
- max-width: min(96vw, 1400px);
- max-height: 94vh;
- padding: 0;
- border-radius: 6px;
- overflow: hidden;
-}
-.shot-modal::backdrop {
- background: rgba(0, 0, 0, 0.78);
-}
-.shot-modal img {
- display: block;
- max-width: 100%;
- max-height: 80vh;
- width: auto;
- height: auto;
- margin: 0 auto;
-}
-.shot-modal p {
- margin: 0;
- padding: 14px 22px 18px;
- font-size: 13.5px;
- line-height: 1.6;
- color: var(--muted);
- border-top: 1px solid var(--border);
- background: var(--surface-2);
-}
-.shot-modal__close {
- position: absolute;
- top: 6px;
- right: 8px;
- background: transparent;
- border: 0;
- color: var(--text);
- font-size: 28px;
- line-height: 1;
- padding: 4px 10px;
- cursor: pointer;
- font-family: var(--font-mono);
-}
-.shot-modal__close:hover,
-.shot-modal__close:focus-visible {
- color: var(--accent);
- outline: none;
-}
-
-/* --- Invite-a-friend callout (pricing) ----------------------------- */
-/* A single-row visual banner that names the offer at a glance. The
- detail text lives in a behind the "How it works" button to
- keep the pricing page scannable. */
-.invite-callout {
- display: flex;
- align-items: center;
- gap: 20px;
- padding: 20px 24px;
- margin: 0 0 40px;
- background: linear-gradient(135deg,
- color-mix(in srgb, var(--accent) 12%, var(--surface)),
- var(--surface));
- border: 1px solid color-mix(in srgb, var(--accent) 35%, var(--border));
- border-left: 4px solid var(--accent);
- border-radius: 6px;
-}
-.invite-callout__icon {
- font-size: 32px;
- line-height: 1;
- flex-shrink: 0;
- filter: saturate(1.1);
-}
-.invite-callout__body { flex: 1; min-width: 0; }
-.invite-callout__eyebrow {
- font-family: var(--font-mono);
- font-size: 10.5px;
- letter-spacing: 0.10em;
- text-transform: uppercase;
- color: var(--accent);
- font-weight: 600;
- margin-bottom: 4px;
-}
-.invite-callout__headline {
- font-size: 19px;
- font-weight: 600;
- color: var(--text);
- line-height: 1.3;
- margin-bottom: 4px;
-}
-.invite-callout__headline strong { color: var(--accent); font-weight: 700; }
-.invite-callout__sub {
- font-size: 13px;
- color: var(--muted);
- line-height: 1.5;
-}
-.invite-callout .btn-secondary { flex-shrink: 0; }
-@media (max-width: 560px) {
- .invite-callout { flex-direction: column; align-items: flex-start; gap: 14px; }
- .invite-callout .btn-secondary { width: 100%; }
-}
-
-/* Generic text-only modal — reuse for any "click for the details"
- pattern. Same mechanics as .shot-modal. */
-.text-modal {
- border: 1px solid var(--border);
- background: var(--surface);
- color: var(--text);
- max-width: min(92vw, 560px);
- max-height: 88vh;
- padding: 28px 28px 24px;
- border-radius: 6px;
- overflow-y: auto;
- position: relative;
- line-height: 1.6;
-}
-.text-modal::backdrop { background: rgba(0, 0, 0, 0.65); }
-.text-modal__title {
- font-size: 20px;
- font-weight: 700;
- margin: 0 0 14px;
- padding-right: 36px;
- color: var(--text);
-}
-.text-modal__head {
- font-family: var(--font-mono);
- font-size: 10.5px;
- letter-spacing: 0.10em;
- text-transform: uppercase;
- color: var(--muted);
- font-weight: 600;
- margin: 20px 0 8px;
-}
-.text-modal p {
- font-size: 13.5px;
- color: var(--text);
- margin: 0 0 12px;
-}
-.text-modal__list {
- margin: 0 0 8px;
- padding-left: 22px;
-}
-.text-modal__list li {
- font-size: 13.5px;
- color: var(--text);
- margin-bottom: 6px;
- line-height: 1.55;
-}
-.text-modal code {
- font-family: var(--font-mono);
- font-size: 12px;
- background: var(--surface-2);
- padding: 1px 5px;
- border-radius: 2px;
-}
-.text-modal__close {
- position: absolute;
- top: 8px;
- right: 10px;
- background: transparent;
- border: 0;
- color: var(--muted);
- font-size: 26px;
- line-height: 1;
- padding: 4px 10px;
- cursor: pointer;
- font-family: var(--font-mono);
-}
-.text-modal__close:hover,
-.text-modal__close:focus-visible {
- color: var(--accent);
- outline: none;
-}
-
-/* ---------- Dashboard portfolio edit mode -----------------------------
- *
- * Inline composer that sits above the portfolio table. Aesthetic:
- * terminal-style command line, no boxed-form chrome, ghost controls,
- * tinted-neutral palette pulled from --border / --dim / --muted, accent
- * is theme-aware (deep teal in light, electric cyan in dark).
- */
-
-/* The portfolio panel header gains two extra children (the EDIT / Done
- * pills). The global `.panel-header` uses `space-between`, which works
- * for headers with only title+meta but collapses meta into title once
- * any later child has `margin-left: auto`. Switch this header to a
- * gap-based flow; meta now sits 12px from the title, edit pill at the
- * far right via its own auto-margin. */
-#portfolio-panel .panel-header {
- justify-content: flex-start;
- gap: 12px;
-}
-
-/* EDIT / Done toggle buttons in the panel header. */
-.pf-edit-btn,
-.pf-done-btn {
- display: inline-flex;
- align-items: center;
- gap: 4px;
- background: transparent;
- border: 1px solid var(--border);
- color: var(--dim);
- padding: 2px 8px;
- border-radius: 2px;
- cursor: pointer;
- font-family: var(--font-mono);
- font-size: 11px;
- letter-spacing: 0.04em;
- text-transform: lowercase;
- margin-left: auto;
- transition: color 120ms ease-out, border-color 120ms ease-out;
-}
-.pf-edit-btn:hover, .pf-done-btn:hover {
- color: var(--accent);
- border-color: var(--accent);
-}
-/* The JS toggles these via the `hidden` attribute. `display: inline-flex`
- * above otherwise wins over the UA's `[hidden] { display: none }`. */
-.pf-edit-btn[hidden], .pf-done-btn[hidden] { display: none; }
-
-/* × button per row — hidden by default, visible only in edit mode. */
-.pf-row-del-cell { width: 20px; text-align: center; }
-.pf-row-del {
- display: none;
- background: transparent;
- border: none;
- color: var(--dim);
- cursor: pointer;
- font-size: 14px;
- padding: 0 4px;
- font-family: inherit;
- transition: color 120ms ease-out;
-}
-#portfolio-panel.pf-editing .pf-row-del { display: inline; }
-#portfolio-panel.pf-editing .pf-row-del:hover { color: var(--negative); }
-
-/* ---------- Inline add-position composer ----------------------------- */
-
-.pf-add {
- padding: 6px 12px 8px;
- border-bottom: 1px dashed var(--border);
- margin-bottom: 6px;
- font-family: var(--font-mono);
- font-size: 12px;
-}
-.pf-add__line {
- display: flex;
- align-items: center;
- gap: 6px;
- flex-wrap: wrap;
-}
-.pf-add__prompt {
- color: var(--accent);
- font-weight: 600;
- user-select: none;
- margin-right: 2px;
-}
-.pf-add__div {
- width: 1px;
- height: 14px;
- background: var(--border);
- margin: 0 2px;
-}
-.pf-add__at {
- color: var(--dim);
- font-size: 11px;
- user-select: none;
-}
-.pf-add__line input[type="text"],
-.pf-add__line input[type="number"],
-.pf-add__line input[type="date"] {
- background: transparent;
- border: none;
- border-bottom: 1px solid var(--border);
- color: var(--text);
- padding: 2px 4px;
- font-family: inherit;
- font-size: 12px;
- line-height: 1.4;
- font-variant-numeric: tabular-nums;
- border-radius: 0;
-}
-.pf-add__line input:focus {
- outline: none;
- border-bottom-color: var(--accent);
- background: color-mix(in srgb, var(--accent) 6%, transparent);
-}
-.pf-add__line input::placeholder {
- color: var(--dim);
- font-style: italic;
-}
-.pf-add__ticker {
- width: 80px;
- text-transform: uppercase;
- letter-spacing: 0.05em;
- font-weight: 600;
-}
-.pf-add__num--qty { width: 56px; text-align: right; }
-.pf-add__num--cost { width: 76px; text-align: right; }
-.pf-add__date { width: 128px; margin-left: 4px; }
-
-/* Tiny pill that shows after a successful validate: "172.40 USD". */
-.pf-add-currency {
- color: var(--muted);
- font-size: 11px;
- min-width: 24px;
- letter-spacing: 0.02em;
-}
-.pf-add-currency:empty { display: none; }
-
-/* Calendar-icon button — ghost, square, terminal-feel. */
-.pf-add__icon {
- background: transparent;
- border: 1px solid var(--border);
- color: var(--dim);
- width: 22px;
- height: 22px;
- padding: 0;
- border-radius: 2px;
- cursor: pointer;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- transition: color 120ms ease-out, border-color 120ms ease-out;
-}
-.pf-add__icon:hover {
- color: var(--accent);
- border-color: var(--accent);
-}
-.pf-add__icon:focus-visible {
- outline: none;
- color: var(--accent);
- border-color: var(--accent);
-}
-
-/* Submit button — a square accent-bordered plus glyph. Visually
- * heavier than the ghost calendar icon (larger size, accent border)
- * so the primary action reads as primary. Lights up to solid accent
- * on hover/focus when enabled. */
-.pf-add__submit {
- margin-left: auto;
- background: transparent;
- border: 1px solid var(--accent);
- color: var(--accent);
- width: 26px;
- height: 26px;
- padding: 0;
- border-radius: 2px;
- cursor: pointer;
- font-family: inherit;
- font-size: 18px;
- font-weight: 600;
- line-height: 1;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- transition: background 120ms ease-out, color 120ms ease-out,
- border-color 120ms ease-out, transform 120ms ease-out;
-}
-.pf-add__submit:hover:not(:disabled),
-.pf-add__submit:focus-visible:not(:disabled) {
- background: var(--accent);
- color: var(--bg);
- outline: none;
-}
-.pf-add__submit:active:not(:disabled) {
- transform: scale(0.94);
-}
-.pf-add__submit:disabled {
- border-color: var(--border);
- color: var(--dim);
- cursor: not-allowed;
-}
-
-/* Status indicators next to the ticker + below the row. */
-.pf-add-status {
- font-size: 11px;
- color: var(--muted);
- font-variant-numeric: tabular-nums;
-}
-.pf-add-status:empty { display: none; }
-.pf-add-status--pending { color: var(--dim); font-style: italic; }
-.pf-add-status--ok { color: var(--positive); }
-.pf-add-status--err { color: var(--negative); }
-
-/* Secondary line below the main row — only takes space when a child has
- * content. Holds the date-lookup status and the duplicate warning. */
-.pf-add__notes {
- display: flex;
- gap: 14px;
- margin-top: 4px;
- font-size: 11px;
-}
-.pf-add__notes:has(:empty:only-child) { display: none; }
-.pf-add-warning {
- color: var(--warning);
-}
-.pf-add-warning:empty { display: none; }
-
-/* Quietly explains the controls. Shown only when the form is visible,
- * which is to say only in edit mode. */
-.pf-add__hint {
- margin: 6px 0 0;
- font-size: 11px;
- color: var(--dim);
- line-height: 1.5;
- font-style: italic;
-}
-.pf-add__hint kbd {
- font-family: inherit;
- font-style: normal;
- font-size: 11px;
- padding: 0 4px;
- border: 1px solid var(--border);
- border-radius: 2px;
- color: var(--muted);
- background: color-mix(in srgb, var(--accent) 4%, transparent);
-}
diff --git a/app/static/css/dashboard.css b/app/static/css/dashboard.css
new file mode 100644
index 0000000..956157d
--- /dev/null
+++ b/app/static/css/dashboard.css
@@ -0,0 +1,228 @@
+/* Cassandra — dashboard-specific widgets: market chips, aggregate read
+ * header, indicator summary, glossary tooltips, group tabs, badges. */
+
+/* --- Dashboard top header (markets + aggregate read) ----------------- */
+
+.dash-header {
+ display: grid;
+ grid-template-columns: 1fr;
+ gap: 12px;
+ margin-bottom: 0;
+}
+.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;
+}
+
+.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);
+}
+
+/* --- 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; }
+
+/* BETA indicator pill in the app header — see app/templates/base.html. */
+.beta-chip {
+ display: inline-block;
+ margin-left: 8px;
+ padding: 2px 7px;
+ font-size: 10px;
+ font-weight: 700;
+ letter-spacing: 0.14em;
+ font-family: var(--font-mono);
+ color: var(--bg);
+ background: var(--accent);
+ border-radius: 2px;
+ vertical-align: middle;
+ user-select: none;
+}
diff --git a/app/static/css/layout.css b/app/static/css/layout.css
new file mode 100644
index 0000000..c4293b6
--- /dev/null
+++ b/app/static/css/layout.css
@@ -0,0 +1,185 @@
+/* Cassandra — structural layout: html/body, app shell, header, main grid,
+ * sticky markets bar, scrollbar. */
+
+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;
+}
+
+/* --- Scrollbar -------------------------------------------------------- */
+
+::-webkit-scrollbar { width: 8px; height: 8px; }
+::-webkit-scrollbar-track { background: var(--bg); }
+::-webkit-scrollbar-thumb { background: var(--dim); border-radius: 0; }
+::-webkit-scrollbar-thumb:hover { background: var(--muted); }
diff --git a/app/static/css/log-chat.css b/app/static/css/log-chat.css
new file mode 100644
index 0000000..6e847ec
--- /dev/null
+++ b/app/static/css/log-chat.css
@@ -0,0 +1,282 @@
+/* Cassandra — log panel, log page layout, calendar widget, chat sidebar. */
+
+/* --- 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); }
+
+/* --- Chat sidebar ----------------------------------------------------- */
+
+.chat-header {
+ border-bottom: 1px solid var(--border);
+ padding: 6px 4px 8px;
+ margin-bottom: 6px;
+ display: flex;
+ flex-direction: column;
+}
+.chat-title {
+ color: var(--accent);
+ font-weight: 700;
+ text-transform: uppercase;
+ letter-spacing: 0.1em;
+ font-size: 11px;
+}
+.chat-title::before { content: "▸ "; }
+.chat-hint { color: var(--dim); font-size: 10px; margin-top: 2px; }
+
+.chat-thread {
+ flex: 1 1 auto;
+ overflow-y: auto;
+ padding: 4px 2px;
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ min-height: 0;
+}
+.chat-msg {
+ font-family: var(--font-sans);
+ font-size: 13.5px;
+ padding: 9px 11px;
+ border: 1px solid var(--border);
+ line-height: 1.6;
+ word-wrap: break-word;
+}
+.chat-msg--system {
+ color: var(--muted);
+ font-size: 12px;
+ background: transparent;
+ border-style: dashed;
+ font-family: var(--font-mono);
+}
+.chat-msg--user {
+ background: var(--user-bubble-bg);
+ border-color: var(--accent);
+ color: var(--text);
+ align-self: flex-end;
+ max-width: 92%;
+ white-space: pre-wrap;
+}
+.chat-msg--user::before {
+ content: "you › ";
+ font-family: var(--font-mono);
+ color: var(--accent);
+ opacity: 0.7;
+ font-size: 10px;
+}
+.chat-msg--assistant { background: var(--surface-2); color: var(--text); }
+.chat-msg--assistant::before {
+ content: "cassandra › ";
+ font-family: var(--font-mono);
+ color: var(--accent);
+ opacity: 0.7;
+ font-size: 10px;
+}
+.chat-msg--pending { color: var(--dim); font-style: italic; }
+.chat-msg--error { color: var(--negative); border-color: var(--negative); }
+
+.chat-msg p { margin: 0.4em 0; }
+.chat-msg p:first-child { margin-top: 0; }
+.chat-msg p:last-child { margin-bottom: 0; }
+.chat-msg h2, .chat-msg h3, .chat-msg h4 {
+ font-family: var(--font-mono);
+ color: var(--accent);
+ font-size: 11px;
+ margin: 0.8em 0 0.3em;
+ text-transform: uppercase;
+ letter-spacing: 0.06em;
+}
+.chat-msg strong { color: var(--text); font-weight: 700; }
+.chat-msg em { color: var(--muted); font-style: italic; }
+
+.chat-form {
+ border-top: 1px solid var(--border);
+ padding-top: 6px;
+ display: flex;
+ gap: 6px;
+ align-items: flex-end;
+}
+.chat-form textarea {
+ flex: 1;
+ background: var(--bg);
+ border: 1px solid var(--border);
+ color: var(--text);
+ font-family: inherit;
+ font-size: 12px;
+ padding: 6px 8px;
+ resize: vertical;
+ min-height: 36px;
+ outline: none;
+}
+.chat-form textarea:focus { border-color: var(--accent); }
+.chat-form button {
+ background: transparent;
+ border: 1px solid var(--accent);
+ color: var(--accent);
+ font-family: inherit;
+ font-size: 11px;
+ padding: 6px 12px;
+ text-transform: uppercase;
+ letter-spacing: 0.08em;
+ cursor: pointer;
+}
+.chat-form button:hover:not(:disabled) { background: var(--accent); color: var(--bg); }
+.chat-form button:disabled { opacity: 0.4; cursor: not-allowed; }
diff --git a/app/static/css/news.css b/app/static/css/news.css
new file mode 100644
index 0000000..8827339
--- /dev/null
+++ b/app/static/css/news.css
@@ -0,0 +1,86 @@
+/* Cassandra — news panel: rows, tag chips, filter pills. */
+
+/* --- News ------------------------------------------------------------- */
+
+.news-row {
+ padding: 4px 12px;
+ display: grid;
+ /* age | source | title | tags-on-right | utc-time */
+ grid-template-columns: 50px 130px minmax(0, 1fr) minmax(0, auto) 110px;
+ gap: 12px;
+ font-size: 12px;
+ border-bottom: 1px solid var(--surface-2);
+ align-items: center;
+}
+@media (max-width: 720px) {
+ .news-row { grid-template-columns: 50px 100px 1fr; }
+ .news-row .local,
+ .news-row__tags { display: none; }
+}
+.news-row:hover { background: color-mix(in srgb, var(--accent) 5%, transparent); }
+.news-row .age { color: var(--dim); text-align: right; }
+.news-row .source { color: var(--muted); font-size: 11px; }
+.news-row .title { color: var(--text); }
+.news-row .title:hover { color: var(--accent); }
+.news-row .local {
+ color: var(--muted);
+ font-size: 11px;
+ text-align: right;
+ font-variant-numeric: tabular-nums;
+ white-space: nowrap;
+}
+
+/* News tag chips on each row + the top-bar pill toggles */
+.news-row__tags {
+ display: inline-flex;
+ flex-wrap: nowrap;
+ gap: 3px;
+ justify-content: flex-end;
+ overflow: hidden;
+ max-width: 100%;
+}
+.tag-chip {
+ font-family: var(--font-mono);
+ font-size: 9px;
+ letter-spacing: 0.04em;
+ color: var(--muted);
+ background: var(--surface-2);
+ border: 1px solid var(--border);
+ padding: 0 4px;
+ white-space: nowrap;
+ text-transform: uppercase;
+ line-height: 1.5;
+}
+
+.news-tags {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 4px;
+ padding: 8px 12px;
+ border-bottom: 1px solid var(--border);
+ background: var(--surface-2);
+}
+.news-tag {
+ font-family: var(--font-mono);
+ font-size: 10.5px;
+ letter-spacing: 0.04em;
+ text-transform: uppercase;
+ color: var(--muted);
+ background: transparent;
+ border: 1px solid var(--border);
+ padding: 3px 8px;
+ cursor: pointer;
+}
+.news-tag:hover { color: var(--accent); border-color: var(--accent); }
+.news-tag[data-state="include"] {
+ background: var(--accent);
+ color: var(--bg);
+ border-color: var(--accent);
+}
+.news-tag[data-state="exclude"] {
+ color: var(--negative);
+ border-color: var(--negative);
+ text-decoration: line-through;
+}
+.news-tag--clear { color: var(--dim); border-style: dashed; }
+.news-tag--clear:hover { color: var(--negative); border-color: var(--negative); }
diff --git a/app/static/css/panels.css b/app/static/css/panels.css
new file mode 100644
index 0000000..18293a5
--- /dev/null
+++ b/app/static/css/panels.css
@@ -0,0 +1,92 @@
+/* Cassandra — panel chrome, tables, status LEDs, utility colour classes. */
+
+/* --- 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); }
+
+/* --- 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); }
+
+/* --- Empty / loading state ------------------------------------------- */
+
+.empty {
+ padding: 24px;
+ text-align: center;
+ color: var(--muted);
+ font-size: 11px;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+}
+.htmx-indicator {
+ display: inline-block;
+ color: var(--dim);
+ opacity: 0;
+ transition: opacity 0.2s;
+}
+.htmx-request .htmx-indicator { opacity: 1; }
diff --git a/app/static/css/portfolio.css b/app/static/css/portfolio.css
new file mode 100644
index 0000000..89ffb32
--- /dev/null
+++ b/app/static/css/portfolio.css
@@ -0,0 +1,376 @@
+/* Cassandra — portfolio panel styles: overall stats, actions, inline edit
+ * mode (add composer, delete rows), analysis accordion. */
+
+/* --- 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); }
+
+.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;
+}
+
+/* ---------- Dashboard portfolio edit mode -----------------------------
+ *
+ * Inline composer that sits above the portfolio table. Aesthetic:
+ * terminal-style command line, no boxed-form chrome, ghost controls,
+ * tinted-neutral palette pulled from --border / --dim / --muted, accent
+ * is theme-aware (deep teal in light, electric cyan in dark).
+ */
+
+/* The portfolio panel header gains two extra children (the EDIT / Done
+ * pills). The global `.panel-header` uses `space-between`, which works
+ * for headers with only title+meta but collapses meta into title once
+ * any later child has `margin-left: auto`. Switch this header to a
+ * gap-based flow; meta now sits 12px from the title, edit pill at the
+ * far right via its own auto-margin. */
+#portfolio-panel .panel-header {
+ justify-content: flex-start;
+ gap: 12px;
+}
+
+/* EDIT / Done toggle buttons in the panel header. */
+.pf-edit-btn,
+.pf-done-btn {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ background: transparent;
+ border: 1px solid var(--border);
+ color: var(--dim);
+ padding: 2px 8px;
+ border-radius: 2px;
+ cursor: pointer;
+ font-family: var(--font-mono);
+ font-size: 11px;
+ letter-spacing: 0.04em;
+ text-transform: lowercase;
+ margin-left: auto;
+ transition: color 120ms ease-out, border-color 120ms ease-out;
+}
+.pf-edit-btn:hover, .pf-done-btn:hover {
+ color: var(--accent);
+ border-color: var(--accent);
+}
+/* The JS toggles these via the `hidden` attribute. `display: inline-flex`
+ * above otherwise wins over the UA's `[hidden] { display: none }`. */
+.pf-edit-btn[hidden], .pf-done-btn[hidden] { display: none; }
+
+/* × button per row — hidden by default, visible only in edit mode. */
+.pf-row-del-cell { width: 20px; text-align: center; }
+.pf-row-del {
+ display: none;
+ background: transparent;
+ border: none;
+ color: var(--dim);
+ cursor: pointer;
+ font-size: 14px;
+ padding: 0 4px;
+ font-family: inherit;
+ transition: color 120ms ease-out;
+}
+#portfolio-panel.pf-editing .pf-row-del { display: inline; }
+#portfolio-panel.pf-editing .pf-row-del:hover { color: var(--negative); }
+
+/* ---------- Inline add-position composer ----------------------------- */
+
+.pf-add {
+ padding: 6px 12px 8px;
+ border-bottom: 1px dashed var(--border);
+ margin-bottom: 6px;
+ font-family: var(--font-mono);
+ font-size: 12px;
+}
+.pf-add__line {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ flex-wrap: wrap;
+}
+.pf-add__prompt {
+ color: var(--accent);
+ font-weight: 600;
+ user-select: none;
+ margin-right: 2px;
+}
+.pf-add__div {
+ width: 1px;
+ height: 14px;
+ background: var(--border);
+ margin: 0 2px;
+}
+.pf-add__at {
+ color: var(--dim);
+ font-size: 11px;
+ user-select: none;
+}
+.pf-add__line input[type="text"],
+.pf-add__line input[type="number"],
+.pf-add__line input[type="date"] {
+ background: transparent;
+ border: none;
+ border-bottom: 1px solid var(--border);
+ color: var(--text);
+ padding: 2px 4px;
+ font-family: inherit;
+ font-size: 12px;
+ line-height: 1.4;
+ font-variant-numeric: tabular-nums;
+ border-radius: 0;
+}
+.pf-add__line input:focus {
+ outline: none;
+ border-bottom-color: var(--accent);
+ background: color-mix(in srgb, var(--accent) 6%, transparent);
+}
+.pf-add__line input::placeholder {
+ color: var(--dim);
+ font-style: italic;
+}
+.pf-add__ticker {
+ width: 80px;
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+ font-weight: 600;
+}
+.pf-add__num--qty { width: 56px; text-align: right; }
+.pf-add__num--cost { width: 76px; text-align: right; }
+.pf-add__date { width: 128px; margin-left: 4px; }
+
+/* Tiny pill that shows after a successful validate: "172.40 USD". */
+.pf-add-currency {
+ color: var(--muted);
+ font-size: 11px;
+ min-width: 24px;
+ letter-spacing: 0.02em;
+}
+.pf-add-currency:empty { display: none; }
+
+/* Calendar-icon button — ghost, square, terminal-feel. */
+.pf-add__icon {
+ background: transparent;
+ border: 1px solid var(--border);
+ color: var(--dim);
+ width: 22px;
+ height: 22px;
+ padding: 0;
+ border-radius: 2px;
+ cursor: pointer;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ transition: color 120ms ease-out, border-color 120ms ease-out;
+}
+.pf-add__icon:hover {
+ color: var(--accent);
+ border-color: var(--accent);
+}
+.pf-add__icon:focus-visible {
+ outline: none;
+ color: var(--accent);
+ border-color: var(--accent);
+}
+
+/* Submit button — a square accent-bordered plus glyph. Visually
+ * heavier than the ghost calendar icon (larger size, accent border)
+ * so the primary action reads as primary. Lights up to solid accent
+ * on hover/focus when enabled. */
+.pf-add__submit {
+ margin-left: auto;
+ background: transparent;
+ border: 1px solid var(--accent);
+ color: var(--accent);
+ width: 26px;
+ height: 26px;
+ padding: 0;
+ border-radius: 2px;
+ cursor: pointer;
+ font-family: inherit;
+ font-size: 18px;
+ font-weight: 600;
+ line-height: 1;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ transition: background 120ms ease-out, color 120ms ease-out,
+ border-color 120ms ease-out, transform 120ms ease-out;
+}
+.pf-add__submit:hover:not(:disabled),
+.pf-add__submit:focus-visible:not(:disabled) {
+ background: var(--accent);
+ color: var(--bg);
+ outline: none;
+}
+.pf-add__submit:active:not(:disabled) {
+ transform: scale(0.94);
+}
+.pf-add__submit:disabled {
+ border-color: var(--border);
+ color: var(--dim);
+ cursor: not-allowed;
+}
+
+/* Status indicators next to the ticker + below the row. */
+.pf-add-status {
+ font-size: 11px;
+ color: var(--muted);
+ font-variant-numeric: tabular-nums;
+}
+.pf-add-status:empty { display: none; }
+.pf-add-status--pending { color: var(--dim); font-style: italic; }
+.pf-add-status--ok { color: var(--positive); }
+.pf-add-status--err { color: var(--negative); }
+
+/* Secondary line below the main row — only takes space when a child has
+ * content. Holds the date-lookup status and the duplicate warning. */
+.pf-add__notes {
+ display: flex;
+ gap: 14px;
+ margin-top: 4px;
+ font-size: 11px;
+}
+.pf-add__notes:has(:empty:only-child) { display: none; }
+.pf-add-warning {
+ color: var(--warning);
+}
+.pf-add-warning:empty { display: none; }
+
+/* Quietly explains the controls. Shown only when the form is visible,
+ * which is to say only in edit mode. */
+.pf-add__hint {
+ margin: 6px 0 0;
+ font-size: 11px;
+ color: var(--dim);
+ line-height: 1.5;
+ font-style: italic;
+}
+.pf-add__hint kbd {
+ font-family: inherit;
+ font-style: normal;
+ font-size: 11px;
+ padding: 0 4px;
+ border: 1px solid var(--border);
+ border-radius: 2px;
+ color: var(--muted);
+ background: color-mix(in srgb, var(--accent) 4%, transparent);
+}
diff --git a/app/static/css/public.css b/app/static/css/public.css
new file mode 100644
index 0000000..0e0e361
--- /dev/null
+++ b/app/static/css/public.css
@@ -0,0 +1,717 @@
+/* Cassandra — public pages: landing, pricing, about, terms, privacy,
+ * disclaimer. Shared by all templates extending public_base.html.
+ * Visual language matches the app shell but without dashboard chrome. */
+
+.public-page { background: var(--bg); }
+
+.public-shell {
+ display: flex;
+ flex-direction: column;
+ min-height: 100vh;
+ max-width: 1080px;
+ margin: 0 auto;
+ padding: 0 24px;
+}
+
+.public-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 22px 0 16px;
+ border-bottom: 1px solid var(--border);
+}
+.public-header__brand {
+ color: var(--accent);
+ font-weight: 700;
+ text-decoration: none;
+ font-family: var(--font-mono);
+ font-size: 15px;
+ letter-spacing: 0.01em;
+}
+.public-header__brand::before { content: "▰ "; opacity: 0.6; }
+.public-header__brand:hover { color: var(--text); }
+.public-header__nav { display: flex; align-items: center; gap: 22px; }
+.public-header__nav a {
+ color: var(--muted);
+ font-size: 13px;
+ text-decoration: none;
+}
+.public-header__nav a:hover,
+.public-header__nav a.active { color: var(--text); }
+.public-header__cta {
+ color: var(--accent) !important;
+ border: 1px solid var(--accent);
+ padding: 6px 14px;
+ border-radius: 3px;
+}
+.public-header__cta:hover { background: var(--accent); color: var(--bg) !important; }
+
+.public-main {
+ flex: 1;
+ padding: 48px 0 64px;
+}
+
+.public-footer {
+ border-top: 1px solid var(--border);
+ padding: 28px 0 36px;
+ margin-top: 24px;
+ font-size: 12px;
+ color: var(--muted);
+}
+.public-footer__inner {
+ display: flex;
+ flex-direction: column;
+ gap: 14px;
+}
+.public-footer__brand strong { color: var(--text); margin-right: 10px; }
+.public-footer__tagline { color: var(--muted); }
+.public-footer__links { display: flex; flex-wrap: wrap; gap: 16px; }
+.public-footer__links a { color: var(--muted); text-decoration: none; }
+.public-footer__links a:hover { color: var(--accent); }
+.public-footer__meta { color: var(--dim); font-size: 11px; }
+
+/* --- Hero (landing) -------------------------------------------------- */
+
+.hero {
+ padding: 32px 0 48px;
+ border-bottom: 1px solid var(--border);
+ margin-bottom: 48px;
+}
+.hero__brand {
+ color: var(--muted);
+ font-family: var(--font-mono);
+ font-size: 12px;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+}
+.hero__headline {
+ font-size: clamp(28px, 5vw, 44px);
+ font-weight: 700;
+ line-height: 1.15;
+ color: var(--text);
+ margin: 12px 0 14px;
+ letter-spacing: -0.01em;
+}
+.hero__subhead {
+ font-size: 16px;
+ color: var(--muted);
+ max-width: 640px;
+ line-height: 1.55;
+ margin: 0 0 24px;
+}
+.hero__ctas { display: flex; flex-wrap: wrap; gap: 12px; }
+/* Shared button shape — was previously scoped to .hero__ctas, which made
+ the pricing-card CTAs render as bare anchors. */
+.btn-primary,
+.btn-secondary {
+ display: inline-block;
+ padding: 10px 22px;
+ border-radius: 3px;
+ font-size: 13.5px;
+ font-weight: 500;
+ line-height: 1.4;
+ text-decoration: none;
+ text-align: center;
+ cursor: pointer;
+}
+/* Block variant: full-width within parent, slightly taller — used inside
+ tier cards so each CTA spans the card and reads as the obvious action. */
+.btn-block { display: block; width: 100%; padding: 12px 22px; font-size: 14px; }
+
+/* Qualify with `a` so we beat `a { color: var(--accent) }` and any
+ :link/:visited UA defaults. Without `a.btn-primary` the cascade can
+ resolve in favour of the visited-link color on some browsers and the
+ label disappears against the accent background. */
+a.btn-primary,
+a.btn-primary:link,
+a.btn-primary:visited {
+ background: var(--accent);
+ color: var(--bg);
+ border: 1px solid var(--accent);
+}
+a.btn-primary:hover { background: transparent; color: var(--accent); }
+a.btn-secondary,
+a.btn-secondary:link,
+a.btn-secondary:visited {
+ background: transparent;
+ color: var(--text);
+ border: 1px solid var(--border);
+}
+a.btn-secondary:hover { color: var(--accent); border-color: var(--accent); }
+
+/* --- Feature blocks (landing) --------------------------------------- */
+
+.feature-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
+ gap: 24px;
+ margin: 8px 0 56px;
+}
+.feature-card {
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ padding: 22px 22px 24px;
+ background: var(--surface);
+ /* Flex column so the screenshot thumbnail can dock to the bottom via
+ margin-top:auto — that's what lines the three thumbnails up across
+ cards regardless of body-text length. */
+ display: flex;
+ flex-direction: column;
+}
+.feature-card__tag {
+ font-family: var(--font-mono);
+ font-size: 10.5px;
+ color: var(--accent);
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+ margin-bottom: 10px;
+}
+.feature-card__title {
+ font-size: 17px;
+ font-weight: 600;
+ color: var(--text);
+ margin: 0 0 10px;
+}
+.feature-card__body {
+ font-size: 13.5px;
+ line-height: 1.6;
+ color: var(--muted);
+ margin: 0;
+ /* Grow to fill the flex column so the thumbnail below docks to the
+ bottom of the card. With grid-stretched equal-height cards, this is
+ what aligns the thumbnails across the three cards. */
+ flex-grow: 1;
+}
+
+/* --- Section primitives reused across pricing/about/legal ---------- */
+
+.public-section {
+ margin: 0 0 56px;
+}
+.public-section__head {
+ font-size: 20px;
+ font-weight: 600;
+ color: var(--text);
+ margin: 0 0 16px;
+ padding-bottom: 8px;
+ border-bottom: 1px solid var(--border);
+}
+.public-section h3 {
+ font-size: 15px;
+ font-weight: 600;
+ color: var(--text);
+ margin: 24px 0 8px;
+}
+.public-section p,
+.public-section li {
+ font-size: 14px;
+ line-height: 1.65;
+ color: var(--text);
+}
+.public-section p { margin: 0 0 14px; }
+.public-section ul {
+ margin: 0 0 16px;
+ padding-left: 22px;
+}
+.public-section li { margin-bottom: 6px; }
+.public-section a { color: var(--accent); }
+
+.public-section--callout {
+ border-left: 3px solid var(--accent);
+ padding: 16px 22px;
+ background: var(--surface);
+ border-radius: 0 4px 4px 0;
+ margin: 0 0 32px;
+}
+.public-section--warning {
+ border-left-color: var(--negative);
+ background: color-mix(in srgb, var(--negative) 6%, var(--bg));
+}
+.public-section--warning a { color: var(--text); }
+
+/* --- "What this is not" strip on landing --------------------------- */
+
+.not-strip {
+ border: 1px dashed var(--border);
+ padding: 18px 22px;
+ border-radius: 4px;
+ margin: 0 0 56px;
+ background: var(--surface);
+}
+.not-strip strong { color: var(--text); }
+.not-strip ul { display: flex; flex-wrap: wrap; gap: 18px 28px; margin: 8px 0 0; padding: 0; list-style: none; }
+.not-strip li { color: var(--muted); font-size: 13px; }
+.not-strip li::before { content: "✕ "; color: var(--negative); font-weight: 700; margin-right: 4px; }
+
+/* --- Pricing comparison -------------------------------------------- */
+
+.tier-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
+ gap: 20px;
+ margin: 8px 0 40px;
+}
+.tier-card {
+ position: relative;
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ padding: 28px 26px 28px;
+ background: var(--surface);
+ display: flex;
+ flex-direction: column;
+}
+.tier-card--featured {
+ border-color: var(--accent);
+ box-shadow: 0 0 0 1px var(--accent) inset,
+ 0 12px 32px rgba(15, 23, 42, 0.10);
+}
+[data-theme="dark"] .tier-card--featured {
+ box-shadow: 0 0 0 1px var(--accent) inset,
+ 0 12px 32px rgba(0, 0, 0, 0.45);
+}
+.tier-card__badge {
+ position: absolute;
+ top: -11px;
+ left: 24px;
+ background: var(--accent);
+ color: var(--bg);
+ font-family: var(--font-mono);
+ font-size: 10px;
+ letter-spacing: 0.10em;
+ text-transform: uppercase;
+ font-weight: 600;
+ padding: 4px 10px;
+ border-radius: 3px;
+}
+/* Tier name — the actual heading, not the small uppercase chip it used
+ to be. Pairs with .tier-card__tagline for a one-line value framing. */
+.tier-card__name {
+ font-size: 26px;
+ font-weight: 700;
+ letter-spacing: -0.01em;
+ color: var(--text);
+ margin: 0 0 4px;
+ line-height: 1.1;
+}
+.tier-card__tagline {
+ font-size: 13px;
+ color: var(--muted);
+ line-height: 1.5;
+ margin-bottom: 22px;
+}
+.tier-card__price {
+ font-size: 40px;
+ font-weight: 700;
+ color: var(--text);
+ line-height: 1;
+ margin-bottom: 8px;
+ letter-spacing: -0.02em;
+}
+.tier-card__price-unit {
+ font-size: 15px;
+ color: var(--muted);
+ font-weight: 400;
+ letter-spacing: 0;
+}
+.tier-card__price-hint {
+ font-size: 12px;
+ color: var(--muted);
+ line-height: 1.55;
+ margin-bottom: 20px;
+}
+.tier-card__divider {
+ height: 1px;
+ background: var(--border);
+ margin: 0 0 18px;
+}
+.tier-card__list-head {
+ font-family: var(--font-mono);
+ font-size: 10.5px;
+ color: var(--muted);
+ letter-spacing: 0.10em;
+ text-transform: uppercase;
+ margin-bottom: 12px;
+}
+.tier-card ul {
+ list-style: none;
+ padding: 0;
+ margin: 0 0 24px;
+ flex: 1;
+}
+.tier-card li {
+ font-size: 13.5px;
+ color: var(--text);
+ line-height: 1.55;
+ padding: 8px 0 8px 22px;
+ position: relative;
+ border-bottom: 1px solid var(--border);
+}
+.tier-card li:last-child { border-bottom: 0; }
+.tier-card li::before {
+ content: "✓";
+ position: absolute;
+ left: 0;
+ top: 8px;
+ color: var(--positive);
+ font-weight: 700;
+}
+.tier-card__cta { margin-top: 18px; }
+/* Consent block above the Subscribe buttons (paid card, logged-in
+ free user). The Subscribe buttons render disabled; ticking the box
+ is what enables them. Wording covers ToS agreement (both cadences)
+ + the Reg 36 CCR 2013 waiver (monthly only). */
+.tier-card__consent {
+ display: flex;
+ gap: 10px;
+ align-items: flex-start;
+ margin-bottom: 14px;
+ padding: 12px 14px;
+ background: var(--surface-2);
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ font-size: 12px;
+ line-height: 1.55;
+ color: var(--muted);
+ cursor: pointer;
+}
+.tier-card__consent input[type="checkbox"] {
+ flex-shrink: 0;
+ margin-top: 2px;
+ cursor: pointer;
+}
+.tier-card__consent a {
+ color: var(--accent);
+ text-decoration: underline;
+}
+.tier-card__consent strong { color: var(--text); }
+
+.tier-card__more {
+ margin-top: 14px;
+ padding-top: 14px;
+ border-top: 1px dashed var(--border);
+ font-size: 12px;
+ color: var(--muted);
+ line-height: 1.55;
+}
+
+/* Side-by-side feature comparison table. Lives below the cards and
+ makes the deltas readable at a glance — the cards sell, the table
+ confirms. */
+.compare-table {
+ width: 100%;
+ border-collapse: collapse;
+ margin: 0 0 16px;
+ font-size: 13.5px;
+}
+.compare-table th,
+.compare-table td {
+ text-align: left;
+ padding: 12px 14px;
+ border-bottom: 1px solid var(--border);
+ vertical-align: top;
+ line-height: 1.5;
+}
+.compare-table thead th {
+ font-family: var(--font-mono);
+ font-size: 10.5px;
+ letter-spacing: 0.10em;
+ text-transform: uppercase;
+ color: var(--muted);
+ font-weight: 600;
+ border-bottom: 1px solid var(--border);
+}
+.compare-table th[scope="row"] {
+ font-weight: 500;
+ color: var(--text);
+ width: 38%;
+}
+.compare-table td.compare-table__free { color: var(--muted); }
+.compare-table td.compare-table__paid { color: var(--text); font-weight: 500; }
+.compare-table td.compare-table__paid strong { color: var(--accent); font-weight: 600; }
+.compare-table td.compare-table__none { color: var(--dim); }
+@media (max-width: 520px) {
+ .compare-table th[scope="row"] { width: 50%; }
+ .compare-table th, .compare-table td { padding: 10px 8px; font-size: 13px; }
+}
+
+/* --- Landing-page screenshots: hero shot, thumbnails, gallery, lightbox --- */
+
+/* All clickable screenshots are s — reset the default chrome so they
+ read as image cards, not form controls. */
+.shot {
+ appearance: none;
+ -webkit-appearance: none;
+ background: var(--surface);
+ border: 1px solid var(--border);
+ padding: 0;
+ margin: 0;
+ display: block;
+ width: 100%;
+ cursor: zoom-in;
+ border-radius: 4px;
+ overflow: hidden;
+ transition: border-color 120ms ease, transform 120ms ease,
+ box-shadow 160ms ease;
+ position: relative;
+ box-shadow: 0 6px 22px rgba(15, 23, 42, 0.18),
+ 0 2px 6px rgba(15, 23, 42, 0.10);
+}
+.shot:hover {
+ border-color: var(--accent);
+ transform: translateY(-1px);
+ box-shadow: 0 10px 28px rgba(15, 23, 42, 0.22),
+ 0 4px 10px rgba(15, 23, 42, 0.14);
+}
+.shot:focus-visible {
+ outline: none;
+ border-color: var(--accent);
+ box-shadow: 0 0 0 2px var(--accent),
+ 0 6px 22px rgba(15, 23, 42, 0.18);
+}
+/* Dark mode: the soft slate shadow disappears against the near-black bg.
+ Use a deeper, slightly accent-tinted glow so the cards still lift. */
+[data-theme="dark"] .shot {
+ box-shadow: 0 8px 30px rgba(0, 0, 0, 0.55),
+ 0 2px 8px rgba(0, 0, 0, 0.35);
+}
+[data-theme="dark"] .shot:hover {
+ box-shadow: 0 14px 36px rgba(0, 0, 0, 0.65),
+ 0 0 0 1px rgba(0, 217, 255, 0.20);
+}
+
+.shot img {
+ display: block;
+ width: 100%;
+ height: auto;
+}
+
+/* Hero screenshot — sits just below the headline CTAs, full landing width. */
+.shot-hero {
+ max-width: 960px;
+ margin: 0 auto 56px;
+ padding: 0 24px;
+}
+.shot--hero .shot__zoom {
+ position: absolute;
+ bottom: 10px;
+ right: 12px;
+ font-family: var(--font-mono);
+ font-size: 11px;
+ letter-spacing: 0.12em;
+ text-transform: uppercase;
+ color: var(--bg);
+ background: var(--accent);
+ padding: 4px 9px;
+ border-radius: 3px;
+ opacity: 0.85;
+ pointer-events: none;
+}
+
+/* Thumbnail at the bottom of each feature card. */
+.feature-card__shot {
+ margin-top: 18px;
+}
+.feature-card__shot img {
+ max-height: 200px;
+ object-fit: cover;
+ object-position: top left;
+}
+
+/* "More views" strip — flex so we can drop in 2-3 extra shots later. */
+.shots-section {
+ margin-top: 8px;
+}
+.shots-grid {
+ display: grid;
+ gap: 18px;
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
+ margin-top: 12px;
+}
+.shot__caption {
+ padding: 10px 12px;
+ font-size: 12.5px;
+ line-height: 1.5;
+ color: var(--muted);
+ background: var(--surface);
+ border-top: 1px solid var(--border);
+ text-align: left;
+}
+.shot__caption strong {
+ display: block;
+ font-size: 13px;
+ color: var(--text);
+ margin-bottom: 2px;
+ font-family: var(--font-mono);
+ letter-spacing: 0.04em;
+}
+
+/* Lightbox. handles the modal mechanics. */
+.shot-modal {
+ border: 1px solid var(--border);
+ background: var(--surface);
+ color: var(--text);
+ max-width: min(96vw, 1400px);
+ max-height: 94vh;
+ padding: 0;
+ border-radius: 6px;
+ overflow: hidden;
+}
+.shot-modal::backdrop {
+ background: rgba(0, 0, 0, 0.78);
+}
+.shot-modal img {
+ display: block;
+ max-width: 100%;
+ max-height: 80vh;
+ width: auto;
+ height: auto;
+ margin: 0 auto;
+}
+.shot-modal p {
+ margin: 0;
+ padding: 14px 22px 18px;
+ font-size: 13.5px;
+ line-height: 1.6;
+ color: var(--muted);
+ border-top: 1px solid var(--border);
+ background: var(--surface-2);
+}
+.shot-modal__close {
+ position: absolute;
+ top: 6px;
+ right: 8px;
+ background: transparent;
+ border: 0;
+ color: var(--text);
+ font-size: 28px;
+ line-height: 1;
+ padding: 4px 10px;
+ cursor: pointer;
+ font-family: var(--font-mono);
+}
+.shot-modal__close:hover,
+.shot-modal__close:focus-visible {
+ color: var(--accent);
+ outline: none;
+}
+
+/* --- Invite-a-friend callout (pricing) ----------------------------- */
+
+.invite-callout {
+ display: flex;
+ align-items: center;
+ gap: 20px;
+ padding: 20px 24px;
+ margin: 0 0 40px;
+ background: linear-gradient(135deg,
+ color-mix(in srgb, var(--accent) 12%, var(--surface)),
+ var(--surface));
+ border: 1px solid color-mix(in srgb, var(--accent) 35%, var(--border));
+ border-left: 4px solid var(--accent);
+ border-radius: 6px;
+}
+.invite-callout__icon {
+ font-size: 32px;
+ line-height: 1;
+ flex-shrink: 0;
+ filter: saturate(1.1);
+}
+.invite-callout__body { flex: 1; min-width: 0; }
+.invite-callout__eyebrow {
+ font-family: var(--font-mono);
+ font-size: 10.5px;
+ letter-spacing: 0.10em;
+ text-transform: uppercase;
+ color: var(--accent);
+ font-weight: 600;
+ margin-bottom: 4px;
+}
+.invite-callout__headline {
+ font-size: 19px;
+ font-weight: 600;
+ color: var(--text);
+ line-height: 1.3;
+ margin-bottom: 4px;
+}
+.invite-callout__headline strong { color: var(--accent); font-weight: 700; }
+.invite-callout__sub {
+ font-size: 13px;
+ color: var(--muted);
+ line-height: 1.5;
+}
+.invite-callout .btn-secondary { flex-shrink: 0; }
+@media (max-width: 560px) {
+ .invite-callout { flex-direction: column; align-items: flex-start; gap: 14px; }
+ .invite-callout .btn-secondary { width: 100%; }
+}
+
+/* Generic text-only modal — reuse for any "click for the details" pattern. */
+.text-modal {
+ border: 1px solid var(--border);
+ background: var(--surface);
+ color: var(--text);
+ max-width: min(92vw, 560px);
+ max-height: 88vh;
+ padding: 28px 28px 24px;
+ border-radius: 6px;
+ overflow-y: auto;
+ position: relative;
+ line-height: 1.6;
+}
+.text-modal::backdrop { background: rgba(0, 0, 0, 0.65); }
+.text-modal__title {
+ font-size: 20px;
+ font-weight: 700;
+ margin: 0 0 14px;
+ padding-right: 36px;
+ color: var(--text);
+}
+.text-modal__head {
+ font-family: var(--font-mono);
+ font-size: 10.5px;
+ letter-spacing: 0.10em;
+ text-transform: uppercase;
+ color: var(--muted);
+ font-weight: 600;
+ margin: 20px 0 8px;
+}
+.text-modal p {
+ font-size: 13.5px;
+ color: var(--text);
+ margin: 0 0 12px;
+}
+.text-modal__list {
+ margin: 0 0 8px;
+ padding-left: 22px;
+}
+.text-modal__list li {
+ font-size: 13.5px;
+ color: var(--text);
+ margin-bottom: 6px;
+ line-height: 1.55;
+}
+.text-modal code {
+ font-family: var(--font-mono);
+ font-size: 12px;
+ background: var(--surface-2);
+ padding: 1px 5px;
+ border-radius: 2px;
+}
+.text-modal__close {
+ position: absolute;
+ top: 8px;
+ right: 10px;
+ background: transparent;
+ border: 0;
+ color: var(--muted);
+ font-size: 26px;
+ line-height: 1;
+ padding: 4px 10px;
+ cursor: pointer;
+ font-family: var(--font-mono);
+}
+.text-modal__close:hover,
+.text-modal__close:focus-visible {
+ color: var(--accent);
+ outline: none;
+}
diff --git a/app/static/css/settings.css b/app/static/css/settings.css
new file mode 100644
index 0000000..1d6c0e5
--- /dev/null
+++ b/app/static/css/settings.css
@@ -0,0 +1,381 @@
+/* Cassandra — settings page: rows, selects, dropzone, invite block,
+ * user menu dropdown, import preview, action buttons. */
+
+/* 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; }
+
+/* --- 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: