diff --git a/CHANGELOG.md b/CHANGELOG.md
index c9be523..100a4ee 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,12 @@
本项目按 `v主版本.次版本.修订版本` 标记发布。下列内容以对应 Git tag 和已提交功能为准。
+## v1.14.0 — 2026-09-28
+
+- 新增数据库管理器 App:固定服务器与用户名的登录页、直接操作数据库的确认提示、按表分页浏览及 JSON 增删改查。
+- 管理操作只作用于当前 ChatGPT 登录用户的记录;口令保存在服务端密钥中,短时会话、失败次数限制、版本冲突检测与写入二次确认保护数据。
+- 记忆、目标、待办、收藏、档案与设置可在管理器查看;标签由记忆内容维护,旧版备份表不开放直接编辑。
+
## v1.13.0 — 2026-09-28
- D1 新增按记录存储的记忆、标签、目标、待办和收藏表;资料与设置保留 JSON。
diff --git a/README.md b/README.md
index ac35193..0ca34fd 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
以桌面操作系统为交互方式的个人记录网站。用 App 记录日常、管理目标与待办,并沿时间线回看已经留下的内容。
-在线站点:[Life OS](https://life-os-personal-desktop.biss0.chatgpt.site) · 当前应用版本:**v1.13.0** · [更新日志](CHANGELOG.md)
+在线站点:[Life OS](https://life-os-personal-desktop.biss0.chatgpt.site) · 当前应用版本:**v1.14.0** · [更新日志](CHANGELOG.md)
## 功能
@@ -13,6 +13,7 @@
| 博物馆 | 回看收藏的记忆。 |
| 目标与待办 | 创建目标和任务,设置截止日期、优先级和完成状态;目标倒计时可选择显示在桌面。 |
| 日历 | 在月视图与日视图中查看记忆、任务和目标节点,并为选定日期新增记忆。 |
+| 数据库管理器 | 在再次登录并确认风险后,按表查看、创建、编辑和删除当前账号的云端记录。 |
| 洞察 | 周报和月报展示记录频率、完成任务数、常见天气、能量记录及收藏或标记高光的记忆;仅依据已保存的数据统计。 |
| 档案 | 查看记录概况并维护个人资料。 |
| 浏览器 | 在桌面窗口里打开网页或使用 Google 搜索,支持后退、前进、刷新和外部打开。默认打开 Google;第三方网站可能禁止 iframe 嵌入。 |
@@ -35,6 +36,8 @@
主要本地数据键:`lifeos_memories`、`lifeos_museum`、`lifeos_profile`、`lifeos_settings`、`lifeos_system`。云端表结构见 `drizzle/0001_lifeos_records.sql`:记忆、标签、目标、待办、收藏各自按记录保存,资料与设置保留按用户存储的 JSON。旧版 `lifeos_states` 作为只读迁移来源保留;用户首次访问新版本时自动回填新表,旧表不会被清空。单条记录的修改使用版本号校验;冲突时会保存本地副本并要求选择。旧版浏览器标签页需刷新,整份状态写入接口已停用,避免覆盖新数据。应用的备份 `schemaVersion: 2` 与数据库迁移编号是两回事。建议定期导出备份。
+数据库管理器的服务器固定为本站 D1,用户名固定为 `lifeos-admin`,密码保存在托管环境的 `LIFEOS_DB_ADMIN_PASSWORD` 密钥中,不写进仓库或浏览器。它在原有账号登录之上再次验证密码;会话仅在当前页面内存中保留 20 分钟,退出后立即失效,连续五次输错锁定 15 分钟。管理 API 只允许当前账号在预设表内按记录操作,没有任意 SQL 执行入口。更改云端记录后应刷新桌面,使其他 App 重新加载数据。派生的标签索引和旧版状态备份不能在管理器中单独修改。
+
## 构建与验证
项目无需安装第三方 npm 依赖,使用 Node.js 运行:
@@ -55,6 +58,7 @@ public/tasks.js 目标与待办
public/calendar.js 日历
public/browser-app.js iframe 浏览器
public/terminal-app.js 命令解析与终端界面
+public/db-admin.js 数据库管理器界面
public/data-actions.js 图形界面与终端共用的数据写入
public/i18n-renderers.js 记忆、洞察、设置等双语界面
public/desktop-personalization.js 图标、窗口与壁纸偏好
@@ -64,6 +68,7 @@ public/locales/ 中文、英文文案
public/service-worker.js PWA 缓存
worker/index.js Worker 路由与必应图片代理
worker/cloud-state.js 云端记录迁移与同步 API
+worker/db-admin.js 当前用户的数据库管理 API
drizzle/ D1 数据表迁移
scripts/ 构建与产物验证
```
diff --git a/drizzle/0002_lifeos_db_admin.sql b/drizzle/0002_lifeos_db_admin.sql
new file mode 100644
index 0000000..b7cf596
--- /dev/null
+++ b/drizzle/0002_lifeos_db_admin.sql
@@ -0,0 +1,12 @@
+CREATE TABLE lifeos_admin_sessions (
+ token_hash TEXT PRIMARY KEY,
+ user_id TEXT NOT NULL,
+ expires_at INTEGER NOT NULL
+);
+CREATE INDEX idx_lifeos_admin_sessions_user ON lifeos_admin_sessions(user_id);
+
+CREATE TABLE lifeos_admin_attempts (
+ user_id TEXT PRIMARY KEY,
+ failures INTEGER NOT NULL DEFAULT 0,
+ locked_until INTEGER NOT NULL DEFAULT 0
+);
diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json
index 3d683c4..1149b42 100644
--- a/drizzle/meta/_journal.json
+++ b/drizzle/meta/_journal.json
@@ -15,6 +15,13 @@
"when": 1790640000000,
"tag": "0001_lifeos_records",
"breakpoints": true
+ },
+ {
+ "idx": 2,
+ "version": "7",
+ "when": 1790649000000,
+ "tag": "0002_lifeos_db_admin",
+ "breakpoints": true
}
]
}
diff --git a/package.json b/package.json
index 43b3281..041d430 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "life-os",
- "version": "1.13.0",
+ "version": "1.14.0",
"private": true,
"type": "module",
"scripts": {
diff --git a/public/addons.js b/public/addons.js
index bb762bc..8c9180d 100644
--- a/public/addons.js
+++ b/public/addons.js
@@ -3,9 +3,9 @@
const DRAFT_KEY = 'lifeos_today_draft_v1';
const localDay = () => { const d = new Date(); return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`; };
const escapeHtml = value => String(value || '').replace(/[&<>]/g, char => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[char]));
- const APP_VERSION = '1.13.0';
+ const APP_VERSION = '1.14.0';
window.LifeOSVersion = APP_VERSION;
- const appIds = ['today', 'timeline', 'museum', 'insights', 'profile', 'tasks', 'calendar', 'system', 'browser', 'terminal'];
+ const appIds = ['today', 'timeline', 'museum', 'insights', 'profile', 'tasks', 'calendar', 'system', 'browser', 'terminal', 'database'];
const t = (key, values) => LifeI18n.t(key, values);
function readDraft() {
diff --git a/public/db-admin.js b/public/db-admin.js
new file mode 100644
index 0000000..bf418ed
--- /dev/null
+++ b/public/db-admin.js
@@ -0,0 +1,139 @@
+/* Direct database editor. The session token stays in memory and expires server-side. */
+(() => {
+ const KINDS = ['memories', 'goals', 'tasks', 'museum', 'profile', 'settings', 'system'];
+ const text = (key, values) => LifeI18n.t(`db.${key}`, values);
+ const escape = value => String(value ?? '').replace(/[&<>"']/g, char => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[char]));
+ let session = null, expiresAt = 0, kind = 'memories', page = 0, rows = [], total = 0, selected = null, creating = false, error = '', notice = '', draft = null, draftId = null;
+ const current = () => document.querySelector('#content-database');
+
+ async function api(path, options = {}) {
+ const response = await fetch('/api/db-admin/' + path, {
+ ...options, cache: 'no-store', headers: {
+ 'content-type': 'application/json', ...(session ? { 'x-lifeos-admin-session': session } : {}), ...options.headers
+ }
+ });
+ const result = await response.json();
+ if (response.status === 401 && path !== 'login') { session = null; expiresAt = 0; error = text('sessionExpired'); render(current()); }
+ if (!response.ok) throw new Error(result.error || text('requestFailed'));
+ return result;
+ }
+
+ function login(node) {
+ node.innerHTML = `
LIFE OS / D1
${text('title')}
${text('loginBody')}
`;
+ node.querySelector('form').addEventListener('submit', async event => {
+ event.preventDefault();
+ const form = event.currentTarget, button = form.querySelector('button[type=submit]');
+ button.disabled = true; error = '';
+ try {
+ const result = await api('login', { method: 'POST', body: JSON.stringify({
+ username: form.elements.username.value, password: form.elements.password.value,
+ acknowledged: form.elements.acknowledged.checked
+ }) });
+ form.elements.password.value = '';
+ session = result.token; expiresAt = result.expiresAt; notice = text('connected');
+ await load(); render(current());
+ } catch (cause) { error = cause.message; render(current()); }
+ finally { button.disabled = false; }
+ });
+ }
+
+ async function load() {
+ if (!session) return;
+ const data = await api(`tables/${kind}/rows?page=${page}`);
+ rows = data.rows; total = data.total;
+ if (selected) selected = rows.find(row => row.id === selected.id) || null;
+ }
+
+ function editor() {
+ if (!selected && !creating) return `${text('selectRow')}
`;
+ const record = selected, initial = creating ? (['profile', 'settings', 'system'].includes(kind) ? {} : { id: '' }) : record.value;
+ return ``;
+ }
+
+ function workspace(node) {
+ node.innerHTML = `D1 / ${text('ownData')}
${text('title')}
${text('directMode')}
+
+ ${notice ? `
${escape(notice)}
` : ''}${error ? `
${escape(error)}
` : ''}
+
${text(kind)}${text('count', { count: total })}
+ ${rows.length ? rows.map(row => `
`).join('') : `
${text('empty')}
`}
+ ${page + 1} / ${Math.max(1, Math.ceil(total / 50))}
+
+
+
`;
+ node.querySelectorAll('[data-kind]').forEach(button => button.addEventListener('click', async () => {
+ kind = button.dataset.kind; page = 0; selected = null; creating = false; draft = null; draftId = null; error = ''; notice = '';
+ try { await load(); render(current()); } catch (cause) { error = cause.message; render(current()); }
+ }));
+ node.querySelectorAll('[data-row]').forEach(button => button.addEventListener('click', () => {
+ selected = rows.find(row => row.id === button.dataset.row); creating = false; draft = null; draftId = null; render(current());
+ }));
+ node.querySelectorAll('[data-action]').forEach(button => button.addEventListener('click', async () => {
+ const action = button.dataset.action;
+ if (action === 'reload') { location.reload(); return; }
+ if (action === 'logout') { try { await api('logout', { method: 'POST' }); } catch {} session = null; selected = null; notice = ''; render(current()); return; }
+ if (action === 'new') { creating = true; selected = null; draft = null; draftId = null; render(current()); return; }
+ if (action === 'cancel') { creating = false; draft = null; draftId = null; render(current()); return; }
+ if (action === 'delete') { await remove(); return; }
+ if (['refresh', 'prev', 'next'].includes(action)) {
+ if (action === 'prev') page--; if (action === 'next') page++;
+ try { await load(); render(current()); } catch (cause) { error = cause.message; render(current()); }
+ }
+ }));
+ node.querySelector('.db-editor')?.addEventListener('submit', save);
+ node.querySelector('.db-editor textarea')?.addEventListener('input', event => { draft = event.currentTarget.value; });
+ node.querySelector('.db-editor input[name=id]')?.addEventListener('input', event => { draftId = event.currentTarget.value; });
+ }
+
+ async function save(event) {
+ event.preventDefault();
+ const form = event.currentTarget, id = form.elements.id.value.trim(), before = selected;
+ draft = form.elements.json.value; draftId = id;
+ let value;
+ try { value = JSON.parse(form.elements.json.value); }
+ catch { error = text('invalidJson'); render(current()); return; }
+ if (!value || Array.isArray(value) || typeof value !== 'object' || (!['profile', 'settings', 'system'].includes(kind) && value.id !== id)) {
+ error = text('idMismatch'); render(current()); return;
+ }
+ if (!confirm(text('saveConfirm', { id }))) return;
+ const method = creating ? 'POST' : 'PATCH';
+ try {
+ await api(`tables/${kind}/rows/${encodeURIComponent(id)}`, { method,
+ body: JSON.stringify({ baseVersion: before?.revision || 0, value }) });
+ creating = false; selected = null; draft = null; draftId = null; error = ''; notice = text('saved');
+ await load(); selected = rows.find(row => row.id === id) || null; render(current());
+ } catch (cause) { error = cause.message; render(current()); }
+ }
+ async function remove() {
+ if (!selected || !confirm(text('deleteConfirm', { id: selected.id }))) return;
+ try {
+ await api(`tables/${kind}/rows/${encodeURIComponent(selected.id)}?version=${selected.revision}`, { method: 'DELETE' });
+ selected = null; draft = null; draftId = null; error = ''; notice = text('deleted'); await load(); render(current());
+ } catch (cause) { error = cause.message; render(current()); }
+ }
+ function render(node) {
+ if (!node) return;
+ if (!session || expiresAt <= Date.now()) { session = null; login(node); }
+ else workspace(node);
+ }
+ window.databaseApp = render;
+
+ const style = document.createElement('style');
+ style.textContent = `#content-database{padding:0}.db-intro{max-width:550px;margin:35px auto;padding:0 24px}.db-intro .sub{line-height:1.6}.db-login{display:grid;gap:15px;margin-top:24px}.db-login label,.db-editor label{display:grid;gap:7px;color:var(--muted);font-size:14px}.db-login input:not([type=checkbox]),.db-editor input,.db-editor textarea{width:100%;border:1px solid var(--line);background:var(--panel2);color:var(--ink);border-radius:9px;padding:11px}.db-login input[readonly],.db-editor input[readonly]{opacity:.7}.db-warning{border:1px solid #ce865b;background:#b8673024;border-radius:10px;padding:15px;line-height:1.5}.db-warning strong{color:var(--ink)}.db-warning p{margin:7px 0 0;font-size:13px;color:var(--muted)}.db-ack{display:flex!important;align-items:flex-start;gap:9px}.db-ack input{margin-top:4px}.db-error{color:#ff9292;font-size:13px}.db-notice{color:var(--accent);font-size:13px}.db-shell{padding:22px;min-height:100%}.db-head{display:flex;justify-content:space-between;gap:16px;align-items:start}.db-head h1{margin-bottom:7px}.db-head .sub{margin:0 0 18px}.db-toolbar{display:flex;align-items:center;gap:8px;flex-wrap:wrap;padding:12px 0;border-top:1px solid var(--line)}.db-tables{display:flex;flex:1;gap:5px;overflow-x:auto}.db-tables button,.db-pages button{white-space:nowrap;border:1px solid var(--line);border-radius:8px;background:var(--panel2);padding:8px 10px;color:var(--ink);font-size:13px}.db-tables button.active{background:var(--accent);color:var(--bg)}.db-columns{display:grid;grid-template-columns:minmax(180px,.8fr) minmax(260px,1.3fr);gap:15px;min-height:380px}.db-list,.db-detail{min-width:0;border:1px solid var(--line);border-radius:11px;background:var(--panel2)}.db-list{display:flex;flex-direction:column}.db-list-head{display:flex;justify-content:space-between;padding:13px;border-bottom:1px solid var(--line);font-size:13px}.db-rows{flex:1;max-height:450px;overflow:auto}.db-row{display:grid;gap:3px;width:100%;border:0;border-bottom:1px solid var(--line);background:transparent;text-align:left;padding:11px 13px;color:var(--ink)}.db-row.active,.db-row:hover{background:color-mix(in srgb,var(--accent) 12%,var(--panel2))}.db-row strong{font-size:14px;overflow:hidden;text-overflow:ellipsis}.db-row small{color:var(--muted);overflow:hidden;text-overflow:ellipsis}.db-empty,.db-placeholder{padding:25px;color:var(--muted);font-size:14px}.db-pages{display:flex;justify-content:space-between;align-items:center;gap:7px;padding:10px;border-top:1px solid var(--line);font-size:12px}.db-pages button:disabled{opacity:.4}.db-detail{padding:15px}.db-editor{display:grid;gap:14px}.db-editor-head{display:flex;justify-content:space-between;gap:10px}.db-editor-head strong{overflow-wrap:anywhere}.db-editor textarea{resize:vertical;font:13px/1.5 'DM Mono',monospace;min-height:240px}.db-note,.db-foot{font-size:12px;line-height:1.5;color:var(--muted)}.db-link{border:0;background:none;color:var(--accent);padding:0;text-decoration:underline}.db-foot{margin:16px 0 0}@media(max-width:680px){.db-shell{padding:15px}.db-columns{grid-template-columns:1fr}.db-rows{max-height:210px}.db-intro{margin:20px auto;padding:0 18px}}`;
+ document.head.append(style);
+})();
diff --git a/public/i18n-renderers.js b/public/i18n-renderers.js
index a1e721b..b390d16 100644
--- a/public/i18n-renderers.js
+++ b/public/i18n-renderers.js
@@ -110,7 +110,7 @@
window.saveProfile = () => { save('lifeos_profile', { name: pname.value, birthday: pbday.value, startDate: pstart.value || day(), motto: pmotto.value }); renderAll(); toast(t('profile.saved')); };
window.systemApp = container => {
const state = settings(), preference = LifeI18n.preference(), bytes = new Blob([JSON.stringify({ mems: mems(), museum: museum(), profile: profile(), system: sys() })]).size, oldBackup = state.lastBackup && Date.now() - new Date(state.lastBackup) > 12096e5, detected = browserLanguage() === 'zh' ? t('system.detectedZh') : t('system.detectedEn');
- container.innerHTML = `${t('system.eyebrow')}
${t('system.title')}
${t('system.appearance')}
${['dark','light','auto'].map(item => ``).join('')}
${t('system.language')}
${detected}
${t('system.storage')}
${t('system.local')}
${t('system.storageSummary',{memories:LifeI18n.number(mems().length),museum:LifeI18n.number(museum().length),tasks:LifeI18n.number(sys().tasksBoard?.tasks?.length||0),size:LifeI18n.number(Math.ceil(bytes/1024))})}
${t('system.dataStorage')}
${t('system.stored')}
${t('system.backups')}
${t('system.lastBackup',{value:state.lastBackup ? short(state.lastBackup.slice(0,10)) : t('common.never')})}${oldBackup ? `
${t('system.recommended')}` : ''}
${t('system.version')}
${t('system.release',{version:window.LifeOSVersion||'1.13.0'})}
`;
+ container.innerHTML = `${t('system.eyebrow')}
${t('system.title')}
${t('system.appearance')}
${['dark','light','auto'].map(item => ``).join('')}
${t('system.language')}
${detected}
${t('system.storage')}
${t('system.local')}
${t('system.storageSummary',{memories:LifeI18n.number(mems().length),museum:LifeI18n.number(museum().length),tasks:LifeI18n.number(sys().tasksBoard?.tasks?.length||0),size:LifeI18n.number(Math.ceil(bytes/1024))})}
${t('system.dataStorage')}
${t('system.stored')}
${t('system.backups')}
${t('system.lastBackup',{value:state.lastBackup ? short(state.lastBackup.slice(0,10)) : t('common.never')})}${oldBackup ? `
${t('system.recommended')}` : ''}
${t('system.version')}
${t('system.release',{version:window.LifeOSVersion||'1.14.0'})}
`;
};
window.importPreview = () => { const data = importData, board = data.system?.tasksBoard || {}; rootModal().innerHTML = `${t('dialog.backup')}
${t('dialog.ready')}
${t('dialog.exported',{date:short(data.exportedAt.slice(0,10))})}
${t('dialog.memories',{count:data.memories.length})}
${t('dialog.museumItems',{count:data.museum.length})}
${t('dialog.goals',{count:board.goals?.length || 0})}
${t('dialog.tasks',{count:board.tasks?.length || 0})}
`; };
window.replaceConfirm = () => { rootModal().innerHTML = `${t('dialog.final')}
${t('dialog.replaceQuestion')}
${t('dialog.replaceWarning')}
`; };
diff --git a/public/index.html b/public/index.html
index e83b1d3..9962a39 100644
--- a/public/index.html
+++ b/public/index.html
@@ -5,7 +5,7 @@
*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--ink);font-family:Manrope,system-ui;overflow:hidden}button,input,textarea{font:inherit;color:inherit}button{cursor:pointer}.desktop{height:100vh;background:radial-gradient(ellipse at 20% 0%,#3f4a503d,transparent 40%),radial-gradient(ellipse at 93% 65%,#59604224,transparent 42%),var(--bg);position:relative}.brand{position:absolute;top:28px;left:32px;font:500 11px 'DM Mono';letter-spacing:.18em;color:var(--muted)}.desktop-icons{padding:72px 32px;display:grid;grid-template-columns:repeat(3,96px);gap:22px}.app-icon{border:0;background:transparent;color:var(--ink);display:grid;gap:9px;justify-items:center;font:500 10px 'DM Mono';letter-spacing:.12em}.glyph{width:48px;height:48px;background:var(--panel);border:1px solid var(--line);border-radius:14px;display:grid;place-items:center;box-shadow:0 8px 22px #0002;font-size:20px}.app-icon:hover .glyph,.taskapp.active{border-color:var(--accent);box-shadow:0 0 0 3px color-mix(in srgb,var(--accent) 12%,transparent)}.taskbar{position:absolute;bottom:18px;left:50%;transform:translateX(-50%);height:54px;display:flex;align-items:center;gap:5px;padding:6px;background:color-mix(in srgb,var(--panel) 94%,transparent);border:1px solid var(--line);border-radius:17px;box-shadow:var(--shadow);z-index:1000}.life,.taskapp,.clock{height:40px;border:0;border-radius:11px;background:transparent;color:var(--ink);padding:0 12px;font:500 11px 'DM Mono';letter-spacing:.1em}.life{background:var(--accent);color:#151812;font-weight:500}.taskapp{min-width:42px;font-size:17px;padding:0 10px}.clock{border-left:1px solid var(--line);line-height:1.35;text-align:left;font-size:10px}.launcher,.calendar{position:absolute;bottom:82px;background:var(--panel);border:1px solid var(--line);box-shadow:var(--shadow);border-radius:17px;padding:19px;z-index:1200}.launcher{left:calc(50% - 310px);width:300px}.calendar{right:calc(50% - 310px);width:230px}.hidden{display:none!important}.launchhead{display:flex;justify-content:space-between;font:500 11px 'DM Mono';letter-spacing:.12em}.launchgrid{display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin:18px 0}.launchitem{border:1px solid transparent;background:var(--panel2);border-radius:10px;padding:13px 7px;color:var(--ink);font:500 9px 'DM Mono';letter-spacing:.08em}.launchitem:hover{border-color:var(--accent)}.systemline{border-top:1px solid var(--line);padding-top:12px;color:var(--muted);font:10px 'DM Mono';letter-spacing:.05em}.window{position:absolute;width:min(700px,calc(100vw - 100px));height:min(590px,calc(100vh - 130px));background:var(--panel);border:1px solid var(--line);border-radius:15px;box-shadow:var(--shadow);overflow:hidden;animation:in .18s ease;display:flex;flex-direction:column}.window.max{width:calc(100vw - 36px)!important;height:calc(100vh - 98px)!important;left:18px!important;top:18px!important}.titlebar{height:48px;display:flex;align-items:center;gap:10px;border-bottom:1px solid var(--line);padding:0 13px;cursor:grab;flex:none}.titlebar:active{cursor:grabbing}.titleicon{width:22px;height:22px;border:1px solid var(--line);border-radius:7px;display:grid;place-items:center;font-size:12px}.wtitle{font:500 11px 'DM Mono';letter-spacing:.13em;flex:1}.controls{display:flex;gap:5px}.controls button{width:25px;height:25px;border:0;background:transparent;color:var(--muted);border-radius:6px}.controls button:hover{background:var(--panel2);color:var(--ink)}.content{padding:24px;overflow:auto;height:100%}.eyebrow{font:10px 'DM Mono';letter-spacing:.16em;color:var(--muted);margin-bottom:8px}h1,h2,p{margin-top:0}h1{font-size:28px;letter-spacing:-.05em;margin-bottom:3px}h2{font-size:13px;letter-spacing:.07em}.sub{color:var(--muted);font-size:14px}.today-grid{display:grid;grid-template-columns:1.3fr .7fr;gap:24px}.entry{width:100%;height:190px;resize:none;background:var(--panel2);border:1px solid var(--line);border-radius:12px;padding:15px;outline:none;line-height:1.55}.entry:focus{border-color:var(--accent)}.choices{display:flex;gap:7px;flex-wrap:wrap}.chip{border:1px solid var(--line);border-radius:99px;background:transparent;padding:8px 10px;color:var(--muted);font:10px 'DM Mono';letter-spacing:.06em}.chip.selected{background:var(--accent);border-color:var(--accent);color:#171b12}.energy{display:flex;gap:7px}.energy button{width:31px;height:31px;border:1px solid var(--line);background:var(--panel2);color:var(--muted);border-radius:6px}.energy button.selected{background:var(--accent);color:#171b12;border-color:var(--accent)}.film{border-top:1px solid var(--line);margin-top:18px;padding-top:16px}.film-title{font-size:20px;letter-spacing:-.03em}.film-copy{color:var(--muted);font-size:13px;line-height:1.55}.primary,.secondary,.danger{border:0;padding:11px 14px;border-radius:9px;font:500 10px 'DM Mono';letter-spacing:.1em}.primary{background:var(--accent);color:#151812}.secondary{background:var(--panel2);border:1px solid var(--line)}.danger{background:#a94e4e;color:#fff}.actions{display:flex;gap:9px;margin-top:18px}.status{font:10px 'DM Mono';letter-spacing:.1em;color:var(--accent);align-self:center}.timeline{position:relative;margin:8px 0}.memory{border-left:1px solid var(--line);padding:0 0 23px 25px;position:relative}.memory:before{content:'';position:absolute;left:-4px;top:5px;width:7px;height:7px;background:var(--accent);border-radius:50%}.memory button{all:unset;cursor:pointer;width:100%}.memory .date{font:10px 'DM Mono';color:var(--muted);letter-spacing:.1em}.memory .mtitle{font-size:18px;margin:5px 0}.memory .snippet{color:var(--muted);font-size:13px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.empty{display:grid;place-items:center;min-height:280px;color:var(--muted);text-align:center;font-size:14px}.museum{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px}.artifact{background:var(--panel2);border:1px solid var(--line);border-radius:12px;padding:17px;text-align:left;color:var(--ink)}.artifact:hover{border-color:var(--accent)}.artifact span{display:block;font:10px 'DM Mono';color:var(--muted);letter-spacing:.08em}.artifact strong{display:block;font-size:18px;margin:18px 0 7px;letter-spacing:-.04em}.insights{display:grid;grid-template-columns:repeat(2,1fr);gap:12px}.metric{border:1px solid var(--line);border-radius:12px;padding:18px;min-height:120px}.metric .big{font-size:36px;letter-spacing:-.07em}.bars{display:flex;align-items:end;height:56px;gap:5px;margin-top:12px}.bar{background:var(--accent);opacity:.78;width:12%;border-radius:3px 3px 0 0}.profile-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:12px;margin:20px 0}.stat{border-top:1px solid var(--line);padding-top:10px}.stat b{display:block;font-size:24px;letter-spacing:-.06em}.stat span{font:10px 'DM Mono';color:var(--muted);letter-spacing:.08em}.formrow{display:grid;grid-template-columns:1fr 1.5fr;gap:12px;align-items:center;margin:9px 0}.formrow input{border:1px solid var(--line);background:var(--panel2);border-radius:8px;padding:10px}.settings section{border-top:1px solid var(--line);padding:17px 0}.settings section:first-child{border-top:0}.settings p{color:var(--muted);font-size:13px;line-height:1.55}.modal{position:fixed;inset:0;background:#0008;display:grid;place-items:center;z-index:5000}.modalbox{position:relative;width:min(410px,calc(100vw - 36px));background:var(--panel);border:1px solid var(--line);border-radius:14px;padding:23px;box-shadow:var(--shadow)}.modal-close{position:absolute;top:12px;right:12px;width:32px;height:32px;border:1px solid var(--line);border-radius:50%;background:transparent;color:var(--ink);font-size:24px;line-height:1;cursor:pointer}.modal-close:hover,.modal-close:focus-visible{background:var(--accent);color:#fff;outline:none}.toast{position:fixed;top:24px;left:50%;transform:translateX(-50%);background:var(--accent);color:#131611;padding:12px 15px;border-radius:9px;z-index:6000;font:500 10px 'DM Mono';letter-spacing:.1em;animation:in .18s ease}.boot{position:fixed;inset:0;background:var(--bg);z-index:9000;display:grid;place-content:center;text-align:center;gap:13px;font:12px 'DM Mono';letter-spacing:.16em}.boot b{font-size:28px;color:var(--ink)}@keyframes in{from{opacity:0;transform:scale(.98)}to{opacity:1;transform:scale(1)}}
@media(max-width:650px){body{overflow:auto}.desktop{min-height:100vh}.brand{left:22px;top:20px}.desktop-icons{padding:79px 20px 100px;grid-template-columns:repeat(3,1fr);gap:28px 16px}.taskbar{bottom:11px;width:calc(100% - 22px);justify-content:space-between}.taskapp{display:inline-grid;place-items:center;min-width:32px;padding:0 6px}.clock{display:none}.window{position:fixed!important;inset:0!important;width:100%!important;height:100%!important;border:0;border-radius:0;z-index:2000!important}.window.min{display:none}.titlebar{padding-top:env(safe-area-inset-top);height:53px}.content{padding:20px}.today-grid{grid-template-columns:1fr}.launcher{left:11px;bottom:76px;width:calc(100% - 22px)}.calendar{right:11px;bottom:76px}.museum,.insights,.profile-grid{grid-template-columns:1fr 1fr}.profile-grid{gap:15px}.formrow{grid-template-columns:1fr}.boot{font-size:10px}}
LIFE OSINITIALIZING PERSONAL SYSTEM...