` element (or equivalent role) instead of a placeholder anchor.
+
+If an anchor is used for navigation, it MUST include a valid `href` in the initial HTML (not only populated later by client-side JavaScript).
+
+#### Scenario: Modal CTAs are crawlable
+- **WHEN** the homepage is loaded and the media modal markup exists in the DOM
+- **THEN** any CTA anchors within the modal have valid `href` values, or are not rendered as anchors until an `href` is available
diff --git a/openspec/changes/lighthouse-remediation/specs/wcag-responsive-ui/spec.md b/openspec/changes/lighthouse-remediation/specs/wcag-responsive-ui/spec.md
new file mode 100644
index 0000000..bfb984d
--- /dev/null
+++ b/openspec/changes/lighthouse-remediation/specs/wcag-responsive-ui/spec.md
@@ -0,0 +1,18 @@
+## ADDED Requirements
+
+### Requirement: Color contrast for secondary UI text and chips
+The site MUST meet WCAG 2.2 AA contrast requirements for non-decorative text, including secondary/muted metadata text and pill/chip labels.
+
+This includes (but is not limited to):
+- card footer date and view-count text
+- pill/chip labels (e.g., source labels)
+
+The contrast ratio MUST be at least 4.5:1 for normal text.
+
+#### Scenario: Card metadata contrast passes
+- **WHEN** a content card is rendered with date and view-count metadata
+- **THEN** the metadata text has a contrast ratio of at least 4.5:1 against its background
+
+#### Scenario: Pill label contrast passes
+- **WHEN** a pill/chip label is rendered (e.g., a source label)
+- **THEN** the pill label text has a contrast ratio of at least 4.5:1 against the pill background
diff --git a/openspec/changes/lighthouse-remediation/tasks.md b/openspec/changes/lighthouse-remediation/tasks.md
new file mode 100644
index 0000000..d5a30ca
--- /dev/null
+++ b/openspec/changes/lighthouse-remediation/tasks.md
@@ -0,0 +1,39 @@
+## 1. Baseline And Repro
+
+- [ ] 1.1 Run Lighthouse from a clean Chrome profile (no extensions) for both Mobile and Desktop and save reports (JSON)
+- [ ] 1.2 Record current failing audits and their affected selectors/URLs (from the Lighthouse "details" tables)
+
+## 2. Accessibility Contrast
+
+- [ ] 2.1 Adjust global CSS tokens/styles so `.muted` card metadata meets 4.5:1 contrast on cards
+- [ ] 2.2 Adjust pill/chip background + text colors to meet 4.5:1 contrast (e.g., `.pill` and source variants)
+- [ ] 2.3 Re-run Lighthouse accessibility category and confirm `color-contrast` passes
+
+## 3. SEO Hygiene (robots + crawlable links)
+
+- [ ] 3.1 Update `site/public/robots.txt` to use an absolute sitemap URL (e.g., `Sitemap: https://santhoshj.com/sitemap-index.xml`)
+- [ ] 3.2 Fix non-crawlable anchors in the media modal by ensuring anchors always have `href` in initial HTML or switching to buttons until navigable
+- [ ] 3.3 Re-run Lighthouse SEO category and confirm `robots-txt` and `crawlable-anchors` pass
+
+## 4. CSP / Best Practices
+
+- [ ] 4.1 Identify the exact CSP-related DevTools Issue message (Chrome DevTools → Issues) and capture the text
+- [ ] 4.2 Implement a CSP baseline at the reverse proxy/origin that allows required resources (self + Umami + image/frame origins) and avoids permissive wildcards
+- [ ] 4.3 Reduce inline scripts that force `unsafe-inline` (move registration / modal scripts to external files or use nonce/hash approach)
+- [ ] 4.4 Re-run Lighthouse Best Practices and confirm `inspector-issues` passes
+
+## 5. Performance: Fonts, CSS/JS, And Images
+
+- [ ] 5.1 Remove render-blocking third-party font stylesheet by self-hosting Manrope and loading via `@font-face`
+- [ ] 5.2 Ensure production CSS is minified (move global CSS into the build pipeline or add a build minification step)
+- [ ] 5.3 Reduce unused CSS on the homepage (prune unused selectors or split critical vs non-critical styles)
+- [ ] 5.4 Reduce unused JS on the homepage (remove unnecessary scripts; ensure analytics is async/defer; avoid extra inline code)
+- [ ] 5.5 Improve thumbnail image delivery (use responsive `srcset` / resized sources where feasible; avoid oversized podcast covers)
+- [ ] 5.6 Improve cache lifetimes for first-party static assets (fingerprint + immutable cache for build assets; revalidate non-fingerprinted)
+- [ ] 5.7 Re-run Lighthouse Performance (mobile + desktop) and confirm 100 score
+
+## 6. Verification
+
+- [ ] 6.1 Run `npm run build` and ensure build succeeds
+- [ ] 6.2 Smoke test site locally (`npm run preview`) including modal, analytics script load, and service worker registration
+- [ ] 6.3 Deploy and confirm production Lighthouse scores are 100/100/100/100
diff --git a/openspec/specs/site-theming/spec.md b/openspec/specs/site-theming/spec.md
new file mode 100644
index 0000000..6f7c162
--- /dev/null
+++ b/openspec/specs/site-theming/spec.md
@@ -0,0 +1,59 @@
+## Purpose
+
+Define site-wide theme support (dark, light, high-contrast) using CSS tokens and an application mechanism that can switch across the entire UI.
+
+## Requirements
+
+### Requirement: Site themes
+The site MUST support three themes:
+- `dark`
+- `light`
+- `high-contrast`
+
+Themes MUST be applied by setting a `data-theme` attribute on the root document element (``).
+
+#### Scenario: Dark theme active
+- **WHEN** `data-theme="dark"` is set on ``
+- **THEN** the site's background, text, and component styling reflect the dark palette
+
+#### Scenario: Light theme active
+- **WHEN** `data-theme="light"` is set on ``
+- **THEN** the site's background, text, and component styling reflect the light palette
+
+#### Scenario: High contrast theme active
+- **WHEN** `data-theme="high-contrast"` is set on ``
+- **THEN** the site uses a high-contrast palette with a clearly visible focus ring and high-contrast borders
+
+### Requirement: Theme persistence
+The site MUST persist the user's theme selection so it is retained across page loads and navigations.
+
+Persistence MUST be stored locally in the browser (e.g., localStorage).
+
+#### Scenario: Theme persists across reload
+- **WHEN** the user selects `light` theme and reloads the page
+- **THEN** the `light` theme remains active
+
+### Requirement: Default theme selection
+If the user has not explicitly selected a theme, the site MUST choose a default theme using environment signals.
+
+Default selection order:
+1) If forced colors / high-contrast mode is active, default to `high-contrast`
+2) Else if the system prefers light color scheme, default to `light`
+3) Else default to `dark`
+
+#### Scenario: No stored preference uses system settings
+- **WHEN** the user has no stored theme preference
+- **THEN** the site selects a default theme based on forced-colors and prefers-color-scheme
+
+### Requirement: Theme switching transition
+Theme changes initiated by the user MUST transition smoothly.
+
+The transition MUST be disabled or substantially reduced when `prefers-reduced-motion: reduce` is set.
+
+#### Scenario: Smooth transition on switch
+- **WHEN** the user switches from `dark` to `light` theme
+- **THEN** theme-affecting properties transition smoothly instead of abruptly switching
+
+#### Scenario: Reduced motion disables theme animation
+- **WHEN** `prefers-reduced-motion: reduce` is set and the user switches theme
+- **THEN** the theme change occurs without noticeable animation
diff --git a/openspec/specs/theme-switcher-notch/spec.md b/openspec/specs/theme-switcher-notch/spec.md
new file mode 100644
index 0000000..9237902
--- /dev/null
+++ b/openspec/specs/theme-switcher-notch/spec.md
@@ -0,0 +1,46 @@
+## Purpose
+
+Define the requirements for a floating, accessible theme switcher notch anchored to the right side of the viewport.
+
+## Requirements
+
+### Requirement: Floating theme switcher notch
+The site MUST provide a floating theme switcher control anchored to the right side of the viewport.
+
+The control MUST be positioned below the primary navigation bar and MUST leave sufficient vertical space for secondary navigation.
+
+#### Scenario: Notch positioned below header
+- **WHEN** the page loads
+- **THEN** the theme switcher notch is visible on the right side and does not overlap the sticky header or sub-navigation
+
+### Requirement: Notch interaction and animation
+The notch MUST provide a hover affordance (a small, tasteful animation) that indicates it is interactive.
+
+The hover animation MUST be disabled or substantially reduced under `prefers-reduced-motion: reduce`.
+
+#### Scenario: Hover animation present
+- **WHEN** a pointer user hovers the notch
+- **THEN** the notch animates in a way that suggests it can be expanded or interacted with
+
+#### Scenario: Reduced motion disables hover animation
+- **WHEN** `prefers-reduced-motion: reduce` is set
+- **THEN** hovering the notch does not trigger a noticeable animation
+
+### Requirement: Theme selection UI
+The notch MUST expose the three theme options (`dark`, `light`, `high-contrast`) and allow the user to select one.
+
+The control MUST be keyboard accessible:
+- it MUST be reachable via `Tab`
+- it MUST have a visible focus indicator
+- selection MUST be possible using keyboard input
+
+#### Scenario: Keyboard selects theme
+- **WHEN** a keyboard user focuses the notch and selects `high-contrast`
+- **THEN** the site updates to the `high-contrast` theme and the selection is persisted
+
+### Requirement: Accessibility labels
+The notch and theme options MUST have accessible labels.
+
+#### Scenario: Screen reader announces theme switcher
+- **WHEN** a screen reader user focuses the theme switcher control
+- **THEN** it announces an appropriate label (e.g., "Theme" or "Theme switcher") and the currently selected theme
diff --git a/openspec/specs/wcag-responsive-ui/spec.md b/openspec/specs/wcag-responsive-ui/spec.md
index ecf8b93..47eee87 100644
--- a/openspec/specs/wcag-responsive-ui/spec.md
+++ b/openspec/specs/wcag-responsive-ui/spec.md
@@ -69,3 +69,31 @@ The site MUST ensure text remains readable:
- **WHEN** a user navigates between pages
- **THEN** typography (font family and basic scale) remains consistent
+### Requirement: Theme switching accessibility
+Theme switching controls MUST be accessible and usable with keyboard and assistive technology.
+
+The theme switcher control MUST:
+- be reachable via keyboard navigation
+- provide a visible focus indication
+- expose an accessible name/label
+- allow selecting any supported theme without requiring a pointer
+
+#### Scenario: Theme switcher is keyboard reachable
+- **WHEN** a keyboard user tabs through the page
+- **THEN** the theme switcher notch receives focus and shows a visible focus indicator
+
+#### Scenario: Theme switcher is labeled
+- **WHEN** a screen reader user focuses the theme switcher
+- **THEN** it announces a meaningful label and the current theme state
+
+### Requirement: High contrast theme meets WCAG intent
+The `high-contrast` theme MUST provide materially higher contrast than the default theme.
+
+The theme MUST keep text readable and interactive affordances obvious, including:
+- strong foreground/background contrast
+- clearly visible focus ring
+- strong borders on interactive elements
+
+#### Scenario: High contrast theme improves readability
+- **WHEN** the user enables `high-contrast` theme
+- **THEN** primary text and secondary UI labels remain clearly readable and interactive elements are visually distinct
diff --git a/site/public/styles/global.css b/site/public/styles/global.css
index bb1445c..b444022 100644
--- a/site/public/styles/global.css
+++ b/site/public/styles/global.css
@@ -9,6 +9,85 @@
--accent: #ffcd4a;
--accent2: #5ee4ff;
--focus: rgba(94, 228, 255, 0.95);
+
+ --stroke-weak: rgba(255, 255, 255, 0.08);
+ --stroke-mid: rgba(255, 255, 255, 0.12);
+ --stroke-strong: rgba(255, 255, 255, 0.18);
+
+ --layer-1: rgba(255, 255, 255, 0.04);
+ --layer-2: rgba(255, 255, 255, 0.06);
+ --layer-3: rgba(255, 255, 255, 0.08);
+
+ --surface-0: rgba(10, 14, 28, 0.7);
+ --surface-1: rgba(10, 14, 28, 0.92);
+
+ --glow-a: rgba(94, 228, 255, 0.22);
+ --glow-b: rgba(255, 205, 74, 0.18);
+ --glow-c: rgba(140, 88, 255, 0.14);
+
+ --theme-notch-top: 84px;
+}
+
+html {
+ color-scheme: dark;
+}
+
+html[data-theme="light"] {
+ color-scheme: light;
+ --bg0: #f7f9fc;
+ --bg1: #e9eef6;
+ --fg: #0b1224;
+ --muted: rgba(11, 18, 36, 0.7);
+ --card: rgba(0, 0, 0, 0.03);
+ --card2: rgba(0, 0, 0, 0.05);
+ --stroke: rgba(0, 0, 0, 0.14);
+ --accent: #b45309;
+ --accent2: #0ea5b7;
+ --focus: rgba(14, 165, 183, 0.85);
+
+ --stroke-weak: rgba(0, 0, 0, 0.08);
+ --stroke-mid: rgba(0, 0, 0, 0.12);
+ --stroke-strong: rgba(0, 0, 0, 0.18);
+
+ --layer-1: rgba(0, 0, 0, 0.03);
+ --layer-2: rgba(0, 0, 0, 0.045);
+ --layer-3: rgba(0, 0, 0, 0.06);
+
+ --surface-0: rgba(255, 255, 255, 0.78);
+ --surface-1: rgba(255, 255, 255, 0.92);
+
+ --glow-a: rgba(14, 165, 183, 0.18);
+ --glow-b: rgba(180, 83, 9, 0.16);
+ --glow-c: rgba(37, 99, 235, 0.12);
+}
+
+html[data-theme="high-contrast"] {
+ color-scheme: dark;
+ --bg0: #000000;
+ --bg1: #000000;
+ --fg: #ffffff;
+ --muted: rgba(255, 255, 255, 0.92);
+ --card: rgba(0, 0, 0, 0.85);
+ --card2: rgba(0, 0, 0, 0.92);
+ --stroke: rgba(255, 255, 255, 0.85);
+ --accent: #ffcd4a;
+ --accent2: #5ee4ff;
+ --focus: rgba(255, 255, 255, 0.95);
+
+ --stroke-weak: rgba(255, 255, 255, 0.55);
+ --stroke-mid: rgba(255, 255, 255, 0.75);
+ --stroke-strong: rgba(255, 255, 255, 0.9);
+
+ --layer-1: rgba(255, 255, 255, 0.08);
+ --layer-2: rgba(255, 255, 255, 0.12);
+ --layer-3: rgba(255, 255, 255, 0.16);
+
+ --surface-0: rgba(0, 0, 0, 0.9);
+ --surface-1: rgba(0, 0, 0, 0.96);
+
+ --glow-a: transparent;
+ --glow-b: transparent;
+ --glow-c: transparent;
}
* {
@@ -48,9 +127,9 @@ body::before {
z-index: -1;
pointer-events: none;
background:
- radial-gradient(1200px 800px at 10% 10%, rgba(94, 228, 255, 0.22), transparent 60%),
- radial-gradient(1100px 800px at 90% 20%, rgba(255, 205, 74, 0.18), transparent 58%),
- radial-gradient(1200px 900px at 30% 90%, rgba(140, 88, 255, 0.14), transparent 62%);
+ radial-gradient(1200px 800px at 10% 10%, var(--glow-a), transparent 60%),
+ radial-gradient(1100px 800px at 90% 20%, var(--glow-b), transparent 58%),
+ radial-gradient(1200px 900px at 30% 90%, var(--glow-c), transparent 62%);
}
a {
@@ -75,8 +154,8 @@ textarea:focus-visible {
z-index: 999;
padding: 10px 12px;
border-radius: 999px;
- border: 1px solid rgba(255, 255, 255, 0.18);
- background: rgba(10, 14, 28, 0.92);
+ border: 1px solid var(--stroke-strong);
+ background: var(--surface-1);
color: var(--fg);
font-weight: 800;
transform: translateY(-220%);
@@ -98,8 +177,8 @@ textarea:focus-visible {
top: 0;
z-index: 10;
backdrop-filter: blur(10px);
- background: rgba(10, 14, 28, 0.7);
- border-bottom: 1px solid rgba(255, 255, 255, 0.08);
+ background: var(--surface-0);
+ border-bottom: 1px solid var(--stroke-weak);
padding: 14px 24px;
display: flex;
align-items: center;
@@ -135,8 +214,8 @@ textarea:focus-visible {
width: 44px;
height: 44px;
border-radius: 999px;
- border: 1px solid rgba(255, 255, 255, 0.14);
- background: rgba(255, 255, 255, 0.04);
+ border: 1px solid var(--stroke-mid);
+ background: var(--layer-1);
color: var(--fg);
}
@@ -155,12 +234,12 @@ textarea:focus-visible {
right: 0;
height: 2px;
border-radius: 999px;
- background: rgba(242, 244, 255, 0.92);
+ background: color-mix(in srgb, var(--fg) 92%, transparent);
}
.nav-toggle-icon::before {
top: 0;
- box-shadow: 0 5px 0 rgba(242, 244, 255, 0.92);
+ box-shadow: 0 5px 0 color-mix(in srgb, var(--fg) 92%, transparent);
}
.nav-toggle-icon::after {
@@ -186,11 +265,11 @@ textarea:focus-visible {
flex-direction: column;
gap: 6px;
border-radius: 16px;
- border: 1px solid rgba(255, 255, 255, 0.14);
- background: rgba(10, 14, 28, 0.92);
+ border: 1px solid var(--stroke-mid);
+ background: var(--surface-1);
box-shadow:
0 18px 60px rgba(0, 0, 0, 0.55),
- 0 0 0 1px rgba(255, 255, 255, 0.05) inset;
+ 0 0 0 1px color-mix(in srgb, var(--stroke-weak) 60%, transparent) inset;
transform-origin: top right;
transition:
opacity 160ms ease,
@@ -219,11 +298,224 @@ textarea:focus-visible {
.nav a {
padding: 12px 12px;
border-radius: 12px;
- background: rgba(255, 255, 255, 0.04);
- border: 1px solid rgba(255, 255, 255, 0.08);
+ background: var(--layer-1);
+ border: 1px solid var(--stroke-weak);
}
}
+.theme-notch {
+ position: fixed;
+ top: var(--theme-notch-top);
+ right: max(8px, env(safe-area-inset-right));
+ z-index: 12;
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ pointer-events: none;
+}
+
+.theme-notch > * {
+ pointer-events: auto;
+}
+
+.theme-notch-handle {
+ height: 46px;
+ width: 60px;
+ border-radius: 16px 0 0 16px;
+ border: 1px solid var(--stroke-mid);
+ border-right: 0;
+ background: linear-gradient(180deg, var(--layer-3), var(--layer-1));
+ color: var(--fg);
+ cursor: pointer;
+ backdrop-filter: blur(10px);
+ box-shadow:
+ 0 14px 42px rgba(0, 0, 0, 0.28),
+ 0 0 0 1px color-mix(in srgb, var(--stroke-weak) 60%, transparent) inset;
+ transition:
+ transform 160ms ease,
+ box-shadow 160ms ease,
+ background 160ms ease;
+}
+
+.theme-notch-glyph {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 100%;
+ height: 100%;
+ font-weight: 900;
+ letter-spacing: -0.02em;
+ font-size: 13px;
+ line-height: 1;
+}
+
+.theme-notch-panel {
+ position: absolute;
+ right: 60px;
+ top: 0;
+ display: grid;
+ gap: 6px;
+ padding: 12px;
+ border-radius: 16px;
+ border: 1px solid var(--stroke-mid);
+ background: var(--surface-1);
+ box-shadow:
+ 0 18px 60px rgba(0, 0, 0, 0.32),
+ 0 0 0 1px color-mix(in srgb, var(--stroke-weak) 60%, transparent) inset;
+ opacity: 0;
+ transform: translateX(10px) scale(0.98);
+ visibility: hidden;
+ pointer-events: none;
+ transition:
+ opacity 160ms ease,
+ transform 160ms ease,
+ visibility 0s linear 160ms;
+}
+
+.theme-notch:hover .theme-notch-panel,
+.theme-notch[data-open="true"] .theme-notch-panel,
+.theme-notch:focus-within .theme-notch-panel {
+ opacity: 1;
+ transform: translateX(0) scale(1);
+ visibility: visible;
+ pointer-events: auto;
+ transition:
+ opacity 160ms ease,
+ transform 160ms ease,
+ visibility 0s;
+}
+
+.theme-notch:hover .theme-notch-handle,
+.theme-notch:focus-within .theme-notch-handle,
+.theme-notch[data-open="true"] .theme-notch-handle {
+ transform: translateX(-4px);
+}
+
+.theme-notch-option {
+ display: flex;
+ align-items: center;
+ justify-content: flex-start;
+ gap: 10px;
+ width: 160px;
+ min-height: 40px;
+ padding: 10px 12px;
+ border-radius: 12px;
+ border: 1px solid var(--stroke-weak);
+ background: var(--layer-1);
+ color: var(--fg);
+ cursor: pointer;
+ font-weight: 800;
+ font-size: 13px;
+ line-height: 1.1;
+ letter-spacing: -0.01em;
+ transition:
+ transform 140ms ease,
+ background 140ms ease,
+ border-color 140ms ease;
+}
+
+.theme-notch-option:hover {
+ transform: translateY(-1px);
+ background: var(--layer-2);
+ border-color: var(--stroke-mid);
+}
+
+.theme-notch-dot {
+ width: 9px;
+ height: 9px;
+ border-radius: 999px;
+ border: 2px solid var(--stroke-mid);
+ background: transparent;
+ flex: 0 0 auto;
+}
+
+.theme-notch-option[aria-checked="true"] {
+ border-color: color-mix(in srgb, var(--accent2) 50%, var(--stroke-mid));
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent2) 18%, transparent);
+}
+
+.theme-notch-option[aria-checked="true"] .theme-notch-dot {
+ border-color: var(--accent2);
+ background: var(--accent2);
+}
+
+@media (max-width: 760px) {
+ .theme-notch {
+ right: max(10px, env(safe-area-inset-right));
+ }
+
+ .theme-notch-panel {
+ right: 60px;
+ width: min(80vw, 220px);
+ }
+
+ .theme-notch-option {
+ width: 100%;
+ }
+}
+
+@media (forced-colors: active) {
+ body {
+ background: Canvas;
+ color: CanvasText;
+ }
+
+ body::before {
+ display: none;
+ }
+
+ .site-header,
+ .nav,
+ .card,
+ .hero,
+ .theme-notch-panel,
+ .theme-notch-handle,
+ .theme-notch-option {
+ background: Canvas;
+ color: CanvasText;
+ border-color: CanvasText;
+ box-shadow: none;
+ }
+
+ .theme-notch-option[aria-checked="true"] {
+ outline: 2px solid Highlight;
+ outline-offset: 2px;
+ box-shadow: none;
+ }
+
+ .theme-notch-dot {
+ border-color: CanvasText;
+ }
+}
+
+html[data-theme-transition="on"] body {
+ transition:
+ background 220ms ease,
+ color 220ms ease;
+}
+
+html[data-theme-transition="on"] body::before {
+ transition: opacity 220ms ease;
+}
+
+html[data-theme-transition="on"] .site-header,
+html[data-theme-transition="on"] .nav-toggle,
+html[data-theme-transition="on"] .nav,
+html[data-theme-transition="on"] .card,
+html[data-theme-transition="on"] .cta,
+html[data-theme-transition="on"] .subnav a,
+html[data-theme-transition="on"] .pill,
+html[data-theme-transition="on"] dialog,
+html[data-theme-transition="on"] .theme-notch-panel,
+html[data-theme-transition="on"] .theme-notch-handle,
+html[data-theme-transition="on"] .theme-notch-option {
+ transition:
+ background-color 220ms ease,
+ color 220ms ease,
+ border-color 220ms ease,
+ box-shadow 220ms ease;
+}
+
@media (prefers-reduced-motion: reduce) {
*,
*::before,
@@ -242,15 +534,15 @@ textarea:focus-visible {
flex-wrap: wrap;
margin: 18px 0 8px;
padding-bottom: 6px;
- border-bottom: 1px solid rgba(255, 255, 255, 0.08);
+ border-bottom: 1px solid var(--stroke-weak);
color: var(--muted);
}
.subnav a {
padding: 8px 10px;
border-radius: 999px;
- border: 1px solid rgba(255, 255, 255, 0.12);
- background: rgba(255, 255, 255, 0.04);
+ border: 1px solid var(--stroke-mid);
+ background: var(--layer-1);
font-weight: 700;
font-size: 13px;
}
@@ -271,7 +563,7 @@ textarea:focus-visible {
.prose {
line-height: 1.75;
- color: rgba(242, 244, 255, 0.9);
+ color: color-mix(in srgb, var(--fg) 90%, transparent);
}
.prose a {
@@ -282,11 +574,11 @@ textarea:focus-visible {
max-width: 100%;
height: auto;
border-radius: 16px;
- border: 1px solid rgba(255, 255, 255, 0.12);
+ border: 1px solid var(--stroke-mid);
}
.site-footer {
- border-top: 1px solid rgba(255, 255, 255, 0.08);
+ border-top: 1px solid var(--stroke-weak);
padding: 20px 24px;
text-align: center;
}
@@ -302,7 +594,7 @@ textarea:focus-visible {
align-items: start;
padding: 28px;
border: 1px solid var(--stroke);
- background: rgba(255, 255, 255, 0.04);
+ background: var(--layer-1);
border-radius: 18px;
}
@@ -334,7 +626,7 @@ textarea:focus-visible {
padding: 10px 14px;
border-radius: 999px;
border: 1px solid var(--stroke);
- background: linear-gradient(180deg, rgba(255, 255, 255, 0.08), rgba(255, 255, 255, 0.03));
+ background: linear-gradient(180deg, var(--layer-3), color-mix(in srgb, var(--layer-1) 75%, transparent));
font-weight: 800;
letter-spacing: -0.01em;
}
@@ -378,8 +670,8 @@ textarea:focus-visible {
flex-direction: column;
height: 100%;
border-radius: 16px;
- border: 1px solid rgba(255, 255, 255, 0.1);
- background: rgba(255, 255, 255, 0.04);
+ border: 1px solid var(--stroke-mid);
+ background: var(--card);
overflow: hidden;
transition:
transform 120ms ease,
@@ -401,13 +693,13 @@ button.card {
.card:hover {
transform: translateY(-2px);
- background: rgba(255, 255, 255, 0.06);
+ background: var(--card2);
}
.card-media .img-shimmer-wrap {
width: 100%;
height: 180px;
- border-bottom: 1px solid rgba(255, 255, 255, 0.08);
+ border-bottom: 1px solid var(--stroke-weak);
}
.card-media img {
@@ -415,14 +707,14 @@ button.card {
height: 180px;
object-fit: cover;
display: block;
- border-bottom: 1px solid rgba(255, 255, 255, 0.08);
+ border-bottom: 1px solid var(--stroke-weak);
}
.card-placeholder {
width: 100%;
height: 180px;
- background: rgba(255, 255, 255, 0.06);
- border-bottom: 1px solid rgba(255, 255, 255, 0.08);
+ background: var(--layer-2);
+ border-bottom: 1px solid var(--stroke-weak);
}
/* --- Image shimmer / lazy-load placeholder --- */
@@ -439,7 +731,7 @@ button.card {
.img-shimmer-wrap {
position: relative;
overflow: hidden;
- background: rgba(255, 255, 255, 0.08);
+ background: var(--layer-2);
}
.img-shimmer-wrap::before {
@@ -450,9 +742,9 @@ button.card {
background: linear-gradient(
90deg,
transparent 0%,
- rgba(255, 255, 255, 0.12) 35%,
- rgba(255, 255, 255, 0.22) 50%,
- rgba(255, 255, 255, 0.12) 65%,
+ color-mix(in srgb, var(--fg) 10%, transparent) 35%,
+ color-mix(in srgb, var(--fg) 18%, transparent) 50%,
+ color-mix(in srgb, var(--fg) 10%, transparent) 65%,
transparent 100%
);
animation: shimmer 1.6s ease-in-out infinite;
@@ -492,7 +784,11 @@ button.card {
.card-content {
flex: 1;
padding: 12px 12px 12px;
- background: linear-gradient(180deg, rgba(15, 27, 56, 0.75), rgba(11, 16, 32, 0.32));
+ background: linear-gradient(
+ 180deg,
+ color-mix(in srgb, var(--surface-1) 94%, transparent),
+ color-mix(in srgb, var(--surface-1) 68%, transparent)
+ );
}
.card-title {
@@ -516,8 +812,8 @@ button.card {
justify-content: space-between;
gap: 12px;
padding: 10px 12px;
- border-top: 1px solid rgba(255, 255, 255, 0.08);
- background: rgba(11, 16, 32, 0.45);
+ border-top: 1px solid var(--stroke-weak);
+ background: color-mix(in srgb, var(--surface-1) 78%, transparent);
font-size: 12px;
}
@@ -531,8 +827,8 @@ button.card {
font-weight: 800;
padding: 4px 8px;
border-radius: 999px;
- border: 1px solid rgba(255, 255, 255, 0.16);
- background: rgba(255, 255, 255, 0.06);
+ border: 1px solid var(--stroke-mid);
+ background: var(--layer-1);
}
.pill-youtube {
@@ -554,9 +850,9 @@ button.card {
.empty {
padding: 16px;
border-radius: 14px;
- border: 1px dashed rgba(255, 255, 255, 0.18);
+ border: 1px dashed var(--stroke-strong);
color: var(--muted);
- background: rgba(255, 255, 255, 0.03);
+ background: var(--layer-1);
}
.instagram-media {
diff --git a/site/src/layouts/BaseLayout.astro b/site/src/layouts/BaseLayout.astro
index 5c92564..89ddb05 100644
--- a/site/src/layouts/BaseLayout.astro
+++ b/site/src/layouts/BaseLayout.astro
@@ -50,6 +50,42 @@ const canonicalUrl = `${siteUrl}${canonicalPath.startsWith("/") ? canonicalPath
+
+
{
cfg.umami ? (
@@ -133,6 +169,44 @@ const canonicalUrl = `${siteUrl}${canonicalPath.startsWith("/") ? canonicalPath
+
+
@@ -208,6 +282,136 @@ const canonicalUrl = `${siteUrl}${canonicalPath.startsWith("/") ? canonicalPath
})();
+
+