initial import: static site + CI pipeline
Some checks are pending
Build and Deploy / deploy (push) Waiting to run

This commit is contained in:
djg 2026-09-21 22:38:56 +00:00
commit 911163c633
10 changed files with 633 additions and 0 deletions

View file

@ -0,0 +1,32 @@
name: Build and Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: self-hosted
env:
KUBECONFIG: /home/opc/.kube/config
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Build image
run: |
docker build \
-t git.ch4t.buzz/buzzparty/myadhd:${{ github.sha }} \
-t git.ch4t.buzz/buzzparty/myadhd:latest \
.
- name: Push to registry
run: |
docker push git.ch4t.buzz/buzzparty/myadhd:${{ github.sha }}
docker push git.ch4t.buzz/buzzparty/myadhd:latest
- name: Roll out to k8s
run: |
kubectl set image deployment/static-adhd \
nginx=git.ch4t.buzz/buzzparty/myadhd:${{ github.sha }} \
-n default
kubectl rollout status deployment/static-adhd -n default --timeout=120s

4
Dockerfile Normal file
View file

@ -0,0 +1,4 @@
FROM nginx:alpine
COPY site/ /usr/share/nginx/html/
COPY nginx.conf /etc/nginx/nginx.conf
EXPOSE 80

18
deploy.sh Executable file
View file

@ -0,0 +1,18 @@
#!/usr/bin/env bash
# Build (nothing to build yet — site is static) + sync docroot in place.
set -euo pipefail
cd "$(dirname "$0")"
BASE=/home/opc/zai-home-base
mkdir -p "$BASE/sites/myadhd.dev"
rsync -a --delete site/ "$BASE/sites/myadhd.dev/"
echo "==> verifying through Traefik (origin)"
sleep 1
code=$(curl -sk -o /dev/null -w '%{http_code}' --resolve myadhd.dev:443:127.0.0.1 https://myadhd.dev/ --max-time 10)
echo " https://myadhd.dev -> $code (origin)"
for asset in /static/style.css /static/app.js /static/favicon.svg /robots.txt /sitemap.xml; do
out=$(curl -sk -o /dev/null -w '%{http_code} %{content_type}' --resolve myadhd.dev:443:127.0.0.1 "https://myadhd.dev$asset" --max-time 10)
echo " $asset -> $out"
done
echo "==> deploy complete"

28
nginx.conf Normal file
View file

@ -0,0 +1,28 @@
worker_processes 1;
error_log /dev/stderr warn;
pid /tmp/nginx.pid;
events { worker_connections 1024; }
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
access_log /dev/stdout;
sendfile on;
gzip on;
gzip_types text/plain text/css application/javascript application/json application/xml image/svg+xml;
gzip_min_length 1024;
server {
listen 80;
root /usr/share/nginx/html;
index index.html;
error_page 404 /404.html;
add_header X-Content-Type-Options nosniff always;
add_header Referrer-Policy strict-origin-when-cross-origin always;
location ~* \.(css|js|svg|png|jpg|ico|woff2?)$ {
expires 7d;
add_header Cache-Control "public";
}
location / {
try_files $uri $uri/ $uri/index.html =404;
}
}
}

141
site/index.html Normal file
View file

@ -0,0 +1,141 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>myadhd.dev — ADHD-friendly tools for focus, tasks and momentum</title>
<meta name="description" content="Free, ADHD-friendly tools: task breaker, focus timer with time-blindness support, a dopamine menu, and a one-thing mode. No signup, everything stays in your browser.">
<meta name="theme-color" content="#14141c">
<link rel="canonical" href="https://myadhd.dev/">
<link rel="icon" href="/static/favicon.svg" type="image/svg+xml">
<link rel="stylesheet" href="/static/style.css">
<meta property="og:title" content="myadhd.dev — ADHD-friendly tools">
<meta property="og:description" content="Break down scary tasks, tame time blindness, and build momentum. Free, no signup, data stays in your browser.">
<meta property="og:type" content="website">
</head>
<body>
<header class="top">
<a class="logo" href="/">myadhd<span>.dev</span></a>
<nav class="tabs" role="tablist">
<a href="#breaker" data-tab="breaker" class="tab">Break it down</a>
<a href="#timer" data-tab="timer" class="tab">Focus timer</a>
<a href="#dopamine" data-tab="dopamine" class="tab">Dopamine menu</a>
<a href="#now" data-tab="now" class="tab">One thing</a>
</nav>
</header>
<main>
<!-- ============ TASK BREAKER ============ -->
<section id="breaker" class="tool" hidden>
<div class="tool-head">
<h1>Break it down</h1>
<p class="sub">Big scary tasks are just 510 small boring ones wearing a trench coat. Name the monster; we'll slice it.</p>
</div>
<div class="card">
<form id="breaker-form" class="row">
<input id="breaker-input" type="text" placeholder="The big scary task… e.g. 'do my taxes'" autocomplete="off">
<button class="btn" type="submit">Break it down</button>
</form>
<div id="breaker-out" hidden>
<div class="progress-wrap"><div id="breaker-progress" class="progress"></div></div>
<p class="tiny" id="breaker-count"></p>
<ul id="breaker-steps" class="steps"></ul>
<div class="row gap">
<button class="btn ghost" id="breaker-add" type="button">+ add step</button>
<button class="btn ghost" id="breaker-reset" type="button">start over</button>
</div>
</div>
<p class="tiny">Steps are editable — click any of them to rewrite it your way. Everything saves in this browser.</p>
</div>
</section>
<!-- ============ FOCUS TIMER ============ -->
<section id="timer" class="tool" hidden>
<div class="tool-head">
<h1>Focus timer</h1>
<p class="sub">A timer for time-blind brains: you always see how much time is left, how much is gone, and what phase you're in.</p>
</div>
<div class="card center">
<div class="presets">
<button class="chip preset" data-min="10">10 min</button>
<button class="chip preset" data-min="25">25 min</button>
<button class="chip preset" data-min="45">45 min</button>
<button class="chip preset break" data-min="5">5 min break</button>
<button class="chip preset break" data-min="15">15 min break</button>
</div>
<div class="timer-wrap">
<svg viewBox="0 0 220 220" class="ring" aria-hidden="true">
<circle class="ring-bg" cx="110" cy="110" r="100"/>
<circle id="ring-fg" class="ring-fg" cx="110" cy="110" r="100"/>
</svg>
<div class="timer-readout">
<div id="timer-time" class="timer-time">25:00</div>
<div id="timer-phase" class="timer-phase">pick a length</div>
</div>
</div>
<div class="row center gap">
<button class="btn" id="timer-start">start</button>
<button class="btn ghost" id="timer-reset">reset</button>
</div>
<p class="tiny" id="timer-note">Tip: 25 minutes is a suggestion, not a law. The best length is the one you'll actually start.</p>
</div>
</section>
<!-- ============ DOPAMINE MENU ============ -->
<section id="dopamine" class="tool" hidden>
<div class="tool-head">
<h1>Dopamine menu</h1>
<p class="sub">Quick, healthy mood hits so your brain stops hijacking you for worse ones. Pick how long you have.</p>
</div>
<div class="card center">
<div class="presets">
<button class="chip menu-pick" data-min="5">5 min</button>
<button class="chip menu-pick" data-min="15">15 min</button>
<button class="chip menu-pick" data-min="30">30 min</button>
<button class="chip menu-pick" data-min="60">60 min</button>
</div>
<div id="menu-item" class="menu-item" hidden>
<p id="menu-text" class="menu-text"></p>
</div>
<div class="row center gap" id="menu-actions" hidden>
<button class="btn ghost" id="menu-reroll">something else</button>
<button class="btn" id="menu-done">did it ✓</button>
</div>
<p class="tiny" id="menu-streak"></p>
</div>
</section>
<!-- ============ ONE THING ============ -->
<section id="now" class="tool" hidden>
<div class="tool-head">
<h1>One thing</h1>
<p class="sub">Not the whole task. Not the project. The next physical action your hands can actually do.</p>
</div>
<div class="card center">
<form id="now-form" class="row">
<input id="now-input" type="text" placeholder="The next physical action… e.g. 'open the tax folder'" autocomplete="off">
<button class="btn" type="submit">lock it in</button>
</form>
<div id="now-view" hidden>
<p class="now-label">the one thing:</p>
<p id="now-text" class="now-text"></p>
<div class="row center gap">
<button class="btn" id="now-done">done ✓</button>
<button class="btn ghost" id="now-edit">change</button>
</div>
<p class="tiny">If it still feels too big, start with the 2-minute version: just open the thing and look at it.</p>
</div>
</div>
</section>
</main>
<footer class="foot">
<p><strong>myadhd.dev</strong> — free tools, no signup, no tracking. Everything you type or save lives only in this browser (localStorage). Clear your browser data and it's gone.</p>
<p class="tiny">Not medical advice. Just friendly scaffolding for a brain that works differently.</p>
</footer>
<script src="/static/app.js" defer></script>
</body>
</html>

4
site/robots.txt Normal file
View file

@ -0,0 +1,4 @@
User-agent: *
Allow: /
Sitemap: https://myadhd.dev/sitemap.xml

4
site/sitemap.xml Normal file
View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url><loc>https://myadhd.dev/</loc><changefreq>weekly</changefreq><priority>1.0</priority></url>
</urlset>

264
site/static/app.js Normal file
View file

@ -0,0 +1,264 @@
/* myadhd.dev — client-side tools. No accounts, no network; everything in localStorage. */
(function () {
'use strict';
var $ = function (s, r) { return (r || document).querySelector(s); };
var $$ = function (s, r) { return Array.prototype.slice.call((r || document).querySelectorAll(s)); };
function store(k, v) {
if (v === undefined) { try { return JSON.parse(localStorage.getItem('myadhd.' + k)); } catch (e) { return null; } }
localStorage.setItem('myadhd.' + k, JSON.stringify(v));
}
function today() { return new Date().toISOString().slice(0, 10); }
/* ---------- tabs ---------- */
function showTab(name) {
$$('.tool').forEach(function (t) { t.hidden = (t.id !== name); });
$$('.tab').forEach(function (t) { t.classList.toggle('on', t.dataset.tab === name); });
if (!location.hash.slice(1)) {} // keep url in sync without loops
try { history.replaceState(null, '', '#' + name); } catch (e) {}
}
$$('.tab').forEach(function (t) {
t.addEventListener('click', function (e) { e.preventDefault(); showTab(t.dataset.tab); });
});
var initial = location.hash.slice(1);
showTab(['breaker', 'timer', 'dopamine', 'now'].indexOf(initial) !== -1 ? initial : 'breaker');
/* ---------- task breaker ---------- */
var breakerKey = 'breaker';
function breakerTemplates(task) {
return [
{ t: 'Write down what “done” looks like for: ' + task + '.', mins: 2, done: false },
{ t: 'Gather everything you need for “' + task + '” in one spot.', mins: 5, done: false },
{ t: 'Do the ugliest possible first version of “' + task + '”. No polish allowed.', mins: 5, done: false },
{ t: 'Set a 10-minute timer and start “' + task + '”. Starting is the whole job.', mins: 10, done: false },
{ t: 'Momentum check: keep going, or park it with a note for next time.', mins: 2, done: false },
{ t: 'Close the loop: tidy up and write down the very next step.', mins: 2, done: false }
];
}
function breakerRender() {
var b = store(breakerKey);
$('#breaker-out').hidden = !b;
if (!b) return;
var ul = $('#breaker-steps');
ul.innerHTML = '';
b.steps.forEach(function (s, i) {
var li = document.createElement('li');
li.className = s.done ? 'done' : '';
var tick = document.createElement('button');
tick.className = 'tick'; tick.type = 'button';
tick.setAttribute('aria-label', 'toggle done');
tick.addEventListener('click', function () {
s.done = !s.done; store(breakerKey, b); breakerRender();
});
var txt = document.createElement('span');
txt.className = 'txt'; txt.contentEditable = 'true'; txt.textContent = s.t;
txt.addEventListener('blur', function () { s.t = txt.textContent.trim(); store(breakerKey, b); });
var mins = document.createElement('span');
mins.className = 'mins'; mins.textContent = s.mins ? s.mins + ' min' : '';
li.appendChild(tick); li.appendChild(txt); li.appendChild(mins);
ul.appendChild(li);
});
var done = b.steps.filter(function (s) { return s.done; }).length;
$('#breaker-progress').style.width = b.steps.length ? (100 * done / b.steps.length) + '%' : '0';
$('#breaker-count').textContent = done + ' of ' + b.steps.length + ' steps done' +
(done === b.steps.length && b.steps.length ? ' — look at you. 🎉' : '');
}
$('#breaker-form').addEventListener('submit', function (e) {
e.preventDefault();
var task = $('#breaker-input').value.trim();
if (!task) return;
store(breakerKey, { task: task, steps: breakerTemplates(task) });
breakerRender();
});
$('#breaker-add').addEventListener('click', function () {
var b = store(breakerKey); if (!b) return;
b.steps.push({ t: 'Next step…', mins: 0, done: false });
store(breakerKey, b); breakerRender();
});
$('#breaker-reset').addEventListener('click', function () {
localStorage.removeItem('myadhd.' + breakerKey);
$('#breaker-input').value = '';
breakerRender();
});
breakerRender();
/* ---------- focus timer ---------- */
var CIRC = 628.3;
var timer = { total: 25 * 60, left: 25 * 60, running: false, phase: 'focus', iv: null };
function timerPaint() {
var m = Math.floor(timer.left / 60), s = timer.left % 60;
$('#timer-time').textContent = (m < 10 ? '0' : '') + m + ':' + (s < 10 ? '0' : '') + s;
$('#ring-fg').style.strokeDashoffset = CIRC * (1 - timer.left / timer.total);
var ph = $('#timer-phase');
ph.textContent = timer.running ? (timer.phase === 'focus' ? 'focus — you got this' : 'break — actually rest') : ph.textContent;
ph.className = 'timer-phase' + (timer.phase === 'break' ? ' break' : '');
}
function beep() {
try {
var ctx = new (window.AudioContext || window.webkitAudioContext)();
[[660, 0], [880, 0.18]].forEach(function (n) {
var o = ctx.createOscillator(), g = ctx.createGain();
o.frequency.value = n[0]; o.type = 'sine';
g.gain.setValueAtTime(0.001, ctx.currentTime + n[1]);
g.gain.exponentialRampToValueAtTime(0.12, ctx.currentTime + n[1] + 0.02);
g.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + n[1] + 0.4);
o.connect(g); g.connect(ctx.destination);
o.start(ctx.currentTime + n[1]); o.stop(ctx.currentTime + n[1] + 0.45);
});
} catch (e) {}
}
function timerStop() { clearInterval(timer.iv); timer.iv = null; timer.running = false; }
$('#timer-start').addEventListener('click', function () {
if (timer.running) { timerStop(); $('#timer-start').textContent = 'resume'; timerPaint(); return; }
timer.running = true;
$('#timer-start').textContent = 'pause';
timer.iv = setInterval(function () {
timer.left--;
if (timer.left <= 0) {
timerStop(); beep();
if (timer.phase === 'focus') {
store('sessions', (store('sessions') || 0) + 1);
timer.phase = 'break';
$('#timer-note').textContent = 'Focus round done (' + store('sessions') + ' total). Stand up. Drink water. Actually take the break.';
} else {
timer.phase = 'focus';
$('#timer-note').textContent = 'Break over. Pick a length and go again — or stop here, that is allowed too.';
}
timer.total = timer.left = (timer.phase === 'break' ? 5 : 25) * 60;
$('#timer-start').textContent = 'start';
}
timerPaint();
}, 1000);
timerPaint();
});
$('#timer-reset').addEventListener('click', function () {
timerStop(); timer.left = timer.total; $('#timer-start').textContent = 'start';
timerPaint();
});
$$('.preset').forEach(function (p) {
p.addEventListener('click', function () {
timerStop();
timer.total = timer.left = (+p.dataset.min) * 60;
timer.phase = p.classList.contains('break') ? 'break' : 'focus';
$('#timer-start').textContent = 'start';
timerPaint();
});
});
timerPaint();
/* ---------- dopamine menu ---------- */
var MENU = {
5: [
'Put on one song and tidy one single surface.',
'Drink a big glass of water like it is a magic potion.',
'Step outside for two minutes and name five sounds you hear.',
'Stretch your arms and neck slowly — like a cat waking up.',
'Text someone one true compliment. No context, no apology.',
'Draw the nearest object as badly as you possibly can.',
'Open the window and take ten slow breaths.',
'Rearrange exactly three things on your desk.',
'Do ten squats or one ridiculous dance move.',
'Write down three things you can see right now that you like.'
],
15: [
'Walk once around the block — phone stays home or in pocket.',
'Build the fanciest snack possible from what you already have.',
'Write a short thank-you note to your future self.',
'Doodle your current mood as a weather report.',
'Do a ten-minute follow-along stretch video.',
'Clean one drawer. One. Set a timer so it cannot grow.',
'Read five pages of any book you own.',
'Water (or rescue) one plant.',
'Have a shower, but treat it like a whole concert.',
'Learn three words in a language you do not speak.'
],
30: [
'Cook something new with a video recipe playing.',
'Go for a 25-minute walk with one podcast episode.',
'Call someone you like. With your voice. Wild, we know.',
'Build something with zero rules: LEGO, blanket fort, collage.',
'Brain-dump everything in your head onto paper for ten minutes.',
'Deep-clean the one object that quietly bothers you most.',
'Follow a 30-minute workout video at whatever intensity you like.',
'Walk or bike to a spot nearby you have never actually been.',
'Pack a small comfort bag for your worst brain days.',
'Ten minutes journaling, ten minutes stretching, ten minutes staring.'
],
60: [
'Long walk or bike ride somewhere with trees.',
'One-hour hobby sprint — make something terrible and be proud.',
'Bake something from scratch. Eat evidence.',
'One-room tidy: music loud, timer rules, no mercy.',
'Wandering trip (thrift store, library) with a $10 budget.',
'Digital reset: close twenty tabs, clear the inbox as far as you can.',
'Follow a full workout or yoga class at home.',
'Meal-prep one lunch so tomorrow-you says thanks.',
'Take a notebook to a café or library and people-watch or sketch.',
'Watch a documentary about something absurd and become an expert.'
]
};
var lastPick = null;
function menuPick(mins) {
var pool = MENU[mins].filter(function (x) { return x !== lastPick; });
var item = pool[Math.floor(Math.random() * pool.length)];
lastPick = item;
$('#menu-text').textContent = item;
$('#menu-item').hidden = false;
$('#menu-actions').hidden = false;
}
$$('.menu-pick').forEach(function (b) {
b.addEventListener('click', function () { menuPick(+b.dataset.min); });
});
$('#menu-reroll').addEventListener('click', function () {
var mins = $$('.menu-pick').filter(function (b) { return b.classList.contains('on'); })[0];
menuPick(mins ? +mins.dataset.min : 5);
});
function paintStreak() {
var s = store('streak');
$('#menu-streak').innerHTML = s && s.streak ?
'Streak: <span class="streak-num">' + s.streak + ' day' + (s.streak > 1 ? 's' : '') + '</span> of healthy dopamine. ' +
(s.last === today() ? 'Today is already logged.' : '') : '';
}
$('#menu-done').addEventListener('click', function () {
var s = store('streak') || { last: '', streak: 0 };
if (s.last !== today()) {
var y = new Date(Date.now() - 86400000).toISOString().slice(0, 10);
s.streak = (s.last === y) ? s.streak + 1 : 1;
s.last = today();
store('streak', s);
}
$('#menu-item').hidden = true;
$('#menu-actions').hidden = true;
paintStreak();
});
paintStreak();
/* ---------- one thing ---------- */
var nowSaved = store('now');
if (nowSaved && nowSaved.text) {
$('#now-text').textContent = nowSaved.text;
$('#now-form').hidden = true;
$('#now-view').hidden = false;
}
$('#now-form').addEventListener('submit', function (e) {
e.preventDefault();
var t = $('#now-input').value.trim();
if (!t) return;
store('now', { text: t });
$('#now-text').textContent = t;
$('#now-form').hidden = true;
$('#now-view').hidden = false;
});
$('#now-edit').addEventListener('click', function () {
$('#now-view').hidden = true;
$('#now-form').hidden = false;
$('#now-input').value = '';
$('#now-input').focus();
});
$('#now-done').addEventListener('click', function () {
localStorage.removeItem('myadhd.now');
$('#now-view').hidden = true;
$('#now-form').hidden = false;
$('#now-input').value = '';
$('#now-input').focus();
});
})();

6
site/static/favicon.svg Normal file
View file

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<rect x="2" y="2" width="60" height="60" rx="14" fill="#a78bfa" stroke="#2c2c3c" stroke-width="4"/>
<path d="M18 40c0-10 6-16 14-16s14 6 14 16z" fill="#14141c"/>
<circle cx="27" cy="34" r="3" fill="#a78bfa"/>
<circle cx="37" cy="34" r="3" fill="#a78bfa"/>
</svg>

After

Width:  |  Height:  |  Size: 332 B

132
site/static/style.css Normal file
View file

@ -0,0 +1,132 @@
/* myadhd.dev — calm, low-stimulation dark theme */
* { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #14141c;
--bg2: #1b1b26;
--card: #1e1e2a;
--line: #2c2c3c;
--text: #e6e6f0;
--dim: #9c9cb4;
--accent: #a78bfa;
--accent-dim: #6d5aa8;
--good: #7dd3a0;
--radius: 18px;
}
body {
font-family: ui-rounded, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
background: var(--bg); color: var(--text);
min-height: 100vh; line-height: 1.6;
}
main { max-width: 760px; margin: 0 auto; padding: 0 20px 60px; }
/* header */
.top {
max-width: 760px; margin: 0 auto; padding: 22px 20px 6px;
display: flex; flex-direction: column; gap: 14px;
}
.logo { font-size: 1.5rem; font-weight: 800; color: var(--text); text-decoration: none; letter-spacing: -0.5px; }
.logo span { color: var(--accent); }
.tabs { display: flex; gap: 8px; flex-wrap: wrap; }
.tab {
color: var(--dim); text-decoration: none; font-weight: 600; font-size: .95rem;
padding: 8px 16px; border-radius: 999px; border: 1.5px solid var(--line); background: var(--bg2);
}
.tab:hover { color: var(--text); }
.tab.on { background: var(--accent-dim); border-color: var(--accent-dim); color: #fff; }
/* tools */
.tool-head { margin: 34px 0 18px; }
h1 { font-size: 1.9rem; letter-spacing: -0.5px; }
.sub { color: var(--dim); margin-top: 6px; max-width: 40rem; }
.card {
background: var(--card); border: 1.5px solid var(--line);
border-radius: var(--radius); padding: 24px;
}
.center { text-align: center; }
.row { display: flex; gap: 10px; flex-wrap: wrap; }
.row.center { justify-content: center; }
.row.gap { margin-top: 18px; justify-content: center; }
input[type="text"] {
flex: 1 1 240px; font: inherit; color: var(--text);
background: var(--bg2); border: 1.5px solid var(--line); border-radius: 12px;
padding: 12px 16px; outline: none;
}
input[type="text"]:focus { border-color: var(--accent-dim); }
.btn {
font: inherit; font-weight: 700; cursor: pointer; color: #fff;
background: var(--accent-dim); border: none; border-radius: 12px; padding: 12px 22px;
}
.btn:hover { filter: brightness(1.15); }
.btn.ghost { background: transparent; border: 1.5px solid var(--line); color: var(--dim); }
.btn.ghost:hover { color: var(--text); border-color: var(--accent-dim); }
.chip {
font: inherit; font-size: .9rem; font-weight: 600; cursor: pointer;
color: var(--dim); background: var(--bg2); border: 1.5px solid var(--line);
border-radius: 999px; padding: 7px 15px;
}
.chip:hover, .chip.on { color: var(--text); border-color: var(--accent-dim); }
.presets { display: flex; gap: 8px; flex-wrap: wrap; justify-content: center; margin-bottom: 22px; }
.tiny { color: var(--dim); font-size: .85rem; margin-top: 14px; }
/* breaker */
.progress-wrap { height: 8px; background: var(--bg2); border-radius: 999px; overflow: hidden; margin: 18px 0 6px; }
.progress { height: 100%; width: 0; background: var(--accent); border-radius: 999px; transition: width .3s ease; }
.steps { list-style: none; margin-top: 14px; display: flex; flex-direction: column; gap: 8px; }
.steps li {
display: flex; align-items: flex-start; gap: 12px;
background: var(--bg2); border: 1.5px solid var(--line); border-radius: 12px; padding: 12px 14px;
}
.steps li .tick {
flex: 0 0 auto; width: 22px; height: 22px; margin-top: 2px; cursor: pointer;
border: 2px solid var(--accent-dim); border-radius: 7px; background: transparent;
}
.steps li.done .tick { background: var(--good); border-color: var(--good); }
.steps li.done .txt { color: var(--dim); text-decoration: line-through; }
.steps li .txt { flex: 1; outline: none; }
.steps li .txt:focus { background: var(--bg); border-radius: 6px; }
.steps li .mins { flex: 0 0 auto; color: var(--accent); font-weight: 700; font-size: .85rem; margin-top: 3px; }
/* timer */
.timer-wrap { position: relative; width: 240px; margin: 10px auto; }
.ring { width: 240px; height: 240px; transform: rotate(-90deg); }
.ring-bg { fill: none; stroke: var(--bg2); stroke-width: 10; }
.ring-fg {
fill: none; stroke: var(--accent); stroke-width: 10; stroke-linecap: round;
stroke-dasharray: 628.3; stroke-dashoffset: 0;
}
.timer-readout {
position: absolute; inset: 0; display: flex; flex-direction: column;
align-items: center; justify-content: center;
}
.timer-time { font-size: 2.6rem; font-weight: 800; letter-spacing: 1px; font-variant-numeric: tabular-nums; }
.timer-phase { color: var(--dim); font-size: .9rem; font-weight: 600; }
.timer-phase.break { color: var(--good); }
/* dopamine menu */
.menu-item {
background: var(--bg2); border: 1.5px dashed var(--accent-dim); border-radius: 14px;
padding: 26px 22px; margin: 8px auto; max-width: 34rem;
}
.menu-text { font-size: 1.15rem; font-weight: 600; }
#menu-streak { margin-top: 16px; }
.streak-num { color: var(--accent); font-weight: 800; }
/* one thing */
.now-label { color: var(--dim); text-transform: uppercase; letter-spacing: 2px; font-size: .8rem; margin-top: 8px; }
.now-text { font-size: 1.5rem; font-weight: 700; margin: 12px 0 20px; }
/* footer */
.foot {
max-width: 760px; margin: 40px auto 0; padding: 24px 20px 40px;
border-top: 1.5px solid var(--line); color: var(--dim); font-size: .92rem;
}
.foot .tiny { margin-top: 6px; }
@media (max-width: 560px) {
h1 { font-size: 1.55rem; }
.timer-wrap, .ring { width: 200px; height: 200px; }
}