diff --git a/README.md b/README.md index 68127d5..167bb4b 100644 --- a/README.md +++ b/README.md @@ -461,6 +461,23 @@ sourceColor('hackernews') // hex used for the card spine, chip and active pill The source tab and filter pill are generated automatically from `sources()`. +### Colour system (`assets/style.css`) + +The dashboard has **no house colour**. One ink ramp (`--bg` → `--ink`) carries +every neutral surface, line, and label, and the only saturated colour on screen +belongs to the sources — used strictly as a signal: filter pill, card spine, +source chip, timestamp, feed health. Two rules hold it together: + +- **Bright fill, deep ink type.** Every filled state (active pill, source chip, + primary action) is a bright fill with `--ink-deep` type — no white text on + saturated fills, no gradient fills. +- **One hue per source, mirrored in two places.** `:root` defines the token + (`--youtube`, `--reddit`, `--rt`) and `View::sourceColor()` returns the same + hex for the inline `--src`/`--pill` values the templates emit. Change both. + +The header rail (`.wire-rail`) is the colour legend: one segment per source, in +order. + --- ## Deployment notes (Apache + php-fpm on SELinux, e.g. Fedora) diff --git a/assets/style.css b/assets/style.css index 7b41d17..b04627f 100644 --- a/assets/style.css +++ b/assets/style.css @@ -1,21 +1,47 @@ -:root { - --ink: #160b2e; - --ink-soft: #5b5178; - --bg: #f4f1ff; - --surface: #ffffff; - --line: #e6dffb; +/* ========================================================================== + RSS Feed Dashboard — "Nightwire" palette + -------------------------------------------------------------------------- + Thesis: the dashboard has no house colour. One calm ink family carries the + whole interface; the only saturated colour belongs to the sources + (YouTube / Reddit / RT) and is used strictly as a signal — filter pills, + card spines, chips, timestamps, feed health. - --accent: #6a3dff; - --youtube: #ff1f4b; - --reddit: #ff6a00; - --rt: #00c389; - --sun: #ffc24b; + Rule for every filled state: bright fill, deep ink type. + ========================================================================== */ + +:root { + color-scheme: dark; + + /* Ink ramp — cool indigo-slate, not black */ + --bg: #0f1319; + --surface: #161b24; + --surface-2: #1e242f; + --line: #262d3a; + --line-2: #3a4351; + + --ink: #f1f3f7; + --ink-soft: #a3adbd; + --ink-faint: #828da1; + --ink-deep: #0c1015; /* type that sits on a bright fill */ + + /* Source signals — the only chroma in the UI */ + --youtube: #ff3b5c; + --reddit: #ff7a2f; + --rt: #1fcb93; + + /* Neutral interactive: the "All" tab, neutral pills, primary action */ + --accent: var(--ink); --display: "Space Grotesk", system-ui, sans-serif; --body: "Plus Jakarta Sans", system-ui, -apple-system, sans-serif; --mono: "JetBrains Mono", ui-monospace, monospace; } +::selection { + background: var(--ink); + color: var(--ink-deep); +} + /* ---------- Shell ---------- */ .app-body { @@ -27,17 +53,15 @@ -webkit-font-smoothing: antialiased; } +/* One soft wash of panel light from the top edge. No coloured blobs, so the + source hues stay the only chroma on screen. */ .app-body::before { content: ""; position: fixed; inset: 0; z-index: 0; pointer-events: none; - background: - radial-gradient(42rem 30rem at 6% -8%, rgba(106, 61, 255, 0.18), transparent 62%), - radial-gradient(36rem 26rem at 98% -4%, rgba(255, 31, 75, 0.14), transparent 60%), - radial-gradient(34rem 26rem at 52% 112%, rgba(0, 195, 137, 0.16), transparent 62%), - radial-gradient(30rem 22rem at 82% 78%, rgba(255, 106, 0, 0.12), transparent 60%); + background: radial-gradient(58rem 30rem at 50% -14%, rgba(241, 243, 247, 0.055), transparent 72%); } .site-header, @@ -52,10 +76,10 @@ main { position: sticky; top: 0; z-index: 20; - background: rgba(255, 255, 255, 0.78); + background: rgba(15, 19, 25, 0.82); border-bottom: 1px solid var(--line); - backdrop-filter: saturate(180%) blur(16px); - -webkit-backdrop-filter: saturate(180%) blur(16px); + backdrop-filter: saturate(160%) blur(16px); + -webkit-backdrop-filter: saturate(160%) blur(16px); } .brand { @@ -65,12 +89,18 @@ main { text-decoration: none; } +/* Signal bars: three feeds, ascending strength, one per source hue. */ .brand-mark { - width: 26px; + width: 30px; height: 26px; border-radius: 9px; - background: conic-gradient(from 220deg, var(--accent), var(--youtube), var(--reddit), var(--rt), var(--accent)); - box-shadow: 0 6px 16px rgba(106, 61, 255, 0.4); + border: 1px solid var(--line); + background: + linear-gradient(var(--youtube), var(--youtube)) 6px bottom / 3px 8px no-repeat, + linear-gradient(var(--reddit), var(--reddit)) 12px bottom / 3px 13px no-repeat, + linear-gradient(var(--rt), var(--rt)) 18px bottom / 3px 18px no-repeat, + var(--surface-2); + box-shadow: 0 4px 14px -6px rgba(0, 0, 0, 0.65); } .brand-text { @@ -83,10 +113,7 @@ main { .brand-text em { font-style: normal; - background: linear-gradient(90deg, var(--accent), var(--youtube) 55%, var(--reddit)); - -webkit-background-clip: text; - background-clip: text; - color: transparent; + color: var(--ink-faint); } .header-meta { @@ -99,30 +126,38 @@ main { opacity: 0.4; } +/* Desk is live — neutral pulse, so it is never confused with a source. */ .pulse { width: 8px; height: 8px; border-radius: 50%; - background: var(--rt); + background: var(--ink); animation: pulse 2.2s ease-out infinite; } @keyframes pulse { - 0% { box-shadow: 0 0 0 0 rgba(0, 195, 137, 0.55); } - 70% { box-shadow: 0 0 0 9px rgba(0, 195, 137, 0); } - 100% { box-shadow: 0 0 0 0 rgba(0, 195, 137, 0); } + 0% { box-shadow: 0 0 0 0 rgba(241, 243, 247, 0.32); } + 70% { box-shadow: 0 0 0 9px rgba(241, 243, 247, 0); } + 100% { box-shadow: 0 0 0 0 rgba(241, 243, 247, 0); } } -.spectrum-rail { - height: 5px; +/* Three wires — one segment per source, page-coloured gaps between them. */ +.wire-rail { + height: 3px; background: linear-gradient(90deg, - var(--accent), var(--youtube) 32%, var(--reddit) 60%, var(--sun) 78%, var(--rt)); + var(--youtube) 0, var(--youtube) calc(33.333% - 1px), + var(--bg) calc(33.333% - 1px), var(--bg) calc(33.333% + 1px), + var(--reddit) calc(33.333% + 1px), var(--reddit) calc(66.666% - 1px), + var(--bg) calc(66.666% - 1px), var(--bg) calc(66.666% + 1px), + var(--rt) calc(66.666% + 1px)); } /* ---------- Filter pills ---------- */ .filter-pills { gap: 0.5rem; + scrollbar-width: thin; + scrollbar-color: var(--line-2) transparent; } .filter-pill { @@ -133,13 +168,13 @@ main { padding: 0.42rem 0.95rem; border-radius: 999px; border: 1px solid var(--line); - background: rgba(255, 255, 255, 0.7); + background: rgba(255, 255, 255, 0.04); color: var(--ink); font-weight: 600; font-size: 0.86rem; text-decoration: none; white-space: nowrap; - transition: transform 0.15s ease, box-shadow 0.15s ease, background 0.15s ease, color 0.15s ease; + transition: transform 0.15s ease, background 0.15s ease, border-color 0.15s ease, color 0.15s ease, box-shadow 0.15s ease; } .filter-pill .pill-dot { @@ -152,19 +187,21 @@ main { .filter-pill:hover { transform: translateY(-1px); - box-shadow: 0 8px 18px -10px var(--pill); + background: rgba(255, 255, 255, 0.07); + border-color: var(--line-2); color: var(--ink); } .filter-pill.active { background: var(--pill); border-color: transparent; - color: #fff; - box-shadow: 0 10px 22px -10px var(--pill); + color: var(--ink-deep); + box-shadow: 0 8px 20px -10px color-mix(in srgb, var(--pill) 75%, transparent); } .filter-pill.active .pill-dot { - background: rgba(255, 255, 255, 0.9); + background: currentColor; + opacity: 0.8; } .refresh-chip { @@ -173,8 +210,9 @@ main { gap: 0.4rem; padding: 0.32rem 0.7rem; border-radius: 999px; - background: rgba(106, 61, 255, 0.1); - color: var(--accent); + border: 1px solid var(--line); + background: var(--surface-2); + color: var(--ink-soft); font-size: 0.72rem; font-weight: 500; } @@ -185,12 +223,14 @@ main { font-size: 0.66rem; letter-spacing: 0.14em; text-transform: uppercase; - color: var(--ink-soft); + color: var(--ink-faint); padding-right: 0.15rem; } .range-pills { gap: 0.35rem; + scrollbar-width: thin; + scrollbar-color: var(--line-2) transparent; } .range-pill { @@ -199,26 +239,27 @@ main { padding: 0.32rem 0.75rem; border-radius: 999px; border: 1px solid var(--line); - background: rgba(255, 255, 255, 0.7); + background: rgba(255, 255, 255, 0.04); color: var(--ink-soft); font-size: 0.78rem; font-weight: 600; text-decoration: none; white-space: nowrap; - transition: transform 0.15s ease, box-shadow 0.15s ease, background 0.15s ease, color 0.15s ease, border-color 0.15s ease; + transition: transform 0.15s ease, background 0.15s ease, color 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease; } .range-pill:hover { color: var(--ink); - border-color: var(--accent); + border-color: var(--line-2); + background: rgba(255, 255, 255, 0.07); transform: translateY(-1px); } .range-pill.active { - background: var(--accent); + background: var(--ink); border-color: transparent; - color: #fff; - box-shadow: 0 10px 22px -10px var(--accent); + color: var(--ink-deep); + box-shadow: 0 8px 20px -10px rgba(241, 243, 247, 0.45); } /* ---------- Cards ---------- */ @@ -229,23 +270,25 @@ main { border: 1px solid var(--line); border-top: 4px solid var(--src); border-radius: 18px; - background: var(--surface); + background: + linear-gradient(180deg, color-mix(in srgb, var(--src) 8%, transparent), transparent 34%), + var(--surface); overflow: hidden; - box-shadow: 0 1px 2px rgba(22, 11, 46, 0.04), 0 14px 34px -26px rgba(22, 11, 46, 0.5); - transition: transform 0.18s ease, box-shadow 0.18s ease; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.03), 0 18px 34px -28px rgba(0, 0, 0, 0.9); + transition: transform 0.18s ease, box-shadow 0.18s ease, border-color 0.18s ease; } .feed-card:hover { transform: translateY(-4px); - box-shadow: 0 20px 44px -20px rgba(22, 11, 46, 0.4); - box-shadow: 0 20px 44px -20px color-mix(in srgb, var(--src) 55%, transparent); + border-color: color-mix(in srgb, var(--src) 32%, var(--line)); + box-shadow: 0 22px 44px -24px color-mix(in srgb, var(--src) 45%, transparent), 0 18px 34px -28px rgba(0, 0, 0, 0.9); } .feed-thumb-link { position: relative; display: block; overflow: hidden; - background: #efeaff; + background: color-mix(in srgb, var(--src) 16%, var(--surface)); } .feed-thumb-link::after { @@ -253,7 +296,7 @@ main { position: absolute; inset: 0; pointer-events: none; - background: linear-gradient(to top, rgba(22, 11, 46, 0.5), transparent 52%); + background: linear-gradient(to top, rgba(9, 11, 15, 0.55), transparent 52%); } .feed-thumb { @@ -275,7 +318,7 @@ main { font-weight: 600; letter-spacing: 0.09em; text-transform: uppercase; - color: #fff; + color: var(--ink-deep); background: var(--src); } @@ -286,7 +329,7 @@ main { z-index: 1; padding: 0.24rem 0.6rem; border-radius: 999px; - box-shadow: 0 6px 16px -6px rgba(22, 11, 46, 0.6); + box-shadow: 0 6px 16px -6px rgba(0, 0, 0, 0.7); } .source-chip { @@ -303,7 +346,7 @@ main { .feed-name { font-size: 0.68rem; letter-spacing: 0.02em; - color: var(--ink-soft); + color: var(--ink-faint); } .feed-card .card-title { @@ -338,7 +381,7 @@ main { } .card-footer-meta { - border-top: 1px dashed var(--line); + border-top: 1px solid var(--line); } .author { @@ -349,7 +392,7 @@ main { .timestamp { font-size: 0.72rem; font-weight: 500; - color: var(--src); + color: color-mix(in srgb, var(--src) 78%, var(--ink)); } .mono { @@ -360,21 +403,19 @@ main { .empty-state { padding: 4.5rem 1rem; - color: var(--ink-soft); + color: var(--ink-faint); } +/* Same signal-bars mark as the header, unlit — a quiet, static glyph. */ .empty-mark { - width: 58px; - height: 58px; + width: 44px; + height: 30px; margin: 0 auto 1rem; - border-radius: 20px; - background: conic-gradient(from 220deg, var(--accent), var(--youtube), var(--reddit), var(--rt), var(--accent)); - filter: saturate(1.1); - animation: spin 12s linear infinite; -} - -@keyframes spin { - to { transform: rotate(360deg); } + background: + linear-gradient(var(--youtube), var(--youtube)) 12px bottom / 4px 10px no-repeat, + linear-gradient(var(--reddit), var(--reddit)) 19px bottom / 4px 16px no-repeat, + linear-gradient(var(--rt), var(--rt)) 26px bottom / 4px 22px no-repeat; + opacity: 0.5; } .empty-title { @@ -383,47 +424,48 @@ main { color: var(--ink); } -/* ---------- Button ---------- */ +/* ---------- Primary action ---------- */ -.btn-energy { +/* The tally light: the brightest thing on the page, so there is never a + question about what the primary action is. */ +.btn-tally { border: 0; border-radius: 999px; padding: 0.7rem 1.7rem; font-family: var(--display); font-weight: 600; - color: #fff; - background: linear-gradient(100deg, var(--accent), var(--youtube) 48%, var(--reddit) 78%, var(--rt)); - background-size: 160% 100%; - box-shadow: 0 14px 30px -14px rgba(106, 61, 255, 0.8); - transition: transform 0.15s ease, background-position 0.4s ease, box-shadow 0.15s ease; + color: var(--ink-deep); + background: var(--ink); + box-shadow: 0 12px 28px -14px rgba(241, 243, 247, 0.4); + transition: transform 0.15s ease, background 0.15s ease, box-shadow 0.15s ease; } -.btn-energy:hover, -.btn-energy:focus { - color: #fff; +.btn-tally:hover, +.btn-tally:focus { + color: var(--ink-deep); transform: translateY(-2px); - background-position: 100% 0; - box-shadow: 0 18px 36px -14px rgba(255, 31, 75, 0.7); + background: #ffffff; + box-shadow: 0 18px 34px -14px rgba(241, 243, 247, 0.5); } -.btn-energy:disabled { - opacity: 0.65; +.btn-tally:disabled { + opacity: 0.55; transform: none; - color: #fff; + color: var(--ink-deep); } .alert-soft { - border: 1px solid #ffe1a8; + border: 1px solid color-mix(in srgb, var(--youtube) 40%, transparent); border-radius: 14px; - background: #fff8ea; - color: #7a5a12; + background: color-mix(in srgb, var(--youtube) 12%, var(--surface)); + color: #ffc2ce; } .alert-ok-soft { - border: 1px solid #b7ecd2; + border: 1px solid color-mix(in srgb, var(--rt) 40%, transparent); border-radius: 14px; - background: #ecfbf3; - color: #14624a; + background: color-mix(in srgb, var(--rt) 12%, var(--surface)); + color: #9beed2; } /* ---------- Manage page ---------- */ @@ -434,27 +476,27 @@ main { padding: 0.38rem 0.85rem; border-radius: 999px; border: 1px solid var(--line); - background: rgba(255, 255, 255, 0.7); + background: rgba(255, 255, 255, 0.04); color: var(--ink-soft); font-size: 0.78rem; font-weight: 600; text-decoration: none; white-space: nowrap; - transition: color 0.15s ease, border-color 0.15s ease, transform 0.15s ease, box-shadow 0.15s ease; + transition: color 0.15s ease, border-color 0.15s ease, transform 0.15s ease, background 0.15s ease; } .manage-link:hover { - color: var(--accent); - border-color: var(--accent); + color: var(--ink); + border-color: var(--line-2); + background: rgba(255, 255, 255, 0.07); transform: translateY(-1px); - box-shadow: 0 8px 18px -12px var(--accent); } .manage-title { font-size: 0.68rem; letter-spacing: 0.14em; text-transform: uppercase; - color: var(--ink-soft); + color: var(--ink-faint); } .manage-main { @@ -466,7 +508,7 @@ main { border: 1px solid var(--line); border-radius: 18px; padding: 1.25rem 1.4rem; - box-shadow: 0 1px 2px rgba(22, 11, 46, 0.04), 0 14px 34px -26px rgba(22, 11, 46, 0.5); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.03), 0 18px 34px -28px rgba(0, 0, 0, 0.9); } .manage-notice { @@ -523,7 +565,7 @@ main { font-size: 0.68rem; font-weight: 600; color: var(--ink-soft); - background: rgba(22, 11, 46, 0.06); + background: rgba(255, 255, 255, 0.07); padding: 0.12rem 0.5rem; border-radius: 999px; } @@ -549,7 +591,7 @@ main { } .feed-row:hover { - box-shadow: 0 12px 26px -18px rgba(22, 11, 46, 0.55); + box-shadow: 0 12px 26px -18px rgba(0, 0, 0, 0.85); } .feed-row.is-off { @@ -576,22 +618,21 @@ main { letter-spacing: 0.09em; text-transform: uppercase; color: var(--ink-soft); - background: rgba(22, 11, 46, 0.06); + background: rgba(255, 255, 255, 0.07); padding: 0.16rem 0.5rem; border-radius: 999px; } .feed-row-url { font-size: 0.66rem; - color: var(--ink-soft); + color: var(--ink-faint); text-decoration: none; max-width: 36ch; - opacity: 0.85; } .feed-row-url:hover { - color: var(--accent); - opacity: 1; + color: var(--ink); + text-decoration: underline; } .health { @@ -619,8 +660,7 @@ main { } .health-idle { - color: var(--ink-soft); - opacity: 0.75; + color: var(--ink-faint); } .feed-row-actions { @@ -631,49 +671,76 @@ main { } .btn-mini { - border: 1px solid var(--line); + border: 1px solid var(--line-2); border-radius: 999px; - background: rgba(255, 255, 255, 0.75); + background: rgba(255, 255, 255, 0.04); color: var(--ink-soft); font-size: 0.72rem; font-weight: 600; padding: 0.3rem 0.85rem; - transition: color 0.15s ease, border-color 0.15s ease, background 0.15s ease, transform 0.15s ease; + transition: color 0.15s ease, border-color 0.15s ease, background 0.15s ease; } .btn-mini:hover, .btn-mini:focus { color: var(--ink); - border-color: var(--ink-soft); + border-color: var(--ink-faint); + background: rgba(255, 255, 255, 0.08); } .btn-mini-on { - color: var(--accent); - border-color: rgba(106, 61, 255, 0.45); - background: rgba(106, 61, 255, 0.08); + color: var(--ink); + border-color: var(--ink-faint); + background: rgba(255, 255, 255, 0.1); } .btn-mini-on:hover, .btn-mini-on:focus { - color: var(--accent); - border-color: var(--accent); - background: rgba(106, 61, 255, 0.14); + color: var(--ink); + border-color: var(--ink-soft); + background: rgba(255, 255, 255, 0.14); } .btn-mini-danger:hover, .btn-mini-danger:focus { color: var(--youtube); - border-color: rgba(255, 31, 75, 0.5); - background: rgba(255, 31, 75, 0.07); + border-color: color-mix(in srgb, var(--youtube) 55%, transparent); + background: color-mix(in srgb, var(--youtube) 12%, transparent); +} + +/* ---------- Form controls ---------- */ + +.form-control { + background: var(--surface-2); + border: 1px solid var(--line-2); + color: var(--ink); +} + +.form-control::placeholder { + color: var(--ink-faint); +} + +.form-control:focus { + background: var(--surface-2); + border-color: var(--ink-soft); + color: var(--ink); + box-shadow: 0 0 0 3px rgba(241, 243, 247, 0.12); +} + +code { + color: var(--ink); + background: rgba(255, 255, 255, 0.06); + padding: 0.1rem 0.35rem; + border-radius: 6px; } /* ---------- Focus & motion ---------- */ a:focus-visible, -button:focus-visible { - outline: 3px solid rgba(106, 61, 255, 0.55); +button:focus-visible, +input:focus-visible { + outline: 2px solid var(--ink); outline-offset: 2px; - border-radius: 8px; } @media (prefers-reduced-motion: reduce) { diff --git a/src/View.php b/src/View.php index fcdb625..4a3fce4 100644 --- a/src/View.php +++ b/src/View.php @@ -34,10 +34,10 @@ final class View public static function sourceColor(string $source): string { return match ($source) { - 'youtube' => '#ff1f4b', - 'reddit' => '#ff6a00', - 'rt' => '#00c389', - default => '#6a3dff', + 'youtube' => '#ff3b5c', + 'reddit' => '#ff7a2f', + 'rt' => '#1fcb93', + default => '#f1f3f7', }; } diff --git a/templates/layout.php b/templates/layout.php index 073a51c..a9c8eb6 100644 --- a/templates/layout.php +++ b/templates/layout.php @@ -39,7 +39,7 @@ $errors = $refresh['errors'] ?? []; $total = (int) $page['total']; ?> - + @@ -69,7 +69,7 @@ $total = (int) $page['total']; Manage - +
@@ -151,7 +151,7 @@ $total = (int) $page['total'];
diff --git a/templates/manage.php b/templates/manage.php index 916428f..ba29913 100644 --- a/templates/manage.php +++ b/templates/manage.php @@ -19,7 +19,7 @@ foreach ($feeds as $feed) { $actionUrl = 'manage.php'; ?> - + @@ -44,7 +44,7 @@ $actionUrl = 'manage.php'; ← Dashboard
- +
@@ -70,7 +70,7 @@ $actionUrl = 'manage.php'; - + @@ -91,7 +91,7 @@ $actionUrl = 'manage.php'; - + @@ -104,7 +104,7 @@ $actionUrl = 'manage.php'; - +