← Tiny Tasks history

feat(examples): implement five standalone browser games and apps

Actual project-only Git commit. Email addresses are redacted; source bytes and recorded timestamps are preserved. This is a code record, not human acceptance.

commit 776e92da9946013121e0fe74b49d627b8761dac4
Author:     0xLeif <redacted@invalid>
AuthorDate: Sun Nov 8 19:35:16 2026 -0700
Commit:     0xLeif <redacted@invalid>
CommitDate: Sun Nov 8 19:35:16 2026 -0700

    feat(examples): implement five standalone browser games and apps

diff --git a/apps/tiny-tasks/LICENSE b/apps/tiny-tasks/LICENSE
new file mode 100644
index 0000000..812de8f
--- /dev/null
+++ b/apps/tiny-tasks/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2026 CorvidLabs
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/apps/tiny-tasks/README.md b/apps/tiny-tasks/README.md
new file mode 100644
index 0000000..621591a
--- /dev/null
+++ b/apps/tiny-tasks/README.md
@@ -0,0 +1,31 @@
+# Tiny Tasks
+
+A standalone browser-local example. No runtime dependencies, accounts, analytics, external artwork or network API.
+
+## Run
+
+Install Bun (verified during development with 1.4.0), then from this extracted project:
+
+~~~sh
+bun install --frozen-lockfile
+bun run test
+bun run typecheck
+bun run build
+bun run start
+~~~
+
+Open http://127.0.0.1:4353. PORT overrides the local server port. The only development dependency is exactly TypeScript 5.9.3. Tests use Bun's built-in runner; strict semantic typechecking covers src only with ES2022/DOM libraries. Bun compiles browser code into dist/ui.js. Deploy the contents of dist under a root or a trailing-slash directory such as /examples/tiny-tasks/play/; assets are relative.
+
+## Use and data
+
+Add up to 200 trimmed characters per task, complete and reopen tasks, and expand their ordered timestamped history. There is no delete/history truncation. Export tasks (JSON) includes all current tasks and events.
+
+Data is saved in this origin's localStorage under corvid-examples:tiny-tasks:v1. Reload restores validated version 1 data. Unsupported or malformed data warns and is not overwritten automatically. Valid new user edits or explicit Retry save may replace stored data. If storage access, reading or writing fails, current valid in-memory edits remain usable and an explicit warning is shown. Browser-local storage is not a backup; export before closing the tab if saving fails. Different hosts, ports and browser profiles have separate storage.
+
+Export downloads only when its button is activated. No import, migration, autosent files, server storage or cross-tab synchronization. Runtime storage access, actual downloads, keyboard/mobile behavior and accessibility need separate browser verification; model tests do not prove those properties.
+
+## Source and tests
+
+src/model.ts holds pure immutable state and validation. IDs/times are supplied to model operations. src/storage.ts validates real serialized data through an injected getItem/setItem adapter. src/ui.ts owns DOM, browser storage acquisition and explicit downloads. Invalid model operations return ModelResult errors; they do not throw RangeError. Tests exercise the actual helpers, including unavailable storage and retained edits.
+
+MIT license applies to newly authored code; see LICENSE. No claim of human implementation acceptance or deployment is included.
diff --git a/apps/tiny-tasks/build.ts b/apps/tiny-tasks/build.ts
new file mode 100644
index 0000000..ac49268
--- /dev/null
+++ b/apps/tiny-tasks/build.ts
@@ -0,0 +1,17 @@
+const base = import.meta.dir;
+await Bun.$`mkdir -p ${base}/dist`;
+const result = await Bun.build({
+    entrypoints: [base + "/src/ui.ts"],
+    outdir: base + "/dist",
+    target: "browser",
+    format: "esm",
+    naming: "ui.js",
+});
+if (!result.success) {
+    for (const log of result.logs) console.error(log);
+    process.exit(1);
+}
+for (const name of ["index.html", "styles.css"]) {
+    await Bun.write(base + "/dist/" + name, Bun.file(base + "/" + name));
+}
+console.log("Tiny Tasks built into dist/");
diff --git a/apps/tiny-tasks/bun.lock b/apps/tiny-tasks/bun.lock
new file mode 100644
index 0000000..f920404
--- /dev/null
+++ b/apps/tiny-tasks/bun.lock
@@ -0,0 +1,15 @@
+{
+  "lockfileVersion": 2,
+  "configVersion": 1,
+  "workspaces": {
+    "": {
+      "name": "@corvid-examples/tiny-tasks",
+      "devDependencies": {
+        "typescript": "5.9.3",
+      },
+    },
+  },
+  "packages": {
+    "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
+  }
+}
diff --git a/apps/tiny-tasks/index.html b/apps/tiny-tasks/index.html
new file mode 100644
index 0000000..248428e
--- /dev/null
+++ b/apps/tiny-tasks/index.html
@@ -0,0 +1,18 @@
+<!doctype html>
+<html lang="en">
+<head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
+<meta name="description" content="A small task list with retained history and browser-local storage.">
+<title>Tiny Tasks | CorvidLabs examples</title><link rel="stylesheet" href="./styles.css">
+<script type="module" src="./ui.js"></script></head>
+<body><main class="shell"><nav class="rail" aria-label="Example"><a href="../">Example details</a><span>CorvidLabs / 03</span></nav>
+<section class="surface" aria-labelledby="title"><p class="eyebrow">Small steps, kept in order</p>
+<h1 id="title">Tiny Tasks</h1><p class="intro">Add one useful next step. Complete it, reopen it, and keep the history of what changed.</p>
+<noscript>This app needs JavaScript to edit tasks. No data is sent to a server. The downloadable source includes setup instructions.</noscript>
+<form id="task-form" novalidate><div class="add-row"><div class="field"><label for="task-text">Your next task</label><input id="task-text" type="text" maxlength="200" autocomplete="off" aria-describedby="task-help input-error"></div><button type="submit">Add task</button></div><p id="task-help" class="help">Up to 200 characters. Completed tasks and their history stay here.</p></form>
+<p id="input-error" class="error" role="alert"></p>
+<p id="storage-warning" class="warning" role="alert" hidden></p>
+<p id="save-status" class="status" role="status">New in this tab. Nothing saved yet.</p>
+<p id="task-count" class="count" aria-live="polite"></p><ol id="task-list" class="task-list" aria-label="Your tasks"></ol>
+<div class="toolbar"><button id="export" type="button">Export tasks (JSON)</button><button id="retry" class="secondary" type="button">Retry save</button></div>
+<p class="help">Browser-local only. No account or cloud backup. Private browsing, clearing site data or storage limits can remove saved work. Export a copy before closing if saving is unavailable. No import in this example.</p>
+</section><footer>Built as a small, inspectable example. MIT-licensed source; no external artwork.</footer></main></body></html>
diff --git a/apps/tiny-tasks/serve.ts b/apps/tiny-tasks/serve.ts
new file mode 100644
index 0000000..229d986
--- /dev/null
+++ b/apps/tiny-tasks/serve.ts
@@ -0,0 +1,16 @@
+const base = import.meta.dir + "/dist/";
+const port = Number(process.env.PORT ?? 4353);
+if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error("PORT must be 1..65535.");
+if (!(await Bun.file(base + "index.html").exists())) throw new Error("Run bun run build before bun run start.");
+Bun.serve({
+    hostname: "127.0.0.1",
+    port,
+    fetch(request) {
+        const path = new URL(request.url).pathname;
+        const name = path.endsWith("/") || path.endsWith("/index.html") ? "index.html"
+            : path.endsWith("/ui.js") ? "ui.js" : path.endsWith("/styles.css") ? "styles.css" : null;
+        if (!name) return new Response("Not found", { status: 404 });
+        return new Response(Bun.file(base + name), { headers: { "Cache-Control": "no-store", "X-Content-Type-Options": "nosniff" } });
+    },
+});
+console.log("Tiny Tasks: http://127.0.0.1:" + port);
diff --git a/apps/tiny-tasks/src/model.ts b/apps/tiny-tasks/src/model.ts
index 1777df3..52eb799 100644
--- a/apps/tiny-tasks/src/model.ts
+++ b/apps/tiny-tasks/src/model.ts
@@ -19,15 +19,90 @@ export interface Task {
 export interface TasksState { readonly version: 1; readonly tasks: readonly Task[]; }
 export const MAX_TASK_TEXT_LENGTH = 200;
 
-export function createTasksState(): TasksState { throw new Error("Not implemented"); }
-export function validateTasksState(_value: unknown): ModelResult<TasksState> { throw new Error("Not implemented"); }
-export function addTask(_state: TasksState, _text: string, _id: string, _at: number): ModelResult<TasksState> {
-    throw new Error("Not implemented");
+function objectWithKeys(value: unknown, keys: readonly string[]): value is Record<string, unknown> {
+    return typeof value === "object" && value !== null && !Array.isArray(value)
+        && Object.keys(value).length === keys.length
+        && keys.every(key => Object.hasOwn(value, key));
 }
-export function completeTask(_state: TasksState, _id: string, _at: number): ModelResult<TasksState> {
-    throw new Error("Not implemented");
+
+function validTime(value: unknown): value is number {
+    return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
+}
+
+function validId(value: unknown): value is string {
+    return typeof value === "string" && /^[A-Za-z0-9_-]{1,64}$/.test(value);
+}
+
+function failure(error: string): ModelResult<never> { return { ok: false, error }; }
+
+export function createTasksState(): TasksState { return { version: 1, tasks: [] }; }
+
+export function validateTasksState(value: unknown): ModelResult<TasksState> {
+    if (!objectWithKeys(value, ["version", "tasks"]) || value.version !== 1 || !Array.isArray(value.tasks)) {
+        return failure("Task data must be a version 1 task list.");
+    }
+    const tasks: Task[] = [];
+    const ids = new Set<string>();
+    for (const raw of value.tasks as unknown[]) {
+        if (!objectWithKeys(raw, ["id", "text", "completed", "history"])
+            || !validId(raw.id) || ids.has(raw.id)
+            || typeof raw.text !== "string" || raw.text.trim() !== raw.text
+            || raw.text.length === 0 || raw.text.length > MAX_TASK_TEXT_LENGTH
+            || typeof raw.completed !== "boolean" || !Array.isArray(raw.history) || raw.history.length === 0) {
+            return failure("A stored task has invalid text, identity, status or history.");
+        }
+        const history: TaskEvent[] = [];
+        let previousTime = 0;
+        for (const [index, event] of (raw.history as unknown[]).entries()) {
+            const expected = index === 0 ? "added" : index % 2 === 1 ? "completed" : "reopened";
+            if (!objectWithKeys(event, ["type", "at"]) || event.type !== expected
+                || !validTime(event.at) || event.at < previousTime) {
+                return failure("Task history must contain ordered add, complete and reopen events.");
+            }
+            history.push({ type: expected, at: event.at });
+            previousTime = event.at;
+        }
+        if (raw.completed !== (history.length % 2 === 0)) return failure("Task status disagrees with its history.");
+        ids.add(raw.id);
+        tasks.push({ id: raw.id, text: raw.text, completed: raw.completed, history });
+    }
+    return { ok: true, value: { version: 1, tasks } };
+}
+
+export function addTask(state: TasksState, text: string, id: string, at: number): ModelResult<TasksState> {
+    const checked = validateTasksState(state);
+    if (!checked.ok) return checked;
+    const normalized = text.trim();
+    if (!normalized || normalized.length > MAX_TASK_TEXT_LENGTH) return failure("Enter between 1 and 200 characters.");
+    if (!validId(id) || state.tasks.some(task => task.id === id)) return failure("Task identity must be valid and unique.");
+    if (!validTime(at)) return failure("Task time must be a nonnegative whole millisecond.");
+    return { ok: true, value: { version: 1, tasks: [...checked.value.tasks,
+        { id, text: normalized, completed: false, history: [{ type: "added", at }] }] } };
+}
+
+function transition(state: TasksState, id: string, at: number, completed: boolean): ModelResult<TasksState> {
+    const checked = validateTasksState(state);
+    if (!checked.ok) return checked;
+    const task = checked.value.tasks.find(item => item.id === id);
+    if (!task) return failure("That task could not be found.");
+    if (task.completed === completed) return failure(completed ? "This task is already complete." : "This task is already open.");
+    const last = task.history.at(-1);
+    if (!validTime(at) || !last || at < last.at) return failure("Task time cannot move backwards.");
+    return { ok: true, value: { version: 1, tasks: checked.value.tasks.map(item => item.id === id
+        ? { ...item, completed, history: [...item.history, { type: completed ? "completed" : "reopened", at }] }
+        : item) } };
+}
+
+export function completeTask(state: TasksState, id: string, at: number): ModelResult<TasksState> {
+    return transition(state, id, at, true);
+}
+
+export function reopenTask(state: TasksState, id: string, at: number): ModelResult<TasksState> {
+    return transition(state, id, at, false);
 }
-export function reopenTask(_state: TasksState, _id: string, _at: number): ModelResult<TasksState> {
-    throw new Error("Not implemented");
+
+export function createTasksExport(state: TasksState): FileExport {
+    const checked = validateTasksState(state);
+    if (!checked.ok) throw new Error(checked.error);
+    return { filename: "tiny-tasks.json", mediaType: "application/json", contents: JSON.stringify(checked.value, null, 2) + "\n" };
 }
-export function createTasksExport(_state: TasksState): FileExport { throw new Error("Not implemented"); }
diff --git a/apps/tiny-tasks/src/storage.ts b/apps/tiny-tasks/src/storage.ts
index ce5d87e..bfa4166 100644
--- a/apps/tiny-tasks/src/storage.ts
+++ b/apps/tiny-tasks/src/storage.ts
@@ -1,4 +1,4 @@
-import type { TasksState } from "./model";
+import { validateTasksState, type TasksState } from "./model";
 
 export interface StorageAdapter {
     getItem(key: string): string | null;
@@ -13,9 +13,27 @@ export type SaveResult =
     | { readonly ok: false; readonly message: string };
 
 export const TASKS_STORAGE_KEY = "corvid-examples:tiny-tasks:v1";
-export function loadTasks(_storage: StorageAdapter): LoadResult<TasksState> {
-    throw new Error("Not implemented");
+
+export function loadTasks(storage: StorageAdapter): LoadResult<TasksState> {
+    try {
+        const raw = storage.getItem(TASKS_STORAGE_KEY);
+        if (raw === null) return { status: "empty" };
+        const parsed: unknown = JSON.parse(raw);
+        const checked = validateTasksState(parsed);
+        if (!checked.ok) return { status: "warning", message: checked.error + " Stored data was left untouched." };
+        return { status: "loaded", value: checked.value };
+    } catch {
+        return { status: "warning", message: "Saved data could not be read. Keep working in this tab and export a copy." };
+    }
 }
-export function saveTasks(_storage: StorageAdapter, _state: TasksState): SaveResult {
-    throw new Error("Not implemented");
+
+export function saveTasks(storage: StorageAdapter, state: TasksState): SaveResult {
+    const checked = validateTasksState(state);
+    if (!checked.ok) return { ok: false, message: checked.error };
+    try {
+        storage.setItem(TASKS_STORAGE_KEY, JSON.stringify(checked.value));
+        return { ok: true };
+    } catch {
+        return { ok: false, message: "Changes are in this tab only because browser storage could not save them. Export a copy." };
+    }
 }
diff --git a/apps/tiny-tasks/src/ui.ts b/apps/tiny-tasks/src/ui.ts
new file mode 100644
index 0000000..4c9a533
--- /dev/null
+++ b/apps/tiny-tasks/src/ui.ts
@@ -0,0 +1,125 @@
+import { addTask, completeTask, createTasksExport, createTasksState, reopenTask } from "./model";
+import { loadTasks, saveTasks } from "./storage";
+
+function element<Value extends HTMLElement>(id: string): Value {
+    const found = document.getElementById(id);
+    if (!found) throw new Error("Missing interface element: " + id);
+    return found as Value;
+}
+
+function download(file: { filename: string; mediaType: string; contents: string }): void {
+    const url = URL.createObjectURL(new Blob([file.contents], { type: file.mediaType + ";charset=utf-8" }));
+    const link = document.createElement("a");
+    link.href = url;
+    link.download = file.filename;
+    document.body.append(link);
+    link.click();
+    link.remove();
+    window.setTimeout(() => URL.revokeObjectURL(url), 1000);
+}
+
+const form = element<HTMLFormElement>("task-form");
+const input = element<HTMLInputElement>("task-text");
+const list = element<HTMLOListElement>("task-list");
+const warning = element<HTMLParagraphElement>("storage-warning");
+const message = element<HTMLParagraphElement>("input-error");
+const status = element<HTMLParagraphElement>("save-status");
+const count = element<HTMLParagraphElement>("task-count");
+let state = createTasksState();
+
+function showWarning(text: string): void {
+    warning.textContent = text;
+    warning.hidden = false;
+    status.textContent = "Not saved in browser";
+}
+
+function save(): void {
+    try {
+        const result = saveTasks(window.localStorage, state);
+        if (!result.ok) { showWarning(result.message); return; }
+        warning.hidden = true;
+        status.textContent = "Saved in this browser";
+    } catch {
+        showWarning("Browser storage is unavailable. Your tasks stay in this tab. Export a copy before closing.");
+    }
+}
+
+function render(): void {
+    list.replaceChildren();
+    const completed = state.tasks.filter(task => task.completed).length;
+    count.textContent = state.tasks.length === 0 ? "A little room for your next step." :
+        completed + " of " + state.tasks.length + " complete";
+    for (const task of state.tasks) {
+        const row = document.createElement("li");
+        row.className = task.completed ? "task complete" : "task";
+        const name = document.createElement("span");
+        name.className = "task-name";
+        name.textContent = task.text;
+        const toggle = document.createElement("button");
+        toggle.type = "button";
+        toggle.dataset.taskId = task.id;
+        toggle.textContent = task.completed ? "Reopen" : "Complete";
+        toggle.setAttribute("aria-label", (task.completed ? "Reopen " : "Complete ") + task.text);
+        toggle.addEventListener("click", () => {
+            const last = task.history.at(-1)?.at ?? 0;
+            const at = Math.max(Date.now(), last);
+            const result = task.completed ? reopenTask(state, task.id, at) : completeTask(state, task.id, at);
+            if (!result.ok) { message.textContent = result.error; return; }
+            state = result.value;
+            message.textContent = "";
+            render();
+            save();
+            list.querySelector<HTMLButtonElement>('[data-task-id="' + task.id + '"]')?.focus();
+        });
+        const details = document.createElement("details");
+        const summary = document.createElement("summary");
+        summary.textContent = "History (" + task.history.length + " events)";
+        summary.setAttribute("aria-label", "History for " + task.text);
+        const events = document.createElement("ol");
+        for (const event of task.history) {
+            const item = document.createElement("li");
+            const date = new Date(event.at);
+            item.textContent = event.type[0].toUpperCase() + event.type.slice(1) + " · " + date.toLocaleString();
+            events.append(item);
+        }
+        details.append(summary, events);
+        row.append(name, toggle, details);
+        list.append(row);
+    }
+}
+
+form.addEventListener("submit", event => {
+    event.preventDefault();
+    try {
+        const result = addTask(state, input.value, crypto.randomUUID(), Date.now());
+        if (!result.ok) {
+            message.textContent = result.error;
+            input.setAttribute("aria-invalid", "true");
+            input.focus();
+            return;
+        }
+        state = result.value;
+        input.value = "";
+        input.removeAttribute("aria-invalid");
+        message.textContent = "";
+        render();
+        save();
+        input.focus();
+    } catch {
+        message.textContent = "A task could not be added. Your text is still here; try a current browser.";
+    }
+});
+element<HTMLButtonElement>("export").addEventListener("click", () => {
+    try { download(createTasksExport(state)); }
+    catch { message.textContent = "Export could not start. Your tasks are still here; try again."; }
+});
+element<HTMLButtonElement>("retry").addEventListener("click", save);
+
+try {
+    const loaded = loadTasks(window.localStorage);
+    if (loaded.status === "loaded") { state = loaded.value; status.textContent = "Restored from this browser"; }
+    else if (loaded.status === "warning") showWarning(loaded.message);
+} catch {
+    showWarning("Browser storage is unavailable. Keep working in this tab and export a copy.");
+}
+render();
diff --git a/apps/tiny-tasks/styles.css b/apps/tiny-tasks/styles.css
new file mode 100644
index 0000000..a92cc37
--- /dev/null
+++ b/apps/tiny-tasks/styles.css
@@ -0,0 +1,21 @@
+:root{--paper:#faf9f6}
+:root{font-family:"Helvetica Neue",system-ui,sans-serif;color:#15181b;background:#131619;font-synthesis:none;line-height:1.55}
+*{box-sizing:border-box}body{margin:0;padding:32px 20px 56px}a{color:inherit;text-underline-offset:4px}
+.shell{max-width:760px;margin:auto}.rail{display:flex;justify-content:space-between;gap:16px;color:#f4f3ef;font:13px ui-monospace,monospace;margin-bottom:28px;flex-wrap:wrap}
+.surface{padding:clamp(22px,5vw,48px);background:var(--paper);border-radius:6px;border:1px solid #15181b}
+.eyebrow{font:12px ui-monospace,monospace;letter-spacing:.12em;text-transform:uppercase}
+h1{font-size:clamp(36px,7vw,56px);line-height:1.05;letter-spacing:-.05em;margin:18px 0}p{margin:12px 0}
+.intro{max-width:48ch}.toolbar{display:flex;gap:12px;flex-wrap:wrap;margin:24px 0}
+button,input,textarea{font:inherit}button{min-height:44px;padding:10px 16px;border:1px solid #15181b;border-radius:4px;background:#15181b;color:#faf9f6;cursor:pointer;font-weight:600}
+button.secondary{color:#15181b;background:transparent}button:disabled{opacity:.65;cursor:not-allowed}
+:focus-visible{outline:3px solid #087267;outline-offset:4px}input,textarea{color:#15181b;background:#fffdf9;border:1px solid #505456;border-radius:4px;padding:12px;width:100%;min-width:0;font-size:18px}
+label{display:block;font-weight:650;margin-bottom:8px}.field{flex:1;min-width:0}.add-row{display:flex;gap:10px;align-items:end;margin-top:28px}.add-row button{min-height:54px}
+.help,.status,.count{font-size:14px}.error{color:#871e12;font-weight:600}.warning{border:2px solid #713b00;background:#fff0ca;padding:14px;font-size:16px}
+.task-list{padding:0;list-style:none;margin:28px 0}.task{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:12px;align-items:start;padding:20px 0;border-top:1px solid #777c7d}
+.task-name{font-size:20px;overflow-wrap:anywhere;padding-top:8px}.complete .task-name{text-decoration:line-through;color:#4c5052}
+details{grid-column:1/-1;font-size:14px}summary{cursor:pointer;min-height:44px;padding:10px 0}details ol{padding-left:24px}
+textarea{resize:vertical;min-height:380px;line-height:1.7;background:transparent;border-color:#624311;font-size:20px}
+footer{color:#c8c6be;font-size:14px;margin-top:24px}footer a{display:inline-block;min-height:44px;padding-top:10px}
+noscript{display:block;border:2px solid #871e12;padding:16px;margin:20px 0}
+@media(max-width:430px){body{padding:20px 12px 32px}.add-row{align-items:stretch;flex-direction:column}.task{grid-template-columns:1fr}.task button{justify-self:start}.toolbar button{flex:1}textarea{min-height:330px}}
+@media(prefers-reduced-motion:reduce){*,*::before,*::after{scroll-behavior:auto!important;animation:none!important;transition:none!important}}