feat: add OKLCH palette and visual enhancements

Amolith created

Introduces a comprehensive OKLCH-based color system via palette.css,
replacing the previous RGB/hex approach with perceptually uniform
colors.
Adds visual feedback for top-voted items with checkmark indicator and
3D tactile button press effects. Room URLs now auto-update to include
the room code for easier sharing.

The new palette provides better dark mode support and color consistency
across the interface while maintaining the existing green theme
identity.

Assisted-by: Claude Sonnet 4.5 via Crush

Change summary

server.ts          |   4 
static/app.js      |  42 +++++
static/index.html  |   1 
static/palette.css | 113 ++++++++++++++
static/style.css   | 385 +++++++++++++++++++++++++++++++++++------------
5 files changed, 442 insertions(+), 103 deletions(-)

Detailed changes

server.ts πŸ”—

@@ -542,6 +542,10 @@ serve(async (req) => {
     const css = await Deno.readTextFile("./static/style.css");
     return new Response(css, { headers: { "content-type": "text/css" } });
   }
+  if (url.pathname === "/palette.css") {
+    const css = await Deno.readTextFile("./static/palette.css");
+    return new Response(css, { headers: { "content-type": "text/css" } });
+  }
   if (url.pathname === "/app.js") {
     const js = await Deno.readTextFile("./static/app.js");
     return new Response(js, { headers: { "content-type": "application/javascript" } });

static/app.js πŸ”—

@@ -50,6 +50,9 @@ document.getElementById('leave-btn').addEventListener('click', () => {
   items = [];
   messageQueue = [];
   listContainer.innerHTML = '';
+  
+  // Clear URL when leaving room
+  history.replaceState(null, '', location.pathname);
 });
 
 document.getElementById('copy-btn').addEventListener('click', async () => {
@@ -229,6 +232,11 @@ function handleMessage(msg) {
       }
       isReady = true;
       setUIEnabled(true);
+      
+      // Update URL to include room code for easy copying
+      const newUrl = `${location.origin}${location.pathname}?room=${currentRoom}`;
+      history.replaceState(null, '', newUrl);
+      
       renderTitle();
       render();
       break;
@@ -305,6 +313,13 @@ function vote(itemId, voteType) {
   const sorted = getSortedItems();
   selectedPosition = sorted.findIndex(i => i.id === itemId);
   
+  // Keep the button visually pressed briefly to avoid flicker
+  const btn = document.querySelector(`[data-item-id="${itemId}"] [data-action="vote"][data-vote-type="${voteType}"]`);
+  if (btn) {
+    btn.classList.add('press-lock');
+    setTimeout(() => btn.classList.remove('press-lock'), 220);
+  }
+  
   lastAction = { type: 'vote', itemId, previousVote: currentVote };
   
   if (currentVote === voteType) {
@@ -569,15 +584,31 @@ function render() {
   // Sort: vetoed items last, then by score
   const sorted = getSortedItems();
   
+  // Check if any votes have been cast
+  const hasAnyVotes = sorted.some(item => Object.keys(item.votes).length > 0);
+  
+  // Calculate highest score among non-vetoed items
+  const nonVetoedItems = sorted.filter(item => 
+    !Object.values(item.votes).includes('veto')
+  );
+  const highestScore = nonVetoedItems.length > 0 
+    ? Math.max(...nonVetoedItems.map(item => 
+        Object.values(item.votes).reduce((sum, v) => 
+          sum + (v === 'up' ? 1 : v === 'down' ? -1 : 0), 0)
+      ))
+    : -Infinity;
+  
   listContainer.innerHTML = sorted.map(item => {
     const myVote = item.votes[userId];
     const score = Object.values(item.votes).reduce((sum, v) => 
       sum + (v === 'up' ? 1 : v === 'down' ? -1 : 0), 0);
     const isVetoed = Object.values(item.votes).includes('veto');
     const isSelected = item.id === selectedItemId;
+    const isTopVoted = hasAnyVotes && !isVetoed && score === highestScore && nonVetoedItems.length > 0;
     
     return `
-      <div class="list-item ${isVetoed ? 'vetoed' : ''} ${isSelected ? 'selected' : ''}" data-item-id="${item.id}">
+      <div class="list-item ${isVetoed ? 'vetoed' : ''} ${isSelected ? 'selected' : ''} ${isTopVoted ? 'top-voted' : ''}" data-item-id="${item.id}">
+        ${isTopVoted ? '<svg class="top-check" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg>' : ''}
         <div class="list-item-text">${escapeHtml(item.text)}</div>
         <div class="list-item-actions">
           <div class="score">${score > 0 ? '+' : ''}${score}</div>
@@ -662,6 +693,15 @@ listContainer.addEventListener('dblclick', (e) => {
   editItem(itemId);
 });
 
+// Minimal press-lock for non-vote buttons to smooth release
+document.addEventListener('click', (e) => {
+  const btn = e.target.closest('button');
+  if (!btn) return;
+  if (btn.classList.contains('vote-btn')) return; // handled in vote()
+  btn.classList.add('press-lock');
+  setTimeout(() => btn.classList.remove('press-lock'), 180);
+});
+
 // Make functions global
 window.vote = vote;
 window.deleteItem = deleteItem;

static/index.html πŸ”—

@@ -10,6 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-or-later
   <meta charset="UTF-8">
   <meta name="viewport" content="width=device-width, initial-scale=1.0">
   <title>Sift</title>
+  <link rel="stylesheet" href="/palette.css">
   <link rel="stylesheet" href="/style.css">
 </head>
 <body>

static/palette.css πŸ”—

@@ -0,0 +1,113 @@
+/*
+ * SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
+ *
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+/*
+  Colors generated by Theme Machine | MIT License
+  https://tools.keithjgrant.com/theme-machine/
+*/
+:root {
+  --stone-1: oklch(98% 0.003 121);
+  --stone-2: oklch(97% 0.006 121);
+  --stone-3: oklch(93% 0.01 121);
+  --stone-4: oklch(84% 0.011 121);
+  --stone-5: oklch(80% 0.015 121);
+  --stone-6: oklch(71% 0.018 121);
+  --stone-7: oklch(66% 0.019 121);
+  --stone-8: oklch(58% 0.02 121);
+  --stone-9: oklch(53% 0.019 121);
+  --stone-10: oklch(49% 0.018 121);
+  --stone-11: oklch(42% 0.016 121);
+  --stone-12: oklch(35% 0.014 121);
+  --stone-13: oklch(27% 0.011 121);
+  --stone-14: oklch(20% 0.009 121);
+  --stone-15: oklch(16% 0.007 121);
+  --stone-16: oklch(10% 0.005 121);
+  --stone: var(--stone-8);
+
+  --ash-1: oklch(98% 0.003 301);
+  --ash-2: oklch(97% 0.006 301);
+  --ash-3: oklch(93% 0.01 301);
+  --ash-4: oklch(84% 0.011 301);
+  --ash-5: oklch(80% 0.015 301);
+  --ash-6: oklch(71% 0.018 301);
+  --ash-7: oklch(66% 0.019 301);
+  --ash-8: oklch(58% 0.02 301);
+  --ash-9: oklch(53% 0.019 301);
+  --ash-10: oklch(49% 0.018 301);
+  --ash-11: oklch(42% 0.016 301);
+  --ash-12: oklch(35% 0.014 301);
+  --ash-13: oklch(27% 0.011 301);
+  --ash-14: oklch(20% 0.009 301);
+  --ash-15: oklch(16% 0.007 301);
+  --ash-16: oklch(10% 0.005 301);
+  --ash: var(--ash-8);
+
+  --avocado-1: oklch(98% 0.018 121);
+  --avocado-2: oklch(97% 0.038 121);
+  --avocado-3: oklch(93% 0.062 121);
+  --avocado-4: oklch(84% 0.074 121);
+  --avocado-5: oklch(80% 0.099 121);
+  --avocado-6: oklch(71% 0.117 121);
+  --avocado-7: oklch(66% 0.124 121);
+  --avocado-8: oklch(58% 0.13 121);
+  --avocado-9: oklch(53% 0.124 121);
+  --avocado-10: oklch(49% 0.117 121);
+  --avocado-11: oklch(42% 0.105 121);
+  --avocado-12: oklch(35% 0.092 121);
+  --avocado-13: oklch(27% 0.074 121);
+  --avocado-14: oklch(20% 0.056 121);
+  --avocado-15: oklch(16% 0.043 121);
+  --avocado-16: oklch(10% 0.028 122);
+  --avocado: var(--avocado-8);
+
+  --lilac-1: oklch(97.9% 0.014 306);
+  --lilac-2: oklch(96.7% 0.024 311);
+  --lilac-3: oklch(92.7% 0.049 306);
+  --lilac-4: oklch(84% 0.074 301);
+  --lilac-5: oklch(80% 0.099 301);
+  --lilac-6: oklch(71% 0.117 301);
+  --lilac-7: oklch(66% 0.124 301);
+  --lilac-8: oklch(58% 0.13 301);
+  --lilac-9: oklch(53% 0.124 301);
+  --lilac-10: oklch(49% 0.117 301);
+  --lilac-11: oklch(42% 0.105 301);
+  --lilac-12: oklch(35% 0.092 301);
+  --lilac-13: oklch(27% 0.074 301);
+  --lilac-14: oklch(20% 0.056 301);
+  --lilac-15: oklch(16% 0.043 301);
+  --lilac-16: oklch(10% 0.031 301);
+  --lilac: var(--lilac-8);
+
+  --cranberry-1: oklch(98% 0.01 25);
+  --cranberry-2: oklch(96% 0.03 25);
+  --cranberry-3: oklch(92% 0.06 25);
+  --cranberry-4: oklch(84% 0.09 25);
+  --cranberry-5: oklch(80% 0.12 25);
+  --cranberry-6: oklch(72% 0.15 25);
+  --cranberry-7: oklch(66% 0.17 25);
+  --cranberry-8: oklch(58% 0.18 25);
+  --cranberry-9: oklch(53% 0.19 25);
+  --cranberry-10: oklch(49% 0.18 25);
+  --cranberry-11: oklch(42% 0.16 25);
+  --cranberry-12: oklch(35% 0.14 25);
+  --cranberry-13: oklch(28% 0.12 25);
+  --cranberry-14: oklch(22% 0.1 25);
+  --cranberry-15: oklch(17% 0.08 25);
+  --cranberry-16: oklch(12% 0.06 25);
+  --cranberry: var(--cranberry-8);
+  --danger-color: var(--cranberry-9);
+
+  --background-1: var(--stone-3);
+  --background-2: var(--stone-2);
+  --background-3: var(--ash-4);
+  --background-dark-1: var(--stone-14);
+  --background-dark-2: var(--stone-13);
+  --background-dark-3: var(--stone-11);
+  --text-1: var(--stone-12);
+  --text-2: var(--stone-13);
+  --brand-color: var(--avocado-8);
+  --shadow-color: var(--stone-16);
+}

static/style.css πŸ”—

@@ -6,43 +6,6 @@
 
 :root {
   color-scheme: light dark;
-  
-  /* Light theme (default) */
-  --bg-page: #f5f5f5;
-  --bg-surface: white;
-  --bg-header-code: #e8f5e0;
-  
-  --text-primary: #333;
-  --text-on-accent: white;
-  
-  --accent: #5a8c3a;
-  --accent-hover: #4a7230;
-  --accent-dark: #2d5016;
-  --accent-danger: #b22222;
-  
-  --border: #ddd;
-  --border-accent: #5a8c3a;
-  --shadow: rgba(0, 0, 0, 0.1);
-}
-
-@media (prefers-color-scheme: dark) {
-  :root {
-    --bg-page: #1a1a1a;
-    --bg-surface: #2a2a2a;
-    --bg-header-code: #3a4a34;
-    
-    --text-primary: #e0e0e0;
-    --text-on-accent: white;
-    
-    --accent: #7ab859;
-    --accent-hover: #8cc96f;
-    --accent-dark: #5a8c3a;
-    --accent-danger: #d84444;
-    
-    --border: #444;
-    --border-accent: #7ab859;
-    --shadow: rgba(0, 0, 0, 0.3);
-  }
 }
 
 * {
@@ -53,8 +16,8 @@
 
 body {
   font-family: "Atkinson Hyperlegible Next", system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Noto Sans, sans-serif;
-  background: var(--bg-page);
-  color: var(--text-primary);
+  background: var(--background-1);
+  color: var(--text-1);
   line-height: 1.6;
   -webkit-font-smoothing: antialiased;
   -moz-osx-font-smoothing: grayscale;
@@ -78,7 +41,7 @@ body {
 
 #start-screen h1 {
   margin-bottom: 3rem;
-  color: var(--accent-dark);
+  color: var(--avocado-11);
 }
 
 .buttons {
@@ -95,43 +58,69 @@ body {
 
 button {
   padding: 0.75rem 1.5rem;
-  background: var(--accent);
-  color: var(--text-on-accent);
+  background: var(--brand-color);
+  color: var(--stone-1);
   border: none;
-  border-radius: 6px;
+  border-radius: 8px;
   cursor: pointer;
   font-size: 1rem;
-  transition: background 0.2s, transform 0.05s, box-shadow 0.2s;
+  position: relative;
+  --btn-color: var(--brand-color);
+  transform: translateY(0);
+  box-shadow:
+    0 6px 0 color-mix(in oklch, var(--btn-color) 60%, black),
+    0 12px 18px color-mix(in oklch, var(--stone-16) 30%, transparent),
+    inset 0 1px 0 color-mix(in oklch, white 20%, transparent);
+  transition:
+    transform 140ms cubic-bezier(0.2, 0.7, 0.1, 1),
+    box-shadow 140ms cubic-bezier(0.2, 0.7, 0.1, 1),
+    background 140ms ease;
 }
 
 button:hover {
-  background: var(--accent-hover);
+  box-shadow:
+    0 7px 0 color-mix(in oklch, var(--btn-color) 60%, black),
+    0 14px 20px color-mix(in oklch, var(--stone-16) 35%, transparent),
+    inset 0 1px 0 color-mix(in oklch, white 25%, transparent);
 }
 
 button:active {
-  transform: translateY(1px);
+  transform: translateY(4px);
+  box-shadow:
+    0 2px 0 color-mix(in oklch, var(--btn-color) 60%, black),
+    0 6px 12px color-mix(in oklch, var(--stone-16) 25%, transparent),
+    inset 0 -1px 0 color-mix(in oklch, var(--btn-color) 50%, black);
+}
+
+button.press-lock {
+  transform: translateY(4px);
+  box-shadow:
+    0 2px 0 color-mix(in oklch, var(--btn-color) 60%, black),
+    0 6px 12px color-mix(in oklch, var(--stone-16) 25%, transparent),
+    inset 0 -1px 0 color-mix(in oklch, var(--btn-color) 50%, black);
 }
 
 button:focus-visible,
 input:focus-visible,
 textarea:focus-visible {
-  outline: 2px solid var(--border-accent);
+  outline: 2px solid var(--avocado-7);
   outline-offset: 2px;
 }
 
-input, textarea {
+input,
+textarea {
   padding: 0.75rem;
-  border: 1px solid var(--border);
+  border: 1px solid var(--stone-5);
   border-radius: 6px;
   font-size: 1rem;
   font-family: inherit;
-  background: var(--bg-surface);
-  color: var(--text-primary);
+  background: var(--stone-1);
+  color: var(--text-1);
 }
 
 input::placeholder,
 textarea::placeholder {
-  color: color-mix(in srgb, var(--text-primary) 50%, transparent);
+  color: color-mix(in oklch, var(--text-1) 50%, transparent);
 }
 
 #join-code {
@@ -146,7 +135,7 @@ textarea::placeholder {
 
 .expiry-notice {
   font-size: 0.85rem;
-  color: color-mix(in srgb, var(--text-primary) 60%, transparent);
+  color: color-mix(in oklch, var(--text-1) 60%, transparent);
   margin-top: 1rem;
 }
 
@@ -166,7 +155,7 @@ textarea::placeholder {
   align-items: center;
   margin-bottom: 2rem;
   padding-bottom: 1rem;
-  border-bottom: 2px solid var(--border-accent);
+  border-bottom: 2px solid var(--brand-color);
 }
 
 #list-screen header h2 {
@@ -178,33 +167,37 @@ textarea::placeholder {
   align-items: center;
   gap: 0.5rem;
   font-size: 0.85rem;
-  color: color-mix(in srgb, var(--text-primary) 70%, transparent);
+  color: color-mix(in oklch, var(--text-1) 70%, transparent);
 }
 
 .status-dot {
   width: 8px;
   height: 8px;
   border-radius: 50%;
-  background: #888;
+  background: var(--stone-8);
 }
 
 .status-dot.connected {
-  background: #4caf50;
-  box-shadow: 0 0 4px #4caf50;
+  background: var(--brand-color);
+  box-shadow: 0 0 4px color-mix(in oklch, var(--brand-color) 60%, transparent);
 }
 
 .status-dot.connecting {
-  background: #ff9800;
+  background: var(--cranberry-7);
   animation: pulse 1.5s ease-in-out infinite;
 }
 
 .status-dot.disconnected {
-  background: #f44336;
+  background: var(--cranberry-12);
 }
 
 @keyframes pulse {
-  0%, 100% { opacity: 1; }
-  50% { opacity: 0.5; }
+  0%, 100% {
+    opacity: 1;
+  }
+  50% {
+    opacity: 0.5;
+  }
 }
 
 .header-actions {
@@ -214,15 +207,19 @@ textarea::placeholder {
 
 #room-code {
   font-family: monospace;
-  background: var(--bg-header-code);
+  background: var(--background-2);
   padding: 0.25rem 0.5rem;
   border-radius: 6px;
 }
 
-#copy-btn, #copy-link-btn, #set-title-btn, #reset-votes-btn {
-  background: transparent;
-  color: var(--text-primary);
-  border: 1px solid var(--border);
+#copy-btn,
+#copy-link-btn,
+#set-title-btn,
+#reset-votes-btn {
+  background: var(--stone-1);
+  --btn-color: var(--stone-1);
+  color: var(--text-1);
+  border: none;
   width: 40px;
   height: 40px;
   padding: 0;
@@ -231,9 +228,11 @@ textarea::placeholder {
   place-items: center;
 }
 
-#copy-btn:hover, #copy-link-btn:hover, #set-title-btn:hover, #reset-votes-btn:hover {
-  background: var(--bg-header-code);
-  border-color: var(--border-accent);
+#copy-btn:hover,
+#copy-link-btn:hover,
+#set-title-btn:hover,
+#reset-votes-btn:hover {
+  filter: brightness(1.03);
 }
 
 .header-actions button svg {
@@ -243,13 +242,14 @@ textarea::placeholder {
 }
 
 #leave-btn {
-  background: transparent;
-  color: var(--accent-dark);
-  border: 1px solid var(--border-accent);
+  background: var(--stone-1);
+  --btn-color: var(--stone-1);
+  color: var(--avocado-12);
+  border: none;
 }
 
 #leave-btn:hover {
-  background: var(--bg-header-code);
+  filter: brightness(1.03);
 }
 
 .add-section {
@@ -280,11 +280,12 @@ textarea::placeholder {
 }
 
 .list-item {
-  background: var(--bg-surface);
+  position: relative;
+  background: var(--stone-1);
   padding: 1rem;
   margin-bottom: 0.5rem;
   border-radius: 8px;
-  box-shadow: 0 1px 3px var(--shadow);
+  box-shadow: 0 1px 3px color-mix(in oklch, var(--shadow-color) 20%, transparent);
   display: flex;
   justify-content: space-between;
   align-items: center;
@@ -294,12 +295,13 @@ textarea::placeholder {
 
 .list-item:hover {
   transform: translateY(-1px);
-  box-shadow: 0 4px 12px var(--shadow);
+  box-shadow: 0 4px 12px color-mix(in oklch, var(--shadow-color) 25%, transparent);
 }
 
 .list-item.selected {
-  outline: 2px solid var(--border-accent);
+  outline: 2px solid var(--brand-color);
   outline-offset: 2px;
+  border-radius: 10px;
 }
 
 .list-item.vetoed {
@@ -308,13 +310,15 @@ textarea::placeholder {
   overflow: hidden;
 }
 
-.list-item:has(.vote-btn.active) {
-  border-left-color: var(--accent);
+.top-check {
+  position: absolute;
+  left: -40px;
+  top: 50%;
+  transform: translateY(-50%);
+  color: var(--avocado-9);
+  flex-shrink: 0;
 }
 
-.list-item:has(.vote-btn.veto-active) {
-  border-left-color: var(--accent-danger);
-}
 
 .list-item-text {
   flex: 1;
@@ -346,25 +350,40 @@ textarea::placeholder {
 .delete-btn {
   padding: 0.5rem;
   font-size: 0.9rem;
-  background: var(--accent-danger);
-  opacity: 0.7;
+  background: var(--danger-color);
+  --btn-color: var(--danger-color);
+  color: var(--stone-1);
+  opacity: 0.8;
   transition: opacity 0.2s, background 0.2s;
 }
 
 .delete-btn:hover {
   opacity: 1;
-  background: var(--accent-danger);
+  background: var(--cranberry-8);
 }
 
 .vote-btn.active {
-  background: var(--accent-dark);
+  background: var(--avocado-11);
+  color: var(--stone-1);
+  transform: translateY(4px);
+  box-shadow:
+    0 2px 0 color-mix(in oklch, var(--btn-color) 60%, black),
+    0 6px 12px color-mix(in oklch, var(--stone-16) 25%, transparent),
+    inset 0 -1px 0 color-mix(in oklch, var(--btn-color) 50%, black);
 }
 
 .vote-btn.veto-active {
-  background: var(--accent-danger);
+  background: var(--avocado-11);
+  color: var(--stone-1);
+  transform: translateY(4px);
+  box-shadow:
+    0 2px 0 color-mix(in oklch, var(--btn-color) 60%, black),
+    0 6px 12px color-mix(in oklch, var(--stone-16) 25%, transparent),
+    inset 0 -1px 0 color-mix(in oklch, var(--btn-color) 50%, black);
 }
 
-.vote-btn svg, .delete-btn svg {
+.vote-btn svg,
+.delete-btn svg {
   width: 16px;
   height: 16px;
   display: block;
@@ -374,8 +393,8 @@ textarea::placeholder {
   min-width: 40px;
   text-align: center;
   font-weight: bold;
-  color: var(--accent);
-  background: var(--bg-header-code);
+  color: var(--brand-color);
+  background: var(--stone-2);
   padding: 0.25rem 0.5rem;
   border-radius: 999px;
 }
@@ -387,7 +406,7 @@ textarea::placeholder {
   left: 0;
   width: 100%;
   height: 100%;
-  background: rgba(0, 0, 0, 0.5);
+  background: color-mix(in oklch, var(--stone-16) 60%, transparent);
   display: flex;
   align-items: center;
   justify-content: center;
@@ -395,14 +414,14 @@ textarea::placeholder {
 }
 
 .modal-content {
-  background: var(--bg-surface);
+  background: var(--stone-1);
   padding: 2rem;
   border-radius: 8px;
   max-width: 500px;
   width: 90%;
   max-height: 80vh;
   overflow-y: auto;
-  box-shadow: 0 4px 20px var(--shadow);
+  box-shadow: 0 4px 20px color-mix(in oklch, var(--shadow-color) 35%, transparent);
 }
 
 .modal-header {
@@ -411,17 +430,17 @@ textarea::placeholder {
   align-items: center;
   margin-bottom: 1.5rem;
   padding-bottom: 0.5rem;
-  border-bottom: 2px solid var(--border);
+  border-bottom: 2px solid var(--stone-5);
 }
 
 .modal-header h3 {
   margin: 0;
-  color: var(--accent-dark);
+  color: var(--avocado-12);
 }
 
 .modal-close {
   background: transparent;
-  color: var(--text-primary);
+  color: var(--text-1);
   border: none;
   font-size: 2rem;
   line-height: 1;
@@ -432,11 +451,11 @@ textarea::placeholder {
 }
 
 .modal-close:hover {
-  color: var(--accent-danger);
+  color: var(--cranberry-9);
 }
 
 .modal-body h4 {
-  color: var(--accent);
+  color: var(--brand-color);
   margin-top: 1rem;
   margin-bottom: 0.5rem;
 }
@@ -459,10 +478,10 @@ kbd {
   padding: 0.2rem 0.4rem;
   font-size: 0.85rem;
   font-family: monospace;
-  background: var(--bg-header-code);
-  border: 1px solid var(--border);
+  background: var(--background-2);
+  border: 1px solid var(--stone-5);
   border-radius: 4px;
-  box-shadow: 0 1px 2px var(--shadow);
+  box-shadow: 0 1px 2px color-mix(in oklch, var(--shadow-color) 20%, transparent);
 }
 
 /* Screen reader only */
@@ -484,3 +503,165 @@ kbd {
     animation: none !important;
   }
 }
+
+@media (prefers-color-scheme: dark) {
+  body {
+    background: var(--background-dark-1);
+    color: var(--stone-3);
+  }
+
+  button {
+    background: var(--avocado-9);
+    --btn-color: var(--avocado-9);
+    color: var(--stone-1);
+  }
+
+  button:hover {
+    background: var(--avocado-8);
+  }
+
+  input,
+  textarea {
+    background: var(--background-dark-2);
+    border: 1px solid var(--stone-9);
+    color: var(--stone-3);
+  }
+
+  input::placeholder,
+  textarea::placeholder {
+    color: color-mix(in oklch, var(--stone-3) 50%, transparent);
+  }
+
+  .expiry-notice {
+    color: color-mix(in oklch, var(--stone-3) 60%, transparent);
+  }
+
+  #list-screen header {
+    border-bottom: 2px solid var(--avocado-9);
+  }
+
+  .connection-status {
+    color: color-mix(in oklch, var(--stone-3) 60%, transparent);
+  }
+
+  .status-dot {
+    background: var(--stone-10);
+  }
+
+  .status-dot.connected {
+    background: var(--avocado-9);
+    box-shadow: 0 0 4px color-mix(in oklch, var(--avocado-9) 60%, transparent);
+  }
+
+  .status-dot.connecting {
+    background: var(--cranberry-8);
+  }
+
+  .status-dot.disconnected {
+    background: var(--cranberry-13);
+  }
+
+  #room-code {
+    background: var(--background-dark-2);
+    color: var(--stone-3);
+  }
+
+  #copy-btn,
+  #copy-link-btn,
+  #set-title-btn,
+  #reset-votes-btn {
+    background: var(--background-dark-2);
+    --btn-color: var(--background-dark-2);
+    color: var(--stone-3);
+    border: none;
+  }
+
+  #copy-btn:hover,
+  #copy-link-btn:hover,
+  #set-title-btn:hover,
+  #reset-votes-btn:hover {
+    filter: brightness(1.04);
+  }
+
+  #leave-btn {
+    background: var(--background-dark-2);
+    color: var(--stone-3);
+    border: none;
+    --btn-color: var(--background-dark-2);
+  }
+
+  #leave-btn:hover {
+    filter: brightness(1.04);
+  }
+
+  .list-item {
+    background: var(--background-dark-2);
+    box-shadow: 0 1px 3px color-mix(in oklch, var(--shadow-color) 30%, transparent);
+  }
+
+  .list-item:hover {
+    box-shadow: 0 4px 12px color-mix(in oklch, var(--shadow-color) 45%, transparent);
+  }
+
+  .list-item.selected {
+    outline-color: var(--avocado-9);
+  }
+
+
+  .delete-btn {
+    background: var(--cranberry-10);
+    --btn-color: var(--cranberry-10);
+  }
+
+  .delete-btn:hover {
+    background: var(--cranberry-9);
+  }
+
+  .vote-btn.active {
+    background: var(--avocado-11);
+  }
+
+  .vote-btn.veto-active {
+    background: var(--avocado-12);
+  }
+
+  .score {
+    color: var(--avocado-8);
+    background: var(--stone-14);
+  }
+
+  .modal {
+    background: color-mix(in oklch, var(--background-dark-3) 75%, transparent);
+  }
+
+  .modal-content {
+    background: var(--background-dark-2);
+    box-shadow: 0 4px 20px color-mix(in oklch, var(--shadow-color) 70%, transparent);
+  }
+
+  .modal-header {
+    border-bottom: 2px solid var(--stone-11);
+  }
+
+  .modal-header h3 {
+    color: var(--avocado-8);
+  }
+
+  .modal-close {
+    color: var(--stone-3);
+  }
+
+  .modal-close:hover {
+    color: var(--cranberry-8);
+  }
+
+  .modal-body h4 {
+    color: var(--avocado-8);
+  }
+
+  kbd {
+    background: var(--background-dark-2);
+    border: 1px solid var(--stone-11);
+    box-shadow: 0 1px 2px color-mix(in oklch, var(--shadow-color) 50%, transparent);
+  }
+}