Commit 9a2498a9 by PLN (Algolia)

refactor(styles): extract token layer, kill the global neon leak

Phase 1 Step 4. Move the palette out of globals.css :root into a new
styles/tokens.css: a neutral/professional default on :root (landing +
content) and the ParVagues neon/dark palette scoped to
[data-theme="parvagues"], set on the app/parvagues/layout.tsx wrapper.
The ~90 var(--…) consumers resolve against that ancestor scope, so
theming is one attribute — no per-component edits.

- globals.css: drop the :root palette + hardcoded dark html/body bg;
  body now reads var(--surface)/var(--text-primary).
- parvagues.module.css: remove the dead duplicate .colorRoot/.colorContainer.
- main.css: replace the body:not(:has(.parvagues-root)) element-style hack
  with a positive .site-shell scope on the content Layout container; links
  use the new --link token.
- delete dead styles/dunbar.module.css.

Verified: prod build green (14 routes, ParVagues ISR intact); fresh-dev
smoke all 200; data-theme scoping confirmed (landing neutral, parvagues neon).
Co-Authored-By: 's avatarClaude Opus 4.8 (1M context) <noreply@anthropic.com>
parent 3dfa3a52
import '@/styles/tokens.css';
import '@/styles/globals.css'; import '@/styles/globals.css';
import '@/styles/main.css'; import '@/styles/main.css';
import '@/styles/masonry.css'; import '@/styles/masonry.css';
......
...@@ -31,6 +31,7 @@ export const metadata = { ...@@ -31,6 +31,7 @@ export const metadata = {
export default function ParvaguesLayout({ children }: { children: React.ReactNode }) { export default function ParvaguesLayout({ children }: { children: React.ReactNode }) {
return ( return (
<div <div
data-theme="parvagues"
className={`parvagues-root ${syne.variable} noise-overlay min-h-screen bg-[var(--surface)] text-[var(--text-primary)] selection:bg-[var(--neon-high)]/30 selection:text-white`} className={`parvagues-root ${syne.variable} noise-overlay min-h-screen bg-[var(--surface)] text-[var(--text-primary)] selection:bg-[var(--neon-high)]/30 selection:text-white`}
> >
<RevealController /> <RevealController />
......
...@@ -15,7 +15,7 @@ export const description = "PLN's Selected Works"; ...@@ -15,7 +15,7 @@ export const description = "PLN's Selected Works";
export default function Layout({ children, home }) { export default function Layout({ children, home }) {
const router = useRouter(); const router = useRouter();
return ( return (
<div className={styles.container}> <div className={`site-shell ${styles.container}`}>
<header className={styles.header}> <header className={styles.header}>
{home ? ( {home ? (
<> <>
......
/* Dunbar MVP styles (scoped via CSS Modules) */
.container {
padding: 16px;
max-width: 1400px;
margin: 0 auto;
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 12px;
}
.title {
font-size: 1.5rem;
font-weight: 700;
}
.row {
display: flex;
align-items: center;
gap: 8px;
}
.spacer {
flex: 1;
}
.tabs {
display: flex;
gap: 8px;
margin: 8px 0 16px;
flex-wrap: wrap;
}
.tabBtn {
padding: 8px 12px;
border: 1px solid #ddd;
background: #fafafa;
border-radius: 8px;
cursor: pointer;
transition: background 120ms ease, border-color 120ms ease;
}
.tabBtn:hover {
background: #f0f0f0;
}
.tabActive {
background: #e7f5ec;
border-color: #5a9960;
}
.toolbar {
display: flex;
gap: 8px;
align-items: center;
margin-bottom: 12px;
flex-wrap: wrap;
}
.input, .textarea, .select {
border: 1px solid #ddd;
border-radius: 8px;
padding: 8px 10px;
font-size: 0.95rem;
background: #fff;
}
.textarea {
min-height: 80px;
resize: vertical;
}
.btn {
padding: 8px 12px;
border: 1px solid #222;
background: #222;
color: #fff;
border-radius: 8px;
cursor: pointer;
transition: background 120ms ease, opacity 120ms ease;
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.btnSecondary {
padding: 8px 12px;
border: 1px solid #ddd;
background: #fff;
color: #333;
border-radius: 8px;
cursor: pointer;
}
.list {
border: 1px solid #eee;
border-radius: 10px;
overflow: hidden;
}
.listScroll {
max-height: 60vh;
overflow: auto;
}
.listItem {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 12px;
border-bottom: 1px solid #f2f2f2;
cursor: pointer;
background: #fff;
transition: background 120ms ease;
}
.listItem:hover {
background: #f9f9f9;
}
.itemTitle {
font-weight: 600;
}
.itemMeta {
color: #666;
font-size: 0.9rem;
}
.itemRight {
margin-left: auto;
color: #aaa;
}
.twoCol {
display: grid;
grid-template-columns: 1fr 1.8fr;
gap: 16px;
}
@media (max-width: 900px) {
.twoCol {
grid-template-columns: 1fr;
}
}
.card {
border: 1px solid #eee;
background: #fff;
border-radius: 10px;
padding: 12px;
}
.cardHeader {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
font-weight: 600;
}
.scroll {
max-height: 60vh;
overflow: auto;
}
.switchRow {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 6px;
border-bottom: 1px solid #f5f5f5;
}
.switchRow:hover {
background: #fafafa;
}
.switch {
width: 42px;
height: 24px;
background: #ddd;
border-radius: 999px;
position: relative;
transition: background 120ms ease;
}
.switchOn {
background: #5a9960;
}
.knob {
position: absolute;
top: 3px;
left: 3px;
width: 18px;
height: 18px;
background: #fff;
border-radius: 50%;
transition: left 120ms ease;
box-shadow: 0 1px 2px rgba(0,0,0,0.15);
}
.knobOn {
left: 21px;
}
.timeline {
display: flex;
flex-direction: column;
gap: 10px;
}
.timelineGroup {
margin: 8px 0;
}
.timelineDate {
font-weight: 700;
margin-bottom: 6px;
}
.timelineEvent {
background: #fbfbfb;
border: 1px solid #f0f0f0;
border-radius: 8px;
padding: 8px 10px;
}
.tagRow {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 6px;
}
.tagChip {
display: inline-flex;
align-items: center;
padding: 2px 8px;
border-radius: 999px;
background: #eef7f0;
border: 1px solid #d6e8da;
color: #2c5530;
font-size: 0.8rem;
line-height: 1.4;
gap: 6px;
}
.tagClose {
appearance: none;
border: none;
background: transparent;
color: #2c5530;
font-weight: 800;
cursor: pointer;
padding: 0;
line-height: 1;
}
.badge {
display: inline-block;
padding: 2px 6px;
border-radius: 999px;
background: #eef7f0;
color: #2c5530;
font-size: 0.8rem;
border: 1px solid #d6e8da;
}
.tooltip {
position: fixed;
background: #fff;
border: 1px solid #eee;
border-radius: 8px;
padding: 8px 10px;
box-shadow: 0 8px 24px rgba(0,0,0,0.08);
pointer-events: none;
z-index: 1000;
max-width: 280px;
font-size: 0.9rem;
}
.graphToolbar {
display: flex;
gap: 8px;
align-items: center;
margin-bottom: 8px;
flex-wrap: wrap;
}
.banner {
padding: 8px 10px;
background: #fffbea;
border: 1px solid #fde68a;
color: #7c5e10;
border-radius: 8px;
}
.statsGrid {
display: grid;
grid-template-columns: repeat(4, minmax(140px, 1fr));
gap: 12px;
}
@media (max-width: 700px) {
.statsGrid {
grid-template-columns: repeat(2, minmax(140px, 1fr));
}
}
.statCard {
border: 1px solid #eee;
background: #fff;
border-radius: 10px;
padding: 12px;
}
.statLabel {
color: #666;
font-size: 0.9rem;
}
.statValue {
font-size: 1.6rem;
font-weight: 800;
}
/* Orbits */
.orbitsWrap {
width: 100%;
height: 70vh;
border: 1px solid #eee;
border-radius: 10px;
overflow: hidden;
background: radial-gradient(circle at center, #ffffff 0%, #f7fbf8 100%);
}
.orbitLabel {
fill: #2c5530;
font-size: 12px;
font-weight: 700;
}
.nodeLabel {
fill: #333;
font-weight: 700;
paint-order: stroke;
stroke: #fff;
stroke-width: 3px;
stroke-linejoin: round;
}
/* Network */
.canvasWrap {
width: 100%;
height: 70vh;
border: 1px solid #eee;
border-radius: 10px;
overflow: hidden;
background: #fff;
position: relative;
}
.chevron {
font-size: 14px;
color: #999;
}
/* Password screen */
.lockWrap {
display: flex;
align-items: center;
justify-content: center;
min-height: 50vh;
flex-direction: column;
gap: 10px;
text-align: center;
}
/* Floating controls for Network */
.floatingControls {
position: absolute;
right: 10px;
bottom: 10px;
display: grid;
grid-template-columns: repeat(3, 40px);
gap: 6px;
background: rgba(255,255,255,0.9);
border: 1px solid #eee;
border-radius: 10px;
padding: 8px;
box-shadow: 0 8px 24px rgba(0,0,0,0.08);
}
.ctrlBtn {
width: 40px;
height: 40px;
border: 1px solid #ddd;
border-radius: 8px;
background: #fff;
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
font-weight: 700;
color: #333;
transition: transform 80ms ease, background 120ms ease;
}
.ctrlBtn:active {
transform: scale(0.96);
background: #f6f6f6;
}
.ctrlWide {
grid-column: span 3;
height: 36px;
}
@import "tailwindcss"; @import "tailwindcss";
/* ParVagues palette */ /* Palette lives in tokens.css (neutral :root + scoped [data-theme]). */
:root {
--neon-down: #8900b3;
--neon-low: #a700d1;
--neon-high: #d900ff;
--coral: #ff3d7b;
--biomod: #5bc091;
--cigarette: #ff8c00;
--surface: #0a0a0a;
--surface-raised: #111111;
--text-primary: #e5e5e5;
--text-muted: #737373;
}
html, body { html, body {
background-color: #0a0a0a; background-color: var(--surface);
color: #e5e5e5; color: var(--text-primary);
color-scheme: dark;
} }
html { html {
......
...@@ -12,26 +12,27 @@ body { ...@@ -12,26 +12,27 @@ body {
box-sizing: border-box; box-sizing: border-box;
} }
/* Scope legacy element styles to non-parvagues pages */ /* Legacy element defaults, scoped to the content/landing shell (components/layout.js).
body:not(:has(.parvagues-root)) a { ParVagues styles itself with Tailwind and opts out by not carrying .site-shell. */
color: #0070f3; .site-shell a {
color: var(--link);
text-decoration: none; text-decoration: none;
} }
body:not(:has(.parvagues-root)) a:hover { .site-shell a:hover {
text-decoration: underline; text-decoration: underline;
} }
body:not(:has(.parvagues-root)) img { .site-shell img {
max-width: 100%; max-width: 100%;
display: block; display: block;
} }
body:not(:has(.parvagues-root)) header { .site-shell header {
margin-bottom: 2em; margin-bottom: 2em;
} }
body:not(:has(.parvagues-root)) main h2 { .site-shell main h2 {
margin-bottom: 1em; margin-bottom: 1em;
} }
...@@ -47,7 +48,7 @@ p.code { ...@@ -47,7 +48,7 @@ p.code {
font-family: monospace; font-family: monospace;
} }
body:not(:has(.parvagues-root)) section { .site-shell section {
margin-bottom: 3em; margin-bottom: 3em;
} }
...@@ -55,7 +56,7 @@ small { ...@@ -55,7 +56,7 @@ small {
font-size: 1rem; font-size: 1rem;
} }
body:not(:has(.parvagues-root)) footer { .site-shell footer {
margin-top: 4em; margin-top: 4em;
text-align: center; text-align: center;
color: #228; color: #228;
......
/* ParVagues specific styles */ /* ParVagues specific styles */
/* New Color Palette */ /* Palette comes from tokens.css via [data-theme="parvagues"] on the layout wrapper. */
/* Fixing CSS Modules compatibility - defining colors in a local class */
.colorRoot {
--neon-down: #8900b3;
--neon-low: #a700d1;
--neon-high: #d900ff;
--coral: #ff3d7b;
--biomod: #5bc091;
--cigarette: #ff8c00;
}
/* This class can be added to the main container to provide color variables to all children */
.colorContainer {
composes: colorRoot;
}
.heroSection { .heroSection {
min-height: 100vh; min-height: 100vh;
......
/* Design tokens — the one place section themes diverge.
*
* The default (`:root`) is the neutral / professional palette for the landing
* and content pages. Each section that wants a different look sets
* `data-theme="<name>"` on its layout wrapper; the ~90 `var(--…)` consumers
* underneath resolve against that ancestor scope, so theming is a single
* attribute — no per-component edits.
*
* Token names are shared across themes (--surface, --text-primary, …) so the
* same component reads the right value in any theme. Theme-specific accents
* (--neon-*, --coral) only exist where a theme defines them.
*/
:root {
/* Neutral / professional default (landing + content) */
--surface: #ffffff;
--surface-raised: #f5f5f5;
--text-primary: #1a1a1a;
--text-muted: #555555;
--link: #0070f3;
--link-hover: #0051a8;
--border: #e0e0e0;
color-scheme: light;
}
/* ParVagues — neon / dark. Set on app/parvagues/layout.tsx wrapper. */
[data-theme="parvagues"] {
--neon-down: #8900b3;
--neon-low: #a700d1;
--neon-high: #d900ff;
--coral: #ff3d7b;
--biomod: #5bc091;
--cigarette: #ff8c00;
--surface: #0a0a0a;
--surface-raised: #111111;
--text-primary: #e5e5e5;
--text-muted: #737373;
color-scheme: dark;
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment