A dead simple time tracker that keeps everything in local storage. Next.js, no accounts and no server.
handcodedlocalstoragenextjsreactself-hostedserverlesstime-trackingtimer
1:root {
2 /* Warm-dark biohacker palette */
3 --bg: #0e0d0a;
4 --bg-deep: #090806;
5 --surface: #13120e;
6 --surface-2: #18160f;
7 --border: rgba(221, 215, 205, 0.06);
8 --border-green: rgba(107, 158, 120, 0.2);
9 --border-green-hover: rgba(107, 158, 120, 0.35);
10
11 --text: #ddd7cd;
12 --text-bright: #ede8e0;
13 --text-muted: #a09890;
14 --text-dim: #665f56;
15
16 --green: #6b9e78;
17 --green-bright: #7db88c;
18 --green-hi: #95cca2;
19 --green-dim: rgba(107, 158, 120, 0.12);
20 --green-glow: rgba(107, 158, 120, 0.3);
21
22 --amber: #c9a84c;
23 --amber-bright: #ddc06a;
24 --amber-dim: rgba(201, 168, 76, 0.1);
25
26 --terracotta: #c47055;
27 --terracotta-bright: #e38871;
28 --terracotta-dim: rgba(196, 112, 85, 0.1);
29
30 /* Legacy tokens kept so any missed reference still resolves to the new palette */
31 --color-one: var(--bg);
32 --color-two: var(--surface);
33 --color-three: var(--green);
34 --color-four: var(--green-bright);
35 --color-five: var(--amber);
36
37 --radius: 3px;
38 --radius-lg: 6px;
39 --breakpoint-desktop-max: 1023.99px;
40
41 --font-mono: "JetBrains Mono", "Fira Code", "Cascadia Code", "SF Mono",
42 Menlo, Consolas, ui-monospace, monospace;
43}
44
45* {
46 box-sizing: border-box;
47}
48
49html,
50body {
51 padding: 0;
52 margin: 0;
53}
54
55body {
56 font-family: var(--font-mono);
57 color: var(--text);
58 background-color: var(--bg);
59 min-height: 100vh;
60 width: 100%;
61 overflow-x: hidden;
62 font-size: 15px;
63 line-height: 1.6;
64 letter-spacing: 0.01em;
65 -webkit-font-smoothing: antialiased;
66 text-rendering: optimizeLegibility;
67 /* Subtle grid background for that operator-console feel */
68 background-image:
69 radial-gradient(
70 circle at 15% 10%,
71 rgba(107, 158, 120, 0.04) 0%,
72 transparent 45%
73 ),
74 radial-gradient(
75 circle at 85% 85%,
76 rgba(201, 168, 76, 0.025) 0%,
77 transparent 50%
78 ),
79 linear-gradient(rgba(107, 158, 120, 0.015) 1px, transparent 1px),
80 linear-gradient(90deg, rgba(107, 158, 120, 0.015) 1px, transparent 1px);
81 background-size: auto, auto, 40px 40px, 40px 40px;
82 background-position: 0 0, 0 0, -1px -1px, -1px -1px;
83 background-attachment: fixed;
84}
85
86input,
87button,
88select,
89textarea {
90 font-family: var(--font-mono);
91 color: inherit;
92}
93
94a {
95 color: var(--green-bright);
96 text-decoration: none;
97 transition: color 200ms ease;
98}
99
100a:hover {
101 color: var(--green-hi);
102}
103
104:focus {
105 outline: none;
106}
107
108a:focus-visible,
109button:focus-visible,
110input:focus-visible,
111select:focus-visible,
112textarea:focus-visible {
113 outline: 1px solid var(--green);
114 outline-offset: 2px;
115 box-shadow: 0 0 0 3px var(--green-dim);
116}
117
118::selection {
119 background: var(--green-dim);
120 color: var(--green-hi);
121}
122
123.page-transition {
124 opacity: 0;
125 position: absolute;
126 top: 0;
127 left: 0;
128 right: 0;
129}
130
131.page-transition-appear,
132.page-transition-enter {
133 opacity: 0;
134}
135
136.page-transition-appear-active,
137.page-transition-enter-active {
138 opacity: 1;
139 transition-delay: 250ms;
140 transition-duration: 250ms;
141 transition-property: opacity;
142}
143
144.page-transition-appear-done,
145.page-transition-enter-done {
146 opacity: 1;
147}
148
149.page-transition-exit {
150 opacity: 1;
151}
152
153.page-transition-exit-active {
154 opacity: 0;
155 transition-duration: 250ms;
156 transition-property: opacity;
157}
158
159.fade-appear,
160.fade-enter {
161 opacity: 0;
162 transform: translateX(-40px);
163}
164
165.fade-appear-active,
166.fade-enter-active {
167 opacity: 1;
168 transform: translateX(0);
169 transition-duration: 250ms;
170 transition-property: opacity, transform;
171}
172
173.fade-exit {
174 opacity: 1;
175 transform: translateX(0);
176}
177
178.fade-exit-active {
179 opacity: 0;
180 transform: translateX(40px);
181 transition-duration: 250ms;
182 transition-property: opacity, transform;
183 transition-delay: 0ms !important;
184}
185
186/* Toastify overrides so it matches the dark warm palette */
187.Toastify__toast {
188 font-family: var(--font-mono) !important;
189 font-size: 0.85rem !important;
190 border-radius: var(--radius) !important;
191 background: var(--surface) !important;
192 color: var(--text) !important;
193 border: 1px solid var(--border-green);
194 box-shadow: 0 6px 24px rgba(0, 0, 0, 0.45);
195}
196
197.Toastify__toast--success {
198 border-left: 3px solid var(--green);
199}
200
201.Toastify__toast--error {
202 border-left: 3px solid var(--terracotta);
203}
204
205.Toastify__progress-bar--success {
206 background: var(--green) !important;
207}
208
209.Toastify__progress-bar--error {
210 background: var(--terracotta) !important;
211}
212
213/* ----- Shared page primitives (used by log, summary, about) ----- */
214
215/* Outer page grid: fixed sidebar + fluid main + right gutter.
216 All content pages use this for consistent horizontal alignment. */
217.page-grid {
218 display: grid;
219 grid-template-columns: 60px minmax(0, 1fr) 60px;
220 width: 100%;
221 min-height: 100vh;
222}
223
224@media (max-width: 1023.99px) {
225 .page-grid {
226 grid-template-columns: 0 minmax(0, 1fr) 0;
227 }
228}
229
230.page-main {
231 grid-column: 2;
232 width: 100%;
233 max-width: 1100px;
234 justify-self: start;
235 padding: 52px 40px 120px;
236 box-sizing: border-box;
237}
238
239@media (max-width: 1023.99px) {
240 .page-main {
241 padding: 32px 20px 120px;
242 }
243}
244
245/* Page title bar: uppercase green with glow + hairline underline.
246 Use on every content page's top to signal location. */
247.page-title {
248 font-weight: 700;
249 text-transform: uppercase;
250 font-size: 1.8em;
251 letter-spacing: 0.22em;
252 color: var(--green-bright);
253 text-shadow: 0 0 16px var(--green-glow);
254 margin: 0 0 1.5rem;
255 padding-bottom: 0.8rem;
256 border-bottom: 1px solid var(--border);
257}
258
259@media (max-width: 1023.99px) {
260 .page-title {
261 font-size: 1.3em;
262 }
263}
264
265/* Section label: small uppercase tracked label used above chart / section */
266.section-label {
267 font-size: 0.72em;
268 font-weight: 600;
269 text-transform: uppercase;
270 letter-spacing: 0.22em;
271 color: var(--text-muted);
272 margin: 2rem 0 0.75rem;
273}
274
275.section-label:first-child {
276 margin-top: 0;
277}
278
279/* Empty state: consistent centered message used when a page has no data.
280 Positioned to fill the viewport so it renders identically across pages. */
281.empty-state {
282 position: fixed;
283 top: 0;
284 left: 0;
285 right: 0;
286 bottom: 0;
287 display: flex;
288 flex-direction: column;
289 align-items: center;
290 justify-content: center;
291 gap: 16px;
292 text-align: center;
293 padding: 20px;
294 pointer-events: none;
295}
296
297.empty-state > * {
298 pointer-events: auto;
299}
300
301.empty-state-title {
302 font-weight: 500;
303 font-size: 1.15em;
304 letter-spacing: 0.18em;
305 text-transform: uppercase;
306 color: var(--text-muted);
307 margin: 0;
308 padding-bottom: 14px;
309 position: relative;
310}
311
312.empty-state-title::after {
313 content: "";
314 position: absolute;
315 width: 80px;
316 left: 50%;
317 transform: translateX(-50%);
318 bottom: 0;
319 height: 1px;
320 background: linear-gradient(
321 90deg,
322 transparent,
323 var(--green) 50%,
324 transparent
325 );
326}
327
328.empty-state-hint {
329 font-size: 0.78em;
330 color: var(--text-dim);
331 letter-spacing: 0.06em;
332 max-width: 320px;
333 line-height: 1.6;
334}
335
336@media (max-width: 1023.99px) {
337 .empty-state-title {
338 font-size: 0.9em;
339 }
340}