openvibers/openvibe.network Commits
Move admin, my and login styles into /css files; refresh stale pin and project.read notes (#57)
Changes
@@@ -245,7 +245,7 @@ agent's name and the summary as plain text, never the details; a standing rule's- **Never in it:** secrets or their hashes, client ids, redirect URIs, credentials, members other than the owner, or who requested or decided a grant.- **Archived projects** still read, with `archived_at` set and every app revoked.- **Errors:** an unknown or malformed id is `404 project.not_found`; no token is `401 token.missing`; a token without the capability is `403`.--- **Catalog.** `network.project.read` is `planned` in openvibe-contracts until Contracts makes it `active`. The guard accepts `planned` and refuses only `retired`.++- **Catalog.** `network.project.read` has been `active` in openvibe-contracts since v0.88.0 (first-party). The guard accepts `active` and `planned` and refuses only `retired`.Redirect URIs must be https. `http://localhost`, `127.0.0.1` and `[::1]` are allowed for sandbox apps only. Fragments and embedded credentials are refused. At most 10 per app. A public app needs at least one. A project has at most `DEV_MAX_APPS_PER_PROJECT` active apps.@@@ -308,7 +308,7 @@ Until the relay is turned on in an environment, nobody else receives these event## Known gaps (next steps)- **Consent screen (backend built; page next).** The account chooser shows the app's name, not the capabilities it asks for. Codes and Network need a consent step before third-party production apps go live, and it must list the capability ids the app asks for, marking the ones `capability@1` marks `sensitive`; a request without a `scope` must not consent to capabilities the screen did not name. This screen is the app path's compensation for not using the agent confirmation flow — an authorization-code app acts for a person who is present and authorizes it capability by capability, so it carries no `cap_confirm` (decided 2026-10-04, `docs/t2-projects-and-grants.md` section 9). **The backend half is built:** `GET /oauth/client-info` with the authorize request's `scope` returns `capabilities: [{ id, name, description, sensitive }]` — the app-grantable requested ids, in the order requested, with `description` and `sensitive` from the `capability@1` catalog (`name` is the catalog id, since `capability@1` has no separate display name) — and `refused: [id]` for the unknown or ungrantable ones the page must not name; without a `scope` it returns `capabilities: []`. The authorization code an app receives now carries the explicit consented set (`tokens.issueCode`), so an absent or empty `scope` consents to nothing: the code stores an empty set and the token exchange mints a token with `cap: []` (the person is still identified by `on_behalf_of`), and asking the token endpoint for an id outside that set is `invalid_scope`. Apps have no refresh tokens, so no refreshed token can widen the set either. What remains is the API design session's page: render `capabilities` from `client-info`, mark the `sensitive` ones, and say the `refused` ids will not be granted (`test/consent-screen.test.js`).--- **Service-side project reads.** The route is built, but no service calls it yet. Host holds the grant. Contracts still has to make `network.project.read` `active`.++- **Service-side project reads.** The route is built, but no service calls it yet. Host holds the grant. `network.project.read` has been `active` since openvibe-contracts v0.88.0.- **PowerChat as the first project** (ADR-014 migration). This is not done. PowerChat's existing OAuth client is unchanged.- **Receivers.** Every default sandbox audience must refuse `env: sandbox` except on routes that keep sandbox data apart, and must key tenancy by `project_id`. Media and Events do this in Wave 20. Tools does not yet (see [Sandbox tokens](#sandbox-tokens)).- **Revocation fan-out.** The relayed `network.app.revoked` event is the signal receivers can use to disable an app's subscriptions or tenants early. OpenVibe.Events acts on it (and on `network.grant.changed` withdrawing `events.app.subscribe`): it disables the app's subscriptions and refuses its tokens issued before the revocation. That needs this relay switched on. A receiver that does not act on it only stops the app when its issued tokens expire (within 5 minutes); state the app created there (for example a Media object) is untouched by Network.@@@ -36,7 +36,7 @@ allowances changes.| Subjects | `server/identity/subjects.js` resolves `usr_` and `gst_` only | no `agt_` || Capability guard | `principals.js:347` `guard(capability)` throws at boot on an id the catalog does not have | built || Delegated client capability | `docs/capabilities-proposal/network.project.manage.json` (in the catalog at v0.85.0 as `public`, `planned`) | proposed, unused |--| Service-side project read | `network.project.read` (catalog: `first-party`, `planned`); `GET /internal/projects/:project_id`, granted to Host only | built (lane B step 3); no caller yet; Contracts still has to make it `active` |++| Service-side project read | `network.project.read` (catalog: `first-party`, `active` since v0.88.0); `GET /internal/projects/:project_id`, granted to Host only | built (lane B step 3); no caller yet || Sensitive capabilities, confirmations, budgets, agents | — | missing (agents: built by slice 2) |## 2. Contract vocabulary (v0.85.0, used verbatim)@@@ -305,7 +305,7 @@ inside (the `docs/t2-resource-registry.md` §4 rule).| Method and path | Capability | Body → result ||---|---|---|--| `GET /internal/agents/:agent` (built, slice 6) | `network.project.read` (exists, first-party) | `{ agent, grants, budgets }`, grants and budgets filtered to the caller's own audience (`openvibe.<caller>`) |++| `GET /internal/agents/:agent` (built, slice 6) | `network.project.read` (`active`, first-party since v0.88.0) | `{ agent, grants, budgets }`, grants and budgets filtered to the caller's own audience (`openvibe.<caller>`) || `POST /internal/confirmations` (built, slice 7) | **`network.confirmation.manage`** (published in v0.89.0, `status: planned`) | `{ requested_by: {type:'agent', id}, capability, summary, details?, resources?, request_digest, session_id?, ttl_s? }` → `201 { confirmation }` (pending) or `200 { confirmation }` (approved by a standing rule) || `GET /internal/confirmations/:id` | same | `{ confirmation, used_at }` || `POST /internal/confirmations/:id/consume` | same | `{ request_digest }` → `200 { confirmation, used_at }` |@@@ -17,165 +17,7 @@(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();</script><link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">--<style>--*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}--:root{-- --bg:#0d0d14;--bg-secondary:#151520;--bg-card:#1a1a26;--bg-input:#1c1c28;--bg-tertiary:#222233;-- --border:#2a2a3a;--border-focus:#8b5cf6;-- --accent:#3b82f6;--accent-glow:rgba(139,92,246,0.4);--accent-dark:#1d4ed8;--accent-light:#60a5fa;-- --text:#e0e0e0;--text-secondary:#b0b0b8;--text-muted:#707080;-- --live-red:#ef4444;--success:#22c55e;--warning:#f59e0b;--danger:#ef4444;-- --shadow:0 4px 20px rgba(0,0,0,0.3);--}--html{height:100%;background:var(--bg)}--body{min-height:100%;color:var(--text);font-family:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;padding-bottom:40px}----/* Layout */--.page{max-width:1200px;margin:0 auto;padding:20px}--.page-title{display:flex;align-items:center;gap:12px;margin-bottom:24px}--.page-title h1{font-size:22px;font-weight:800}--.page-title .badge{font-size:10px;padding:3px 10px;border-radius:4px;font-weight:600;text-transform:uppercase;letter-spacing:.5px;background:rgba(231,76,60,0.15);color:#e74c3c}----/* Section tabs */--.section-tabs-shell{display:flex;align-items:center;gap:8px;margin-bottom:24px}--.section-tabs-wrap{flex:1;min-width:0;position:relative;display:flex;align-items:center}--.section-tabs{-- display:flex;gap:4px;background:var(--bg-secondary);border-radius:10px;padding:4px;-- overflow-x:auto;-webkit-overflow-scrolling:touch;scrollbar-width:none;flex:1;min-width:0;--}--.section-tabs::-webkit-scrollbar{height:0}--.section-tabs button{-- flex-shrink:0;padding:8px 14px;border:none;border-radius:8px;font-size:11px;font-weight:600;-- background:transparent;color:var(--text-muted);cursor:pointer;transition:all .15s;white-space:nowrap;--}--.section-tabs button.active{background:var(--bg-card);color:var(--accent-light)}--.section-tabs button:hover:not(.active){color:var(--text)}--.tabs-scroll-btn{-- width:34px;height:34px;flex:0 0 34px;border:none;border-radius:10px;display:flex;align-items:center;justify-content:center;-- background:var(--bg-secondary);color:var(--text-muted);cursor:pointer;transition:all .15s;opacity:0;pointer-events:none;--}--.tabs-scroll-btn.visible{opacity:1;pointer-events:auto}--.tabs-scroll-btn:hover{color:var(--accent-light);background:var(--bg-card)}--.tabs-scroll-btn:disabled{opacity:.3;pointer-events:none}----/* Section */--.section{display:none;animation:sec-in .2s ease}--.section.active{display:block}--@keyframes sec-in{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}----/* Stats grid */--.stats-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:12px;margin-bottom:24px}--.stat-card{background:var(--bg-card);border:1px solid var(--border);border-radius:10px;padding:16px;text-align:center}--.stat-card .value{font-size:24px;font-weight:800;color:var(--accent-light)}--.stat-card .label{font-size:11px;color:var(--text-muted);margin-top:4px;display:flex;align-items:center;justify-content:center;gap:6px}----/* Cards */--.card{background:var(--bg-card);border:1px solid var(--border);border-radius:12px;padding:20px;margin-bottom:16px}--.card h3{font-size:14px;font-weight:700;margin-bottom:12px;display:flex;align-items:center;gap:8px}--.card .chart-wrap{position:relative;width:100%;max-height:320px}--.card canvas{max-height:320px}----/* Tables */--.admin-table{width:100%;border-collapse:collapse;font-size:13px}--.admin-table thead{border-bottom:2px solid var(--border)}--.admin-table th{padding:10px 8px;text-align:left;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.5px;color:var(--text-muted)}--.admin-table td{padding:10px 8px;border-bottom:1px solid var(--border)}--.admin-table tbody tr:hover{background:rgba(139,92,246,0.03)}----/* Buttons */--.btn{padding:8px 16px;border:none;border-radius:8px;font-size:12px;font-weight:600;cursor:pointer;transition:all .15s;display:inline-flex;align-items:center;gap:6px}--.btn-primary{background:var(--accent);color:var(--on-accent,#fff)}--.btn-primary:hover{background:var(--accent-dark)}--.btn-outline{background:transparent;border:1px solid var(--border);color:var(--text)}--.btn-outline:hover{border-color:var(--accent-dark);color:var(--accent-light)}--.btn-danger{background:rgba(231,76,60,0.1);color:var(--live-red);border:1px solid rgba(231,76,60,0.2)}--.btn-danger:hover{background:rgba(231,76,60,0.2)}--.btn-success{background:rgba(46,204,113,0.1);color:var(--success);border:1px solid rgba(46,204,113,0.2)}--.btn-success:hover{background:rgba(46,204,113,0.2)}--.btn-sm{padding:5px 10px;font-size:11px}--.btn:disabled{opacity:.4;cursor:not-allowed}----/* Form fields */--.form-field{margin-bottom:14px}--.form-field label{display:block;font-size:11px;font-weight:600;color:var(--text-muted);text-transform:uppercase;letter-spacing:.5px;margin-bottom:5px}--.form-field input,.form-field select,.form-field textarea{-- width:100%;padding:10px 12px;background:var(--bg-input);border:1px solid var(--border);-- border-radius:8px;color:var(--text);font-size:13px;outline:none;transition:border-color .2s;--}--.form-field input:focus,.form-field select:focus,.form-field textarea:focus{border-color:var(--accent)}----/* Input inline */--.input-row{display:flex;gap:8px;align-items:center;margin-bottom:12px;flex-wrap:wrap}--.input-row input,.input-row select{-- padding:8px 12px;background:var(--bg-input);border:1px solid var(--border);border-radius:8px;-- color:var(--text);font-size:13px;outline:none;flex:1;min-width:160px;--}--.input-row input:focus{border-color:var(--accent)}----/* Badges */--.badge{display:inline-block;padding:2px 8px;border-radius:4px;font-size:10px;font-weight:600;text-transform:uppercase}--.badge-success{background:rgba(46,204,113,0.15);color:#2ecc71}--.badge-danger{background:rgba(231,76,60,0.15);color:#e74c3c}--.badge-warning{background:rgba(245,158,11,0.15);color:#f59e0b}--.badge-info{background:rgba(52,152,219,0.15);color:#3498db}--.badge-admin{background:rgba(231,76,60,0.15);color:#e74c3c}--.badge-global_mod{background:rgba(155,89,182,0.15);color:#9b59b6}--.badge-streamer{background:rgba(155,89,182,0.15);color:#9b59b6}--.badge-user{background:rgba(52,152,219,0.15);color:#3498db}----/* Loading */--.muted{color:var(--text-muted);font-size:13px}--.loading{text-align:center;padding:40px;color:var(--text-muted)}----/* Toast */--.toast-container{position:fixed;top:60px;right:16px;z-index:200000;display:flex;flex-direction:column;gap:8px}--.toast{padding:12px 18px;border-radius:8px;font-size:13px;font-weight:600;color:#fff;animation:toast-in .25s ease;box-shadow:var(--shadow)}--.toast-success{background:#2ecc71}--.toast-error{background:#e74c3c}--.toast-info{background:#3498db}--.toast-warning{background:#f59e0b}--@keyframes toast-in{from{opacity:0;transform:translateX(20px)}to{opacity:1;transform:translateX(0)}}----/* Sub-tabs */--.sub-tabs{display:flex;gap:4px;margin-bottom:16px;overflow-x:auto;-webkit-overflow-scrolling:touch;flex-wrap:wrap}--.sub-tabs button{padding:6px 12px;border:1px solid var(--border);border-radius:6px;background:transparent;color:var(--text-muted);font-size:11px;font-weight:600;cursor:pointer}--.sub-tabs button.active{background:var(--bg-card);color:var(--accent-light);border-color:var(--accent-dark)}----/* Setting rows */--.setting-row{display:flex;align-items:center;gap:12px;padding:10px 0;border-bottom:1px solid var(--border)}--.setting-row label{flex:1}--.setting-row label strong{font-size:13px}--.setting-row label small{font-size:11px;color:var(--text-muted)}----/* Storage bars */--.storage-bar{background:var(--bg-tertiary);border-radius:4px;height:14px;overflow:hidden;position:relative}--.storage-bar-fill{height:100%;border-radius:4px;transition:width .5s}----/* Sensitive fields */--.sensitive-field{display:flex;gap:6px;align-items:center;flex-wrap:wrap}--.sensitive-field input{flex:1}--.sensitive-actions{display:flex;gap:4px}----@media(max-width:800px){-- .page{padding:12px}-- .stats-grid{grid-template-columns:repeat(2,1fr)}-- #analytics-overview > div[style*="grid-template-columns:1fr 1fr"],-- #analytics-service-detail > div[style*="grid-template-columns:1fr 1fr"],-- #analytics-service-detail > div[style*="grid-template-columns:1fr 1fr 1fr"]{display:block!important}-- #analytics-overview > div[style*="grid-template-columns"] > .card,-- #analytics-service-detail > div[style*="grid-template-columns"] > .card{margin-bottom:12px}--}--@media(max-width:500px){-- .stats-grid{grid-template-columns:1fr}-- .admin-table{font-size:11px}-- .section-tabs-shell{gap:6px}-- .tabs-scroll-btn{width:30px;height:30px;flex-basis:30px;border-radius:8px}--}--.ssh-prompt{background:var(--bg-elev,#111);border:1px solid var(--border,#2a2a2a);border-radius:8px;padding:14px;white-space:pre-wrap;word-break:break-word;font-family:ui-monospace,Menlo,Consolas,monospace;font-size:0.8rem;line-height:1.55;color:var(--text,#ddd);max-height:520px;overflow:auto;margin:0}--#sec-ssh code{background:var(--bg-elev,#111);border:1px solid var(--border,#2a2a2a);border-radius:5px;padding:2px 7px;font-family:ui-monospace,Menlo,Consolas,monospace;font-size:0.8rem;display:inline-block;margin-top:3px;word-break:break-all}--#sec-ssh .staff-pill{font-size:0.62rem;font-weight:700;text-transform:uppercase;letter-spacing:.04em;padding:3px 8px;border-radius:999px;vertical-align:middle;margin-left:8px;color:var(--warning,#f0b74a);background:color-mix(in srgb,var(--warning,#f0b74a) 18%,transparent);border:1px solid color-mix(in srgb,var(--warning,#f0b74a) 40%,transparent)}--#sec-ssh .staff-pill.user-pill{color:var(--accent,#6ea8ff);background:color-mix(in srgb,var(--accent,#6ea8ff) 18%,transparent);border-color:color-mix(in srgb,var(--accent,#6ea8ff) 40%,transparent)}--</style>++<link rel="stylesheet" href="/css/admin.css"></head><body>nnew file mode 100644@@@ -0,0 +1,158 @@++++*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}++:root{++ --bg:#0d0d14;--bg-secondary:#151520;--bg-card:#1a1a26;--bg-input:#1c1c28;--bg-tertiary:#222233;++ --border:#2a2a3a;--border-focus:#8b5cf6;++ --accent:#3b82f6;--accent-glow:rgba(139,92,246,0.4);--accent-dark:#1d4ed8;--accent-light:#60a5fa;++ --text:#e0e0e0;--text-secondary:#b0b0b8;--text-muted:#707080;++ --live-red:#ef4444;--success:#22c55e;--warning:#f59e0b;--danger:#ef4444;++ --shadow:0 4px 20px rgba(0,0,0,0.3);++}++html{height:100%;background:var(--bg)}++body{min-height:100%;color:var(--text);font-family:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;padding-bottom:40px}++++/* Layout */++.page{max-width:1200px;margin:0 auto;padding:20px}++.page-title{display:flex;align-items:center;gap:12px;margin-bottom:24px}++.page-title h1{font-size:22px;font-weight:800}++.page-title .badge{font-size:10px;padding:3px 10px;border-radius:4px;font-weight:600;text-transform:uppercase;letter-spacing:.5px;background:rgba(231,76,60,0.15);color:#e74c3c}++++/* Section tabs */++.section-tabs-shell{display:flex;align-items:center;gap:8px;margin-bottom:24px}++.section-tabs-wrap{flex:1;min-width:0;position:relative;display:flex;align-items:center}++.section-tabs{++ display:flex;gap:4px;background:var(--bg-secondary);border-radius:10px;padding:4px;++ overflow-x:auto;-webkit-overflow-scrolling:touch;scrollbar-width:none;flex:1;min-width:0;++}++.section-tabs::-webkit-scrollbar{height:0}++.section-tabs button{++ flex-shrink:0;padding:8px 14px;border:none;border-radius:8px;font-size:11px;font-weight:600;++ background:transparent;color:var(--text-muted);cursor:pointer;transition:all .15s;white-space:nowrap;++}++.section-tabs button.active{background:var(--bg-card);color:var(--accent-light)}++.section-tabs button:hover:not(.active){color:var(--text)}++.tabs-scroll-btn{++ width:34px;height:34px;flex:0 0 34px;border:none;border-radius:10px;display:flex;align-items:center;justify-content:center;++ background:var(--bg-secondary);color:var(--text-muted);cursor:pointer;transition:all .15s;opacity:0;pointer-events:none;++}++.tabs-scroll-btn.visible{opacity:1;pointer-events:auto}++.tabs-scroll-btn:hover{color:var(--accent-light);background:var(--bg-card)}++.tabs-scroll-btn:disabled{opacity:.3;pointer-events:none}++++/* Section */++.section{display:none;animation:sec-in .2s ease}++.section.active{display:block}++@keyframes sec-in{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}++++/* Stats grid */++.stats-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:12px;margin-bottom:24px}++.stat-card{background:var(--bg-card);border:1px solid var(--border);border-radius:10px;padding:16px;text-align:center}++.stat-card .value{font-size:24px;font-weight:800;color:var(--accent-light)}++.stat-card .label{font-size:11px;color:var(--text-muted);margin-top:4px;display:flex;align-items:center;justify-content:center;gap:6px}++++/* Cards */++.card{background:var(--bg-card);border:1px solid var(--border);border-radius:12px;padding:20px;margin-bottom:16px}++.card h3{font-size:14px;font-weight:700;margin-bottom:12px;display:flex;align-items:center;gap:8px}++.card .chart-wrap{position:relative;width:100%;max-height:320px}++.card canvas{max-height:320px}++++/* Tables */++.admin-table{width:100%;border-collapse:collapse;font-size:13px}++.admin-table thead{border-bottom:2px solid var(--border)}++.admin-table th{padding:10px 8px;text-align:left;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.5px;color:var(--text-muted)}++.admin-table td{padding:10px 8px;border-bottom:1px solid var(--border)}++.admin-table tbody tr:hover{background:rgba(139,92,246,0.03)}++++/* Buttons */++.btn{padding:8px 16px;border:none;border-radius:8px;font-size:12px;font-weight:600;cursor:pointer;transition:all .15s;display:inline-flex;align-items:center;gap:6px}++.btn-primary{background:var(--accent);color:var(--on-accent,#fff)}++.btn-primary:hover{background:var(--accent-dark)}++.btn-outline{background:transparent;border:1px solid var(--border);color:var(--text)}++.btn-outline:hover{border-color:var(--accent-dark);color:var(--accent-light)}++.btn-danger{background:rgba(231,76,60,0.1);color:var(--live-red);border:1px solid rgba(231,76,60,0.2)}++.btn-danger:hover{background:rgba(231,76,60,0.2)}++.btn-success{background:rgba(46,204,113,0.1);color:var(--success);border:1px solid rgba(46,204,113,0.2)}++.btn-success:hover{background:rgba(46,204,113,0.2)}++.btn-sm{padding:5px 10px;font-size:11px}++.btn:disabled{opacity:.4;cursor:not-allowed}++++/* Form fields */++.form-field{margin-bottom:14px}++.form-field label{display:block;font-size:11px;font-weight:600;color:var(--text-muted);text-transform:uppercase;letter-spacing:.5px;margin-bottom:5px}++.form-field input,.form-field select,.form-field textarea{++ width:100%;padding:10px 12px;background:var(--bg-input);border:1px solid var(--border);++ border-radius:8px;color:var(--text);font-size:13px;outline:none;transition:border-color .2s;++}++.form-field input:focus,.form-field select:focus,.form-field textarea:focus{border-color:var(--accent)}++++/* Input inline */++.input-row{display:flex;gap:8px;align-items:center;margin-bottom:12px;flex-wrap:wrap}++.input-row input,.input-row select{++ padding:8px 12px;background:var(--bg-input);border:1px solid var(--border);border-radius:8px;++ color:var(--text);font-size:13px;outline:none;flex:1;min-width:160px;++}++.input-row input:focus{border-color:var(--accent)}++++/* Badges */++.badge{display:inline-block;padding:2px 8px;border-radius:4px;font-size:10px;font-weight:600;text-transform:uppercase}++.badge-success{background:rgba(46,204,113,0.15);color:#2ecc71}++.badge-danger{background:rgba(231,76,60,0.15);color:#e74c3c}++.badge-warning{background:rgba(245,158,11,0.15);color:#f59e0b}++.badge-info{background:rgba(52,152,219,0.15);color:#3498db}++.badge-admin{background:rgba(231,76,60,0.15);color:#e74c3c}++.badge-global_mod{background:rgba(155,89,182,0.15);color:#9b59b6}++.badge-streamer{background:rgba(155,89,182,0.15);color:#9b59b6}++.badge-user{background:rgba(52,152,219,0.15);color:#3498db}++++/* Loading */++.muted{color:var(--text-muted);font-size:13px}++.loading{text-align:center;padding:40px;color:var(--text-muted)}++++/* Toast */++.toast-container{position:fixed;top:60px;right:16px;z-index:200000;display:flex;flex-direction:column;gap:8px}++.toast{padding:12px 18px;border-radius:8px;font-size:13px;font-weight:600;color:#fff;animation:toast-in .25s ease;box-shadow:var(--shadow)}++.toast-success{background:#2ecc71}++.toast-error{background:#e74c3c}++.toast-info{background:#3498db}++.toast-warning{background:#f59e0b}++@keyframes toast-in{from{opacity:0;transform:translateX(20px)}to{opacity:1;transform:translateX(0)}}++++/* Sub-tabs */++.sub-tabs{display:flex;gap:4px;margin-bottom:16px;overflow-x:auto;-webkit-overflow-scrolling:touch;flex-wrap:wrap}++.sub-tabs button{padding:6px 12px;border:1px solid var(--border);border-radius:6px;background:transparent;color:var(--text-muted);font-size:11px;font-weight:600;cursor:pointer}++.sub-tabs button.active{background:var(--bg-card);color:var(--accent-light);border-color:var(--accent-dark)}++++/* Setting rows */++.setting-row{display:flex;align-items:center;gap:12px;padding:10px 0;border-bottom:1px solid var(--border)}++.setting-row label{flex:1}++.setting-row label strong{font-size:13px}++.setting-row label small{font-size:11px;color:var(--text-muted)}++++/* Storage bars */++.storage-bar{background:var(--bg-tertiary);border-radius:4px;height:14px;overflow:hidden;position:relative}++.storage-bar-fill{height:100%;border-radius:4px;transition:width .5s}++++/* Sensitive fields */++.sensitive-field{display:flex;gap:6px;align-items:center;flex-wrap:wrap}++.sensitive-field input{flex:1}++.sensitive-actions{display:flex;gap:4px}++++@media(max-width:800px){++ .page{padding:12px}++ .stats-grid{grid-template-columns:repeat(2,1fr)}++ #analytics-overview > div[style*="grid-template-columns:1fr 1fr"],++ #analytics-service-detail > div[style*="grid-template-columns:1fr 1fr"],++ #analytics-service-detail > div[style*="grid-template-columns:1fr 1fr 1fr"]{display:block!important}++ #analytics-overview > div[style*="grid-template-columns"] > .card,++ #analytics-service-detail > div[style*="grid-template-columns"] > .card{margin-bottom:12px}++}++@media(max-width:500px){++ .stats-grid{grid-template-columns:1fr}++ .admin-table{font-size:11px}++ .section-tabs-shell{gap:6px}++ .tabs-scroll-btn{width:30px;height:30px;flex-basis:30px;border-radius:8px}++}++.ssh-prompt{background:var(--bg-elev,#111);border:1px solid var(--border,#2a2a2a);border-radius:8px;padding:14px;white-space:pre-wrap;word-break:break-word;font-family:ui-monospace,Menlo,Consolas,monospace;font-size:0.8rem;line-height:1.55;color:var(--text,#ddd);max-height:520px;overflow:auto;margin:0}++#sec-ssh code{background:var(--bg-elev,#111);border:1px solid var(--border,#2a2a2a);border-radius:5px;padding:2px 7px;font-family:ui-monospace,Menlo,Consolas,monospace;font-size:0.8rem;display:inline-block;margin-top:3px;word-break:break-all}++#sec-ssh .staff-pill{font-size:0.62rem;font-weight:700;text-transform:uppercase;letter-spacing:.04em;padding:3px 8px;border-radius:999px;vertical-align:middle;margin-left:8px;color:var(--warning,#f0b74a);background:color-mix(in srgb,var(--warning,#f0b74a) 18%,transparent);border:1px solid color-mix(in srgb,var(--warning,#f0b74a) 40%,transparent)}++#sec-ssh .staff-pill.user-pill{color:var(--accent,#6ea8ff);background:color-mix(in srgb,var(--accent,#6ea8ff) 18%,transparent);border-color:color-mix(in srgb,var(--accent,#6ea8ff) 40%,transparent)}nnew file mode 100644@@@ -0,0 +1,236 @@++++*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}++:root{++ --bg:#0d0d14;--bg-card:#16161f;--bg-input:#1c1c28;++ --border:#2a2a3a;--border-focus:#8b5cf6;++ --accent:#3b82f6;--accent-glow:rgba(139,92,246,0.4);--accent-dark:#1d4ed8;++ --text:#e0e0e0;--text-muted:#8e8ea0;--text-dim:#505060;++ --live-red:#ef4444;--success:#22c55e;++}++html{min-height:100%;background:var(--bg);color:var(--text);font-family:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;overflow-x:hidden;overflow-y:auto}++body{min-height:100dvh;background:var(--bg);color:var(--text);font-family:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;overflow-x:hidden;overflow-y:auto}++++/* Animated background */++.bg-canvas{position:fixed;inset:0;z-index:0}++.bg-canvas .orb{++ position:absolute;border-radius:50%;filter:blur(100px);opacity:0.15;++ animation:orb-drift 20s ease-in-out infinite alternate;++}++.bg-canvas .orb:nth-child(1){width:400px;height:400px;background:#8b5cf6;top:-100px;left:-100px;animation-delay:0s}++.bg-canvas .orb:nth-child(2){width:500px;height:500px;background:#e74c3c;bottom:-150px;right:-150px;animation-delay:-5s}++.bg-canvas .orb:nth-child(3){width:300px;height:300px;background:#9b59b6;top:40%;left:60%;animation-delay:-10s}++.bg-canvas .orb:nth-child(4){width:250px;height:250px;background:#3498db;bottom:20%;left:10%;animation-delay:-15s}++@keyframes orb-drift{++ 0%{transform:translate(0,0) scale(1)}25%{transform:translate(40px,-30px) scale(1.1)}++ 50%{transform:translate(-20px,50px) scale(0.95)}75%{transform:translate(30px,20px) scale(1.05)}++ 100%{transform:translate(-40px,-40px) scale(1)}++}++++/* Particle field */++.particles{position:fixed;inset:0;z-index:1;pointer-events:none;overflow:hidden}++.particles .p{++ position:absolute;width:2px;height:2px;background:rgba(139,92,246,0.3);border-radius:50%;++ animation:particle-float linear infinite;++}++@keyframes particle-float{++ from{transform:translateY(100vh) scale(0);opacity:0}++ 10%{opacity:1;transform:translateY(90vh) scale(1)}++ 90%{opacity:.6}++ to{transform:translateY(-5vh) scale(0.5);opacity:0}++}++++/* Grid lines */++.grid-overlay{++ position:fixed;inset:0;z-index:0;++ background-image:++ linear-gradient(rgba(139,92,246,0.02) 1px,transparent 1px),++ linear-gradient(90deg,rgba(139,92,246,0.02) 1px,transparent 1px);++ background-size:60px 60px;++ animation:grid-scroll 30s linear infinite;++}++@keyframes grid-scroll{from{background-position:0 0}to{background-position:60px 60px}}++++/* Container */++.login-container{++ position:relative;z-index:10;min-height:100dvh;display:flex;align-items:center;justify-content:center;padding:20px;++}++++/* Card */++.login-card{++ width:100%;max-width:420px;background:var(--bg-card);border:1px solid var(--border);++ border-radius:20px;padding:40px 36px;position:relative;overflow:hidden;++ box-shadow:0 20px 80px rgba(0,0,0,0.5), 0 0 60px rgba(139,92,246,0.03);++ animation:card-entrance .6s cubic-bezier(.34,1.56,.64,1);++}++@keyframes card-entrance{from{transform:scale(.92) translateY(20px);opacity:0}to{transform:scale(1) translateY(0);opacity:1}}++++.login-card::before{++ content:'';position:absolute;top:0;left:0;right:0;height:3px;++ background:linear-gradient(90deg,transparent,var(--accent),transparent);++ animation:shimmer 3s ease-in-out infinite;++}++@keyframes shimmer{0%,100%{opacity:0.3}50%{opacity:1}}++++/* Logo */++.logo{text-align:center;margin-bottom:28px}++.logo .flame{display:block;margin:0 auto 8px;width:72px;height:72px;color:var(--accent-light,var(--accent,#a78bfa))}++@keyframes flame-pulse{0%,100%{transform:scale(1);filter:brightness(1)}50%{transform:scale(1.08);filter:brightness(1.2)}}++.logo h1{font-size:22px;font-weight:800;letter-spacing:-.5px;margin-bottom:4px}++.logo p{font-size:12px;color:var(--text-muted);font-weight:500}++++/* Tabs */++.tab-bar{display:flex;gap:4px;background:var(--bg-input);border-radius:10px;padding:4px;margin-bottom:24px}++.tab-bar button{++ flex:1;padding:8px;border:none;border-radius:8px;font-size:13px;font-weight:600;++ background:transparent;color:var(--text-muted);cursor:pointer;transition:all .2s;++}++.tab-bar button.active{background:var(--accent-dark);color:#fff;box-shadow:0 2px 8px rgba(139,92,246,0.3)}++.tab-bar button:hover:not(.active){color:var(--text)}++++/* Forms */++.form-panel{display:none;animation:panel-in .3s ease}++.form-panel.active{display:block}++@keyframes panel-in{from{transform:translateX(10px);opacity:0}to{transform:translateX(0);opacity:1}}++++.field{margin-bottom:16px;position:relative}++.field label{display:block;font-size:11px;font-weight:600;color:var(--text-muted);text-transform:uppercase;letter-spacing:.5px;margin-bottom:6px}++.field input{++ width:100%;padding:12px 14px;background:var(--bg-input);border:1px solid var(--border);++ border-radius:10px;color:var(--text);font-size:14px;outline:none;++ transition:border-color .2s, box-shadow .2s;++}++.field input:focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-glow)}++.field input::placeholder{color:var(--text-dim)}++++.field .toggle-password{++ position:absolute;right:12px;top:30px;background:none;border:none;++ color:var(--text-muted);cursor:pointer;font-size:14px;padding:4px;++}++++.submit-btn{++ width:100%;padding:14px;border:none;border-radius:10px;font-size:14px;font-weight:700;++ background:linear-gradient(135deg,var(--accent),var(--accent-dark));color:#fff;cursor:pointer;++ transition:all .2s;position:relative;overflow:hidden;++}++.submit-btn:hover{transform:translateY(-1px);box-shadow:0 6px 20px rgba(139,92,246,0.35)}++.submit-btn:active{transform:translateY(0)}++.submit-btn.loading{pointer-events:none;opacity:.7}++.submit-btn .ripple{++ position:absolute;border-radius:50%;background:rgba(255,255,255,0.3);++ transform:scale(0);animation:btn-ripple .6s ease-out;++}++@keyframes btn-ripple{to{transform:scale(4);opacity:0}}++++/* Messages */++.message{padding:10px 14px;border-radius:8px;font-size:12px;font-weight:500;margin-bottom:16px;display:none;animation:msg-in .3s ease}++.message.visible{display:block}++.message.error{background:rgba(231,76,60,0.1);border:1px solid rgba(231,76,60,0.3);color:#e74c3c}++.message.success{background:rgba(46,204,113,0.1);border:1px solid rgba(46,204,113,0.3);color:#2ecc71}++@keyframes msg-in{from{transform:translateY(-4px);opacity:0}to{transform:translateY(0);opacity:1}}++++/* Extras */++.divider{text-align:center;position:relative;margin:24px 0}++.divider::before{content:'';position:absolute;top:50%;left:0;right:0;height:1px;background:var(--border)}++.divider span{position:relative;background:var(--bg-card);padding:0 12px;font-size:11px;color:var(--text-dim);font-weight:500}++++.anon-btn{++ width:100%;padding:12px;border:1px dashed var(--border);border-radius:10px;++ background:transparent;color:var(--text-muted);font-size:13px;font-weight:500;++ cursor:pointer;transition:all .2s;++}++.anon-btn:hover{border-color:var(--accent-dark);color:var(--text);background:rgba(139,92,246,0.04)}++++.services{text-align:center;margin-top:24px;padding-top:16px;border-top:1px solid var(--border)}++.services p{font-size:11px;color:var(--text-dim);margin-bottom:10px}++.services .icons{display:flex;justify-content:center;gap:10px}++.services .icons span{font-size:16px;opacity:0.4;transition:opacity .2s}++.services .icons span:hover{opacity:0.8}++.inline-link{display:inline-flex;align-items:center;gap:6px;margin-top:12px;font-size:12px;color:var(--accent);cursor:pointer;text-decoration:none;font-weight:600}++.inline-link:hover{color:#a78bfa}++.panel-note{font-size:12px;color:var(--text-muted);line-height:1.5;margin-bottom:16px}++++/* Toast */++.login-toast{++ position:fixed;top:20px;right:20px;z-index:100;padding:14px 20px;++ border-radius:10px;font-size:13px;font-weight:500;++ transform:translateX(120%);transition:transform .3s cubic-bezier(.34,1.56,.64,1);++}++.login-toast.show{transform:translateX(0)}++.login-toast.error{background:rgba(231,76,60,0.9);color:#fff}++.login-toast.success{background:rgba(46,204,113,0.9);color:#fff}++++/* ── Account Chooser ───────────────────────────────────── */++.chooser-subtitle{font-size:13px;color:var(--text-muted);text-align:center;margin-bottom:20px;line-height:1.5}++.chooser-subtitle .app-name{color:var(--accent);font-weight:700}++.oauth-consent{margin:0 0 20px;padding:14px 16px;border:1px solid var(--border);border-radius:12px;background:var(--bg-input);font-size:13px;line-height:1.5;text-align:left}++.oauth-consent .consent-head{margin:0 0 10px;color:var(--text)}++.oauth-consent .consent-head b{color:var(--accent)}++.oauth-consent .consent-host{color:var(--text-muted)}++.oauth-consent ul{list-style:none;margin:0;padding:0;display:grid;gap:8px}++.oauth-consent li{padding:8px 10px;border-left:3px solid var(--border);border-radius:6px;background:rgba(255,255,255,0.02)}++.oauth-consent li.sensitive{border-left-color:#f59e0b}++.oauth-consent .consent-name{font-weight:600;color:var(--text)}++.oauth-consent .consent-badge{display:inline-block;margin-left:6px;padding:1px 7px;border-radius:999px;font-size:11px;font-weight:700;color:#1a1204;background:#f59e0b;vertical-align:1px}++.oauth-consent small{display:block;margin-top:2px;color:var(--text-muted);font-size:12px}++.oauth-consent .consent-none,.oauth-consent .consent-refused{margin:0;color:var(--text-muted)}++.oauth-consent .consent-refused{margin-top:10px;font-size:12px}++.oauth-consent code{font-size:11.5px}++++.account-list{display:flex;flex-direction:column;gap:8px;margin-bottom:16px}++.account-item{++ display:flex;align-items:center;gap:12px;padding:12px 14px;++ background:var(--bg-input);border:1px solid var(--border);border-radius:12px;++ cursor:pointer;transition:all .2s;position:relative;++}++.account-item:hover{border-color:var(--accent);background:rgba(139,92,246,0.06);transform:translateX(2px)}++.account-item.active-account{border-color:var(--accent);box-shadow:0 0 0 2px var(--accent-glow)}++.account-item .avatar{++ width:40px;height:40px;border-radius:50%;background:var(--border);++ display:flex;align-items:center;justify-content:center;font-size:18px;++ flex-shrink:0;overflow:hidden;++}++.account-item .avatar img{width:100%;height:100%;object-fit:cover}++.account-item .info{flex:1;min-width:0}++.account-item .info .name{font-size:14px;font-weight:600;color:var(--text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}++.account-item .info .detail{font-size:11px;color:var(--text-muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}++.account-item .arrow{color:var(--text-dim);font-size:14px;flex-shrink:0;transition:color .2s}++.account-item:hover .arrow{color:var(--accent)}++.account-item .remove-btn{++ position:absolute;top:6px;right:6px;width:20px;height:20px;border:none;++ background:rgba(231,76,60,0.15);border-radius:50%;color:#e74c3c;font-size:10px;++ cursor:pointer;display:none;align-items:center;justify-content:center;++ transition:background .2s;line-height:1;++}++.account-item:hover .remove-btn{display:flex}++.account-item .remove-btn:hover{background:rgba(231,76,60,0.3)}++++.chooser-actions{display:flex;flex-direction:column;gap:8px}++.chooser-actions .action-btn{++ display:flex;align-items:center;justify-content:center;gap:8px;padding:12px;++ border:1px dashed var(--border);border-radius:10px;background:transparent;++ color:var(--text-muted);font-size:13px;font-weight:500;cursor:pointer;transition:all .2s;++}++.chooser-actions .action-btn:hover{border-color:var(--accent-dark);color:var(--text);background:rgba(139,92,246,0.04)}++++.back-link{++ display:block;text-align:center;margin-top:16px;font-size:12px;++ color:var(--text-muted);cursor:pointer;text-decoration:none;transition:color .2s;++}++.back-link:hover{color:var(--accent)}++++@media(max-width:480px){++ .login-card{padding:28px 24px;border-radius:16px}++ .logo .flame{width:56px;height:56px}++ .logo .flame .ov-mark{width:56px!important;height:56px!important}++ .logo h1{font-size:18px}++ .login-container{padding:16px}++}++++@media(max-height:820px){++ .login-container{align-items:flex-start;padding-top:24px;padding-bottom:24px}++}++++@media(max-width:380px){++ .login-card{padding:24px 18px;border-radius:14px}++ .tab-bar button{font-size:12px;padding:8px 6px}++}nnew file mode 100644@@@ -0,0 +1,248 @@++++*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}++:root{++ --bg:#0d0d14;--bg-secondary:#151520;--bg-card:#1a1a26;--bg-input:#1c1c28;++ --border:#2a2a3a;--border-focus:#8b5cf6;++ --accent:#3b82f6;--accent-glow:rgba(139,92,246,0.4);--accent-dark:#1d4ed8;--accent-light:#60a5fa;++ --text:#e0e0e0;--text-secondary:#b0b0b8;--text-muted:#707080;++ --live-red:#ef4444;--success:#22c55e;++ --shadow:0 4px 20px rgba(0,0,0,0.3);++}++html{height:100%;background:var(--bg)}++body{min-height:100%;color:var(--text);font-family:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;padding-bottom:40px}++++/* Layout */++.page{max-width:900px;margin:0 auto;padding:20px}++.page-header{display:flex;align-items:center;gap:16px;margin-bottom:32px}++.page-header .avatar{width:72px;height:72px;border-radius:50%;border:3px solid var(--accent);object-fit:cover}++.page-header .info{flex:1}++.page-header .info h1{font-size:22px;font-weight:800;margin-bottom:2px}++.page-header .info .subtitle{font-size:13px;color:var(--text-muted)}++.page-header .info .badges{display:flex;gap:6px;margin-top:6px}++.page-header .info .badges .badge{++ font-size:10px;padding:3px 10px;border-radius:4px;font-weight:600;text-transform:uppercase;letter-spacing:.5px;++}++.badge-admin{background:rgba(231,76,60,0.15);color:#e74c3c}++.badge-anon{background:rgba(139,92,246,0.15);color:var(--accent-light)}++.badge-streamer{background:rgba(155,89,182,0.15);color:#9b59b6}++.badge-user{background:rgba(52,152,219,0.15);color:#3498db}++++/* Section tabs */++.section-tabs{display:flex;gap:4px;margin-bottom:24px;background:var(--bg-secondary);border-radius:10px;padding:4px;overflow-x:auto}++.section-tabs button{++ flex-shrink:0;padding:8px 16px;border:none;border-radius:8px;font-size:12px;font-weight:600;++ background:transparent;color:var(--text-muted);cursor:pointer;transition:all .15s;white-space:nowrap;++}++.section-tabs button.active{background:var(--bg-card);color:var(--accent-light)}++.section-tabs button:hover:not(.active){color:var(--text)}++++/* Approvals (my/approvals.js): one row per waiting request, and the count on its tab */++.section-tabs .tab-badge{display:inline-block;min-width:18px;padding:0 6px;border-radius:9px;background:var(--accent);color:#fff;font-size:11px;line-height:18px;text-align:center}++.approval{border:1px solid var(--border);border-radius:10px;padding:14px 16px;margin-bottom:10px;background:var(--bg-secondary)}++.approval-summary{font-weight:600;color:var(--text);margin-bottom:6px;overflow-wrap:anywhere}++.approval-meta{font-size:12px;color:var(--text-secondary);margin-bottom:10px;overflow-wrap:anywhere}++.approval-who{color:var(--text);font-weight:600}++.approval-cap{font-size:11px;padding:1px 6px;border-radius:5px;background:var(--bg-input);border:1px solid var(--border)}++.approval-resources{list-style:none;display:flex;flex-wrap:wrap;gap:6px;margin:0 0 10px;padding:0}++.approval-resources li{font-size:11px;padding:2px 8px;border-radius:6px;background:var(--bg-input);color:var(--text-secondary);overflow-wrap:anywhere}++.approval .btn-row{display:flex;gap:8px;flex-wrap:wrap}++.approvals-empty{color:var(--text-secondary);font-size:13px}++++/* Section */++.section{display:none;animation:sec-in .2s ease}++.section.active{display:block}++@keyframes sec-in{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}++++/* Cards */++.card{background:var(--bg-card);border:1px solid var(--border);border-radius:12px;padding:20px;margin-bottom:16px}++.card h3{font-size:14px;font-weight:700;margin-bottom:12px;display:flex;align-items:center;gap:8px}++.card h3 .icon{font-size:16px}++.card p{font-size:13px;color:var(--text-secondary);line-height:1.6}++++/* Form fields */++.form-field{margin-bottom:14px}++.form-field label{display:block;font-size:11px;font-weight:600;color:var(--text-muted);text-transform:uppercase;letter-spacing:.5px;margin-bottom:5px}++/* Every text field on the page follows the active theme: its input colour, a hover tint and an accent focus ring. */++:where(input:not([type]),input[type=text],input[type=url],input[type=email],input[type=search],input[type=password],input[type=number],select,textarea){++ padding:10px 12px;background:var(--bg-input);border:1px solid var(--border);border-radius:10px;++ color:var(--text);font:inherit;font-size:13px;outline:none;min-height:40px;++ transition:border-color .18s ease,box-shadow .18s ease,background-color .18s ease;++}++:where(input:not([type]),input[type=text],input[type=url],input[type=email],input[type=search],input[type=password],input[type=number],select,textarea):hover{border-color:color-mix(in srgb,var(--accent) 40%,var(--border))}++:where(input:not([type]),input[type=text],input[type=url],input[type=email],input[type=search],input[type=password],input[type=number],select,textarea):focus{++ border-color:var(--accent);box-shadow:0 0 0 3px color-mix(in srgb,var(--accent) 24%,transparent);++ background:color-mix(in srgb,var(--accent) 5%,var(--bg-input));++}++:where(input,textarea)::placeholder{color:var(--text-muted);opacity:1}++:where(input,select,textarea):user-invalid{border-color:var(--danger,#ef4444)}++.form-field input,.form-field select,.form-field textarea{width:100%}++.form-field textarea{resize:vertical;min-height:60px}++.form-field .hint{font-size:10px;color:var(--text-muted);margin-top:3px}++.theme-submit{display:grid;grid-template-columns:repeat(2,minmax(0,1fr)) auto;gap:12px;align-items:start;margin-top:16px}++.theme-submit .form-field{margin:0}++.theme-submit .btn{margin-top:21px;min-height:40px}++@media (max-width:640px){.theme-submit{grid-template-columns:1fr}.theme-submit .btn{margin-top:0}}++++.btn{++ padding:8px 20px;border:none;border-radius:8px;font-size:12px;font-weight:600;cursor:pointer;transition:all .15s;++}++.btn-primary{background:var(--accent);color:var(--on-accent,#fff)}++.btn-primary:hover{background:var(--accent-dark)}++.btn-outline{background:transparent;border:1px solid var(--border);color:var(--text)}++.btn-outline:hover{border-color:var(--accent-dark);color:var(--accent-light)}++.btn-danger{background:rgba(231,76,60,0.1);color:var(--live-red);border:1px solid rgba(231,76,60,0.2)}++.btn-danger:hover{background:rgba(231,76,60,0.2)}++.btn-row{display:flex;gap:8px;margin-top:12px}++.data-check{display:flex;gap:8px;align-items:flex-start;font-size:13px;color:var(--text-secondary);line-height:1.5}++.data-check input{margin-top:3px}++.data-status{align-self:center;font-size:12px;color:var(--text-secondary)}++.module-row{display:flex;gap:12px;align-items:flex-start;padding:10px 0;border-top:1px solid var(--border)}++.module-row:first-child{border-top:0}++.module-row-main{flex:1;min-width:0}++.module-row-main strong{display:block;font-size:13px}++.module-row-main small{display:block;font-size:12px;color:var(--text-muted);line-height:1.5}++.module-row-main code{display:block;margin-top:4px;font-size:11px;color:var(--text-secondary);white-space:pre-wrap;word-break:break-word;max-height:7.5em;overflow:auto}++++/* Account list */++.account-list{display:flex;flex-direction:column;gap:8px}++.account-item{++ display:flex;align-items:center;gap:12px;padding:12px;border-radius:10px;++ background:var(--bg-input);border:1px solid var(--border);++}++.account-item.active{border-color:var(--accent)}++.account-item img{width:36px;height:36px;border-radius:50%}++.account-item .name{font-size:13px;font-weight:600}++.account-item .meta{font-size:11px;color:var(--text-muted)}++.account-item .actions{margin-left:auto;display:flex;gap:6px}++++/* Notification pref grid */++.notif-grid{display:grid;grid-template-columns:1fr auto auto auto auto;gap:8px;align-items:center;font-size:12px}++.notif-grid .cat{font-weight:600;text-transform:capitalize}++.notif-grid .toggle{width:36px;height:20px;border-radius:10px;border:none;cursor:pointer;position:relative;transition:background .2s}++.notif-grid .toggle::after{content:'';position:absolute;width:16px;height:16px;border-radius:50%;background:#fff;top:2px;left:2px;transition:transform .2s}++.notif-grid .toggle.on{background:var(--accent)}++.notif-grid .toggle.on::after{transform:translateX(16px)}++.notif-grid .toggle.off{background:var(--border)}++++/* Linked services */++.linked-item{display:flex;align-items:center;gap:12px;padding:12px;border-radius:8px;background:var(--bg-input);margin-bottom:8px;text-decoration:none;color:inherit;transition:transform .15s,background .15s}++a.linked-item:hover{background:var(--bg-hover,var(--bg-card));transform:translateX(2px)}++.linked-item .svc-meta{margin-left:auto;text-align:right;font-size:11px;color:var(--text-muted);white-space:nowrap}++.linked-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:8px;margin-top:14px}++.linked-grid a{display:flex;align-items:center;gap:8px;padding:10px 12px;border-radius:8px;border:1px dashed var(--border);font-size:12px;color:var(--text-secondary);text-decoration:none;transition:border-color .15s,color .15s}++.linked-grid a:hover{border-color:var(--accent);color:var(--text)}++.hist-toolbar{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin-bottom:12px}++.hist-chips{display:flex;flex-wrap:wrap;gap:6px}++.hist-chips button{padding:5px 10px;border-radius:999px;border:1px solid var(--border);background:transparent;color:var(--text-secondary);font-size:12px;cursor:pointer;transition:all .15s}++.hist-chips button.active,.hist-chips button:hover{border-color:var(--accent);color:var(--accent-light);background:rgba(var(--accent-rgb,139,92,246),.08)}++.hist-search{flex:1;min-width:160px;padding:7px 10px;border-radius:8px;border:1px solid var(--border);background:var(--bg-input);color:var(--text);font-size:13px}++.hist-pause{display:inline-flex;align-items:center;gap:6px;font-size:12px;color:var(--text-secondary);cursor:pointer}++.btn-small{padding:6px 10px;font-size:12px}++.hist-day{font-size:11px;font-weight:700;letter-spacing:.6px;text-transform:uppercase;color:var(--text-muted);margin:14px 0 6px}++.hist-item{display:flex;align-items:center;gap:12px;padding:9px 10px;border-radius:8px;text-decoration:none;color:inherit;transition:background .12s;animation:sec-in .25s ease both}++.hist-item:hover{background:var(--bg-input)}++.hist-item .hi{width:34px;height:34px;border-radius:9px;display:grid;place-items:center;background:rgba(var(--accent-rgb,139,92,246),.12);color:var(--accent-light);flex:none}++.hist-item .ht{font-size:13px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}++.hist-item .hs{font-size:11px;color:var(--text-muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}++.hist-item .hx{margin-left:auto;display:flex;align-items:center;gap:10px;flex:none;font-size:11px;color:var(--text-muted)}++.hist-item .hx button{background:none;border:none;color:var(--text-muted);cursor:pointer;padding:4px;opacity:0;transition:opacity .15s}++.hist-item:hover .hx button{opacity:1}++.hist-item .hx button:hover{color:var(--danger,#ef4444)}++.hist-empty{padding:26px;text-align:center;color:var(--text-muted);font-size:13px}++.linked-item .svc-icon{font-size:20px;width:32px;text-align:center}++.linked-item .svc-name{font-size:13px;font-weight:600}++.linked-item .svc-user{font-size:11px;color:var(--text-muted)}++.linked-item .svc-status{margin-left:auto;font-size:11px;font-weight:600;color:var(--success)}++++/* Themes */++.theme-hero{display:flex;justify-content:space-between;gap:16px;align-items:flex-start;flex-wrap:wrap;margin-bottom:18px}++.theme-hero-copy{max-width:560px}++.theme-hero-copy p{margin-top:8px}++.theme-current{display:flex;align-items:center;gap:10px;padding:12px 14px;border-radius:10px;background:var(--bg-input);border:1px solid var(--border);min-width:220px}++.theme-current-swatch{display:flex;gap:4px}++.theme-current-swatch span,.theme-preview-swatch span{width:18px;height:18px;border-radius:50%;border:1px solid rgba(255,255,255,.08);display:inline-block}++.theme-current .label{font-size:10px;color:var(--text-muted);text-transform:uppercase;letter-spacing:.08em}++.theme-current .name{font-size:13px;font-weight:700;margin-top:2px}++.theme-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:12px}++.theme-custom-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:10px}++.theme-custom-row{display:flex;align-items:center;gap:8px;font-size:13px;color:var(--text-secondary);background:var(--bg-input);border:1px solid var(--border);border-radius:10px;padding:8px 10px;cursor:pointer}++.theme-custom-row input[type=color]{width:34px;height:34px;border:none;border-radius:8px;background:none;cursor:pointer;padding:0}++.theme-custom-row input[type=color]::-webkit-color-swatch{border:1px solid var(--border);border-radius:6px}++.theme-custom-row input[type=color]::-webkit-color-swatch-wrapper{padding:0}++.theme-card{background:var(--bg-input);border:1px solid var(--border);border-radius:14px;padding:14px;display:flex;flex-direction:column;gap:12px;transition:border-color .15s,transform .15s,box-shadow .15s}++.theme-card:hover{transform:translateY(-2px);border-color:var(--accent)}++.theme-card.active{border-color:var(--accent);box-shadow:0 0 0 1px rgba(139,92,246,.25) inset}++.theme-preview{border-radius:12px;padding:14px;background:linear-gradient(135deg,var(--theme-bg-1,#202331),var(--theme-bg-2,#181a24));border:1px solid rgba(255,255,255,.06)}++.theme-preview-top{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:12px}++.theme-preview-chip{padding:5px 9px;border-radius:999px;background:rgba(255,255,255,.08);color:#fff;font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.08em}++.theme-preview-bars{display:grid;gap:7px}++.theme-preview-bars span{display:block;height:8px;border-radius:999px;background:rgba(255,255,255,.16)}++.theme-preview-bars span:first-child{width:72%}++.theme-preview-bars span:last-child{width:46%}++.theme-card h4{font-size:14px;font-weight:700}++.theme-card p{font-size:12px;color:var(--text-secondary);line-height:1.55}++.theme-card-footer{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-top:auto}++.theme-badges{display:flex;gap:6px;flex-wrap:wrap}++.theme-badge{font-size:10px;padding:4px 8px;border-radius:999px;background:rgba(255,255,255,.05);color:var(--text-muted);text-transform:uppercase;letter-spacing:.08em;font-weight:700}++.theme-empty{padding:16px;border-radius:12px;background:var(--bg-input);border:1px dashed var(--border);text-align:center;color:var(--text-muted);font-size:13px}++++/* Anon info card */++.anon-card{text-align:center;padding:32px 20px}++.anon-card .number{font-size:48px;font-weight:800;color:var(--accent);margin-bottom:8px}++.anon-card .label{font-size:12px;color:var(--text-muted);margin-bottom:16px}++.anon-card .stats{display:flex;justify-content:center;gap:24px}++.anon-card .stat{text-align:center}++.anon-card .stat .val{font-size:20px;font-weight:700}++.anon-card .stat .key{font-size:10px;color:var(--text-muted);text-transform:uppercase}++++/* Loading */++.loading-spinner{text-align:center;padding:40px;color:var(--text-muted);font-size:13px}++.acct-retry-btn{display:inline-block;background:var(--accent,#8b5cf6);color:#0b0d10;border:none;border-radius:6px;padding:3px 12px;font-weight:600;font-size:0.85em;cursor:pointer;vertical-align:baseline}++.acct-retry-btn:hover{filter:brightness(1.08)}++++@media(max-width:600px){++ .page{padding:12px}++ .page-header{flex-direction:column;text-align:center}++ .page-header .info .badges{justify-content:center}++ .notif-grid{grid-template-columns:1fr auto auto}++ .notif-grid .col-email,.notif-grid .col-toast{display:none}++}++++/* Dashboard Showcase */++@keyframes float { 0%, 100% { transform: translateY(0px); } 50% { transform: translateY(-10px); } }++@keyframes fadeInScale { from { opacity: 0; transform: scale(0.95); } to { opacity: 1; transform: scale(1); } }++.dashboard-showcase { margin: 32px 0; padding: 28px; border-radius: 12px; background: var(--bg-card); border: 1px solid var(--border); }++.showcase-header { text-align: center; margin-bottom: 24px; }++.showcase-header h2 { font-size: 1.3rem; font-weight: 800; margin-bottom: 6px; }++.showcase-header p { font-size: 12px; color: var(--text-muted); }++.showcase-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; margin-bottom: 20px; }++.showcase-card { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 16px; border-radius: 10px; background: var(--bg-input); border: 1px solid var(--border); cursor: pointer; text-decoration: none; color: inherit; gap: 8px; animation: float ease-in-out infinite; transition: all 0.3s ease; }++.showcase-card:nth-child(1) { animation-delay: 0s; animation-duration: 3s; }++.showcase-card:nth-child(2) { animation-delay: 0.15s; animation-duration: 3.3s; }++.showcase-card:nth-child(3) { animation-delay: 0.3s; animation-duration: 3.6s; }++.showcase-card:nth-child(4) { animation-delay: 0.45s; animation-duration: 3.2s; }++.showcase-card:hover { transform: translateY(-4px); border-color: var(--accent); background: rgba(139,92,246,0.08); }++.showcase-card-icon { font-size: 1.4rem; color: var(--accent-light); }++.showcase-card-label { font-size: 11px; font-weight: 700; text-align: center; }++.quick-access { display: grid; grid-template-columns: repeat(6, 1fr); gap: 8px; }++.quick-link { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 10px; border-radius: 8px; background: var(--bg); border: 1px solid var(--border); text-decoration: none; color: var(--text-secondary); font-size: 10px; font-weight: 600; text-align: center; transition: all 0.25s ease; animation: fadeInScale 0.5s ease both; }++.quick-link i { font-size: 1rem; margin-bottom: 4px; }++.quick-link:nth-child(1) { animation-delay: 0.1s; }++.quick-link:nth-child(2) { animation-delay: 0.15s; }++.quick-link:nth-child(3) { animation-delay: 0.2s; }++.quick-link:nth-child(4) { animation-delay: 0.25s; }++.quick-link:nth-child(5) { animation-delay: 0.3s; }++.quick-link:nth-child(6) { animation-delay: 0.35s; }++.quick-link:hover { border-color: var(--accent); color: var(--accent-light); background: rgba(139,92,246,0.08); }++@media(max-width:600px){ .showcase-grid { grid-template-columns: repeat(2, 1fr); } .quick-access { grid-template-columns: repeat(3, 1fr); } }++.recent-tools { margin-top: 14px; }++.recent-tools h3 { font-size: 12px; font-weight: 700; color: var(--text-secondary); margin: 0 0 8px; display: flex; gap: 6px; align-items: center; }++.recent-tools-list { display: flex; flex-wrap: wrap; gap: 6px; }++.recent-tool { padding: 6px 10px; border-radius: 999px; background: var(--bg); border: 1px solid var(--border); color: var(--text-secondary); font-size: 12px; font-weight: 600; text-decoration: none; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }++.recent-tool:hover { border-color: var(--accent); color: var(--accent-light); }++.recent-tools-status { margin: 6px 0 0; font-size: 12px; color: var(--text-secondary); }++.recent-tools-status:empty { display: none; }++.recent-tool-item { display: inline-flex; align-items: center; gap: 2px; max-width: 100%; }++.recent-tool-star { border: 0; background: none; color: var(--text-secondary); cursor: pointer; padding: 6px; font-size: 12px; border-radius: 999px; }++.recent-tool-star[aria-pressed="true"] { color: var(--accent-light); }++.recent-tool-star:hover, .recent-tool-star:focus-visible { color: var(--accent-light); }@@@ -12,243 +12,7 @@<link rel="manifest" href="/manifest.webmanifest"><meta name="description" content="Sign in to OpenVibe — one account across OpenVibe.Live, OpenVibe.Tools, OpenVibe.Games, and OpenVibe.Media."><link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">--<style>--*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}--:root{-- --bg:#0d0d14;--bg-card:#16161f;--bg-input:#1c1c28;-- --border:#2a2a3a;--border-focus:#8b5cf6;-- --accent:#3b82f6;--accent-glow:rgba(139,92,246,0.4);--accent-dark:#1d4ed8;-- --text:#e0e0e0;--text-muted:#8e8ea0;--text-dim:#505060;-- --live-red:#ef4444;--success:#22c55e;--}--html{min-height:100%;background:var(--bg);color:var(--text);font-family:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;overflow-x:hidden;overflow-y:auto}--body{min-height:100dvh;background:var(--bg);color:var(--text);font-family:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;overflow-x:hidden;overflow-y:auto}----/* Animated background */--.bg-canvas{position:fixed;inset:0;z-index:0}--.bg-canvas .orb{-- position:absolute;border-radius:50%;filter:blur(100px);opacity:0.15;-- animation:orb-drift 20s ease-in-out infinite alternate;--}--.bg-canvas .orb:nth-child(1){width:400px;height:400px;background:#8b5cf6;top:-100px;left:-100px;animation-delay:0s}--.bg-canvas .orb:nth-child(2){width:500px;height:500px;background:#e74c3c;bottom:-150px;right:-150px;animation-delay:-5s}--.bg-canvas .orb:nth-child(3){width:300px;height:300px;background:#9b59b6;top:40%;left:60%;animation-delay:-10s}--.bg-canvas .orb:nth-child(4){width:250px;height:250px;background:#3498db;bottom:20%;left:10%;animation-delay:-15s}--@keyframes orb-drift{-- 0%{transform:translate(0,0) scale(1)}25%{transform:translate(40px,-30px) scale(1.1)}-- 50%{transform:translate(-20px,50px) scale(0.95)}75%{transform:translate(30px,20px) scale(1.05)}-- 100%{transform:translate(-40px,-40px) scale(1)}--}----/* Particle field */--.particles{position:fixed;inset:0;z-index:1;pointer-events:none;overflow:hidden}--.particles .p{-- position:absolute;width:2px;height:2px;background:rgba(139,92,246,0.3);border-radius:50%;-- animation:particle-float linear infinite;--}--@keyframes particle-float{-- from{transform:translateY(100vh) scale(0);opacity:0}-- 10%{opacity:1;transform:translateY(90vh) scale(1)}-- 90%{opacity:.6}-- to{transform:translateY(-5vh) scale(0.5);opacity:0}--}----/* Grid lines */--.grid-overlay{-- position:fixed;inset:0;z-index:0;-- background-image:-- linear-gradient(rgba(139,92,246,0.02) 1px,transparent 1px),-- linear-gradient(90deg,rgba(139,92,246,0.02) 1px,transparent 1px);-- background-size:60px 60px;-- animation:grid-scroll 30s linear infinite;--}--@keyframes grid-scroll{from{background-position:0 0}to{background-position:60px 60px}}----/* Container */--.login-container{-- position:relative;z-index:10;min-height:100dvh;display:flex;align-items:center;justify-content:center;padding:20px;--}----/* Card */--.login-card{-- width:100%;max-width:420px;background:var(--bg-card);border:1px solid var(--border);-- border-radius:20px;padding:40px 36px;position:relative;overflow:hidden;-- box-shadow:0 20px 80px rgba(0,0,0,0.5), 0 0 60px rgba(139,92,246,0.03);-- animation:card-entrance .6s cubic-bezier(.34,1.56,.64,1);--}--@keyframes card-entrance{from{transform:scale(.92) translateY(20px);opacity:0}to{transform:scale(1) translateY(0);opacity:1}}----.login-card::before{-- content:'';position:absolute;top:0;left:0;right:0;height:3px;-- background:linear-gradient(90deg,transparent,var(--accent),transparent);-- animation:shimmer 3s ease-in-out infinite;--}--@keyframes shimmer{0%,100%{opacity:0.3}50%{opacity:1}}----/* Logo */--.logo{text-align:center;margin-bottom:28px}--.logo .flame{display:block;margin:0 auto 8px;width:72px;height:72px;color:var(--accent-light,var(--accent,#a78bfa))}--@keyframes flame-pulse{0%,100%{transform:scale(1);filter:brightness(1)}50%{transform:scale(1.08);filter:brightness(1.2)}}--.logo h1{font-size:22px;font-weight:800;letter-spacing:-.5px;margin-bottom:4px}--.logo p{font-size:12px;color:var(--text-muted);font-weight:500}----/* Tabs */--.tab-bar{display:flex;gap:4px;background:var(--bg-input);border-radius:10px;padding:4px;margin-bottom:24px}--.tab-bar button{-- flex:1;padding:8px;border:none;border-radius:8px;font-size:13px;font-weight:600;-- background:transparent;color:var(--text-muted);cursor:pointer;transition:all .2s;--}--.tab-bar button.active{background:var(--accent-dark);color:#fff;box-shadow:0 2px 8px rgba(139,92,246,0.3)}--.tab-bar button:hover:not(.active){color:var(--text)}----/* Forms */--.form-panel{display:none;animation:panel-in .3s ease}--.form-panel.active{display:block}--@keyframes panel-in{from{transform:translateX(10px);opacity:0}to{transform:translateX(0);opacity:1}}----.field{margin-bottom:16px;position:relative}--.field label{display:block;font-size:11px;font-weight:600;color:var(--text-muted);text-transform:uppercase;letter-spacing:.5px;margin-bottom:6px}--.field input{-- width:100%;padding:12px 14px;background:var(--bg-input);border:1px solid var(--border);-- border-radius:10px;color:var(--text);font-size:14px;outline:none;-- transition:border-color .2s, box-shadow .2s;--}--.field input:focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-glow)}--.field input::placeholder{color:var(--text-dim)}----.field .toggle-password{-- position:absolute;right:12px;top:30px;background:none;border:none;-- color:var(--text-muted);cursor:pointer;font-size:14px;padding:4px;--}----.submit-btn{-- width:100%;padding:14px;border:none;border-radius:10px;font-size:14px;font-weight:700;-- background:linear-gradient(135deg,var(--accent),var(--accent-dark));color:#fff;cursor:pointer;-- transition:all .2s;position:relative;overflow:hidden;--}--.submit-btn:hover{transform:translateY(-1px);box-shadow:0 6px 20px rgba(139,92,246,0.35)}--.submit-btn:active{transform:translateY(0)}--.submit-btn.loading{pointer-events:none;opacity:.7}--.submit-btn .ripple{-- position:absolute;border-radius:50%;background:rgba(255,255,255,0.3);-- transform:scale(0);animation:btn-ripple .6s ease-out;--}--@keyframes btn-ripple{to{transform:scale(4);opacity:0}}----/* Messages */--.message{padding:10px 14px;border-radius:8px;font-size:12px;font-weight:500;margin-bottom:16px;display:none;animation:msg-in .3s ease}--.message.visible{display:block}--.message.error{background:rgba(231,76,60,0.1);border:1px solid rgba(231,76,60,0.3);color:#e74c3c}--.message.success{background:rgba(46,204,113,0.1);border:1px solid rgba(46,204,113,0.3);color:#2ecc71}--@keyframes msg-in{from{transform:translateY(-4px);opacity:0}to{transform:translateY(0);opacity:1}}----/* Extras */--.divider{text-align:center;position:relative;margin:24px 0}--.divider::before{content:'';position:absolute;top:50%;left:0;right:0;height:1px;background:var(--border)}--.divider span{position:relative;background:var(--bg-card);padding:0 12px;font-size:11px;color:var(--text-dim);font-weight:500}----.anon-btn{-- width:100%;padding:12px;border:1px dashed var(--border);border-radius:10px;-- background:transparent;color:var(--text-muted);font-size:13px;font-weight:500;-- cursor:pointer;transition:all .2s;--}--.anon-btn:hover{border-color:var(--accent-dark);color:var(--text);background:rgba(139,92,246,0.04)}----.services{text-align:center;margin-top:24px;padding-top:16px;border-top:1px solid var(--border)}--.services p{font-size:11px;color:var(--text-dim);margin-bottom:10px}--.services .icons{display:flex;justify-content:center;gap:10px}--.services .icons span{font-size:16px;opacity:0.4;transition:opacity .2s}--.services .icons span:hover{opacity:0.8}--.inline-link{display:inline-flex;align-items:center;gap:6px;margin-top:12px;font-size:12px;color:var(--accent);cursor:pointer;text-decoration:none;font-weight:600}--.inline-link:hover{color:#a78bfa}--.panel-note{font-size:12px;color:var(--text-muted);line-height:1.5;margin-bottom:16px}----/* Toast */--.login-toast{-- position:fixed;top:20px;right:20px;z-index:100;padding:14px 20px;-- border-radius:10px;font-size:13px;font-weight:500;-- transform:translateX(120%);transition:transform .3s cubic-bezier(.34,1.56,.64,1);--}--.login-toast.show{transform:translateX(0)}--.login-toast.error{background:rgba(231,76,60,0.9);color:#fff}--.login-toast.success{background:rgba(46,204,113,0.9);color:#fff}----/* ── Account Chooser ───────────────────────────────────── */--.chooser-subtitle{font-size:13px;color:var(--text-muted);text-align:center;margin-bottom:20px;line-height:1.5}--.chooser-subtitle .app-name{color:var(--accent);font-weight:700}--.oauth-consent{margin:0 0 20px;padding:14px 16px;border:1px solid var(--border);border-radius:12px;background:var(--bg-input);font-size:13px;line-height:1.5;text-align:left}--.oauth-consent .consent-head{margin:0 0 10px;color:var(--text)}--.oauth-consent .consent-head b{color:var(--accent)}--.oauth-consent .consent-host{color:var(--text-muted)}--.oauth-consent ul{list-style:none;margin:0;padding:0;display:grid;gap:8px}--.oauth-consent li{padding:8px 10px;border-left:3px solid var(--border);border-radius:6px;background:rgba(255,255,255,0.02)}--.oauth-consent li.sensitive{border-left-color:#f59e0b}--.oauth-consent .consent-name{font-weight:600;color:var(--text)}--.oauth-consent .consent-badge{display:inline-block;margin-left:6px;padding:1px 7px;border-radius:999px;font-size:11px;font-weight:700;color:#1a1204;background:#f59e0b;vertical-align:1px}--.oauth-consent small{display:block;margin-top:2px;color:var(--text-muted);font-size:12px}--.oauth-consent .consent-none,.oauth-consent .consent-refused{margin:0;color:var(--text-muted)}--.oauth-consent .consent-refused{margin-top:10px;font-size:12px}--.oauth-consent code{font-size:11.5px}----.account-list{display:flex;flex-direction:column;gap:8px;margin-bottom:16px}--.account-item{-- display:flex;align-items:center;gap:12px;padding:12px 14px;-- background:var(--bg-input);border:1px solid var(--border);border-radius:12px;-- cursor:pointer;transition:all .2s;position:relative;--}--.account-item:hover{border-color:var(--accent);background:rgba(139,92,246,0.06);transform:translateX(2px)}--.account-item.active-account{border-color:var(--accent);box-shadow:0 0 0 2px var(--accent-glow)}--.account-item .avatar{-- width:40px;height:40px;border-radius:50%;background:var(--border);-- display:flex;align-items:center;justify-content:center;font-size:18px;-- flex-shrink:0;overflow:hidden;--}--.account-item .avatar img{width:100%;height:100%;object-fit:cover}--.account-item .info{flex:1;min-width:0}--.account-item .info .name{font-size:14px;font-weight:600;color:var(--text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}--.account-item .info .detail{font-size:11px;color:var(--text-muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}--.account-item .arrow{color:var(--text-dim);font-size:14px;flex-shrink:0;transition:color .2s}--.account-item:hover .arrow{color:var(--accent)}--.account-item .remove-btn{-- position:absolute;top:6px;right:6px;width:20px;height:20px;border:none;-- background:rgba(231,76,60,0.15);border-radius:50%;color:#e74c3c;font-size:10px;-- cursor:pointer;display:none;align-items:center;justify-content:center;-- transition:background .2s;line-height:1;--}--.account-item:hover .remove-btn{display:flex}--.account-item .remove-btn:hover{background:rgba(231,76,60,0.3)}----.chooser-actions{display:flex;flex-direction:column;gap:8px}--.chooser-actions .action-btn{-- display:flex;align-items:center;justify-content:center;gap:8px;padding:12px;-- border:1px dashed var(--border);border-radius:10px;background:transparent;-- color:var(--text-muted);font-size:13px;font-weight:500;cursor:pointer;transition:all .2s;--}--.chooser-actions .action-btn:hover{border-color:var(--accent-dark);color:var(--text);background:rgba(139,92,246,0.04)}----.back-link{-- display:block;text-align:center;margin-top:16px;font-size:12px;-- color:var(--text-muted);cursor:pointer;text-decoration:none;transition:color .2s;--}--.back-link:hover{color:var(--accent)}----@media(max-width:480px){-- .login-card{padding:28px 24px;border-radius:16px}-- .logo .flame{width:56px;height:56px}-- .logo .flame .ov-mark{width:56px!important;height:56px!important}-- .logo h1{font-size:18px}-- .login-container{padding:16px}--}----@media(max-height:820px){-- .login-container{align-items:flex-start;padding-top:24px;padding-bottom:24px}--}----@media(max-width:380px){-- .login-card{padding:24px 18px;border-radius:14px}-- .tab-bar button{font-size:12px;padding:8px 6px}--}--</style>++<link rel="stylesheet" href="/css/login.css"></head><body>@@@ -30,255 +30,7 @@(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();</script><link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">--<style>--*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}--:root{-- --bg:#0d0d14;--bg-secondary:#151520;--bg-card:#1a1a26;--bg-input:#1c1c28;-- --border:#2a2a3a;--border-focus:#8b5cf6;-- --accent:#3b82f6;--accent-glow:rgba(139,92,246,0.4);--accent-dark:#1d4ed8;--accent-light:#60a5fa;-- --text:#e0e0e0;--text-secondary:#b0b0b8;--text-muted:#707080;-- --live-red:#ef4444;--success:#22c55e;-- --shadow:0 4px 20px rgba(0,0,0,0.3);--}--html{height:100%;background:var(--bg)}--body{min-height:100%;color:var(--text);font-family:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;padding-bottom:40px}----/* Layout */--.page{max-width:900px;margin:0 auto;padding:20px}--.page-header{display:flex;align-items:center;gap:16px;margin-bottom:32px}--.page-header .avatar{width:72px;height:72px;border-radius:50%;border:3px solid var(--accent);object-fit:cover}--.page-header .info{flex:1}--.page-header .info h1{font-size:22px;font-weight:800;margin-bottom:2px}--.page-header .info .subtitle{font-size:13px;color:var(--text-muted)}--.page-header .info .badges{display:flex;gap:6px;margin-top:6px}--.page-header .info .badges .badge{-- font-size:10px;padding:3px 10px;border-radius:4px;font-weight:600;text-transform:uppercase;letter-spacing:.5px;--}--.badge-admin{background:rgba(231,76,60,0.15);color:#e74c3c}--.badge-anon{background:rgba(139,92,246,0.15);color:var(--accent-light)}--.badge-streamer{background:rgba(155,89,182,0.15);color:#9b59b6}--.badge-user{background:rgba(52,152,219,0.15);color:#3498db}----/* Section tabs */--.section-tabs{display:flex;gap:4px;margin-bottom:24px;background:var(--bg-secondary);border-radius:10px;padding:4px;overflow-x:auto}--.section-tabs button{-- flex-shrink:0;padding:8px 16px;border:none;border-radius:8px;font-size:12px;font-weight:600;-- background:transparent;color:var(--text-muted);cursor:pointer;transition:all .15s;white-space:nowrap;--}--.section-tabs button.active{background:var(--bg-card);color:var(--accent-light)}--.section-tabs button:hover:not(.active){color:var(--text)}----/* Approvals (my/approvals.js): one row per waiting request, and the count on its tab */--.section-tabs .tab-badge{display:inline-block;min-width:18px;padding:0 6px;border-radius:9px;background:var(--accent);color:#fff;font-size:11px;line-height:18px;text-align:center}--.approval{border:1px solid var(--border);border-radius:10px;padding:14px 16px;margin-bottom:10px;background:var(--bg-secondary)}--.approval-summary{font-weight:600;color:var(--text);margin-bottom:6px;overflow-wrap:anywhere}--.approval-meta{font-size:12px;color:var(--text-secondary);margin-bottom:10px;overflow-wrap:anywhere}--.approval-who{color:var(--text);font-weight:600}--.approval-cap{font-size:11px;padding:1px 6px;border-radius:5px;background:var(--bg-input);border:1px solid var(--border)}--.approval-resources{list-style:none;display:flex;flex-wrap:wrap;gap:6px;margin:0 0 10px;padding:0}--.approval-resources li{font-size:11px;padding:2px 8px;border-radius:6px;background:var(--bg-input);color:var(--text-secondary);overflow-wrap:anywhere}--.approval .btn-row{display:flex;gap:8px;flex-wrap:wrap}--.approvals-empty{color:var(--text-secondary);font-size:13px}----/* Section */--.section{display:none;animation:sec-in .2s ease}--.section.active{display:block}--@keyframes sec-in{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}----/* Cards */--.card{background:var(--bg-card);border:1px solid var(--border);border-radius:12px;padding:20px;margin-bottom:16px}--.card h3{font-size:14px;font-weight:700;margin-bottom:12px;display:flex;align-items:center;gap:8px}--.card h3 .icon{font-size:16px}--.card p{font-size:13px;color:var(--text-secondary);line-height:1.6}----/* Form fields */--.form-field{margin-bottom:14px}--.form-field label{display:block;font-size:11px;font-weight:600;color:var(--text-muted);text-transform:uppercase;letter-spacing:.5px;margin-bottom:5px}--/* Every text field on the page follows the active theme: its input colour, a hover tint and an accent focus ring. */--:where(input:not([type]),input[type=text],input[type=url],input[type=email],input[type=search],input[type=password],input[type=number],select,textarea){-- padding:10px 12px;background:var(--bg-input);border:1px solid var(--border);border-radius:10px;-- color:var(--text);font:inherit;font-size:13px;outline:none;min-height:40px;-- transition:border-color .18s ease,box-shadow .18s ease,background-color .18s ease;--}--:where(input:not([type]),input[type=text],input[type=url],input[type=email],input[type=search],input[type=password],input[type=number],select,textarea):hover{border-color:color-mix(in srgb,var(--accent) 40%,var(--border))}--:where(input:not([type]),input[type=text],input[type=url],input[type=email],input[type=search],input[type=password],input[type=number],select,textarea):focus{-- border-color:var(--accent);box-shadow:0 0 0 3px color-mix(in srgb,var(--accent) 24%,transparent);-- background:color-mix(in srgb,var(--accent) 5%,var(--bg-input));--}--:where(input,textarea)::placeholder{color:var(--text-muted);opacity:1}--:where(input,select,textarea):user-invalid{border-color:var(--danger,#ef4444)}--.form-field input,.form-field select,.form-field textarea{width:100%}--.form-field textarea{resize:vertical;min-height:60px}--.form-field .hint{font-size:10px;color:var(--text-muted);margin-top:3px}--.theme-submit{display:grid;grid-template-columns:repeat(2,minmax(0,1fr)) auto;gap:12px;align-items:start;margin-top:16px}--.theme-submit .form-field{margin:0}--.theme-submit .btn{margin-top:21px;min-height:40px}--@media (max-width:640px){.theme-submit{grid-template-columns:1fr}.theme-submit .btn{margin-top:0}}----.btn{-- padding:8px 20px;border:none;border-radius:8px;font-size:12px;font-weight:600;cursor:pointer;transition:all .15s;--}--.btn-primary{background:var(--accent);color:var(--on-accent,#fff)}--.btn-primary:hover{background:var(--accent-dark)}--.btn-outline{background:transparent;border:1px solid var(--border);color:var(--text)}--.btn-outline:hover{border-color:var(--accent-dark);color:var(--accent-light)}--.btn-danger{background:rgba(231,76,60,0.1);color:var(--live-red);border:1px solid rgba(231,76,60,0.2)}--.btn-danger:hover{background:rgba(231,76,60,0.2)}--.btn-row{display:flex;gap:8px;margin-top:12px}--.data-check{display:flex;gap:8px;align-items:flex-start;font-size:13px;color:var(--text-secondary);line-height:1.5}--.data-check input{margin-top:3px}--.data-status{align-self:center;font-size:12px;color:var(--text-secondary)}--.module-row{display:flex;gap:12px;align-items:flex-start;padding:10px 0;border-top:1px solid var(--border)}--.module-row:first-child{border-top:0}--.module-row-main{flex:1;min-width:0}--.module-row-main strong{display:block;font-size:13px}--.module-row-main small{display:block;font-size:12px;color:var(--text-muted);line-height:1.5}--.module-row-main code{display:block;margin-top:4px;font-size:11px;color:var(--text-secondary);white-space:pre-wrap;word-break:break-word;max-height:7.5em;overflow:auto}----/* Account list */--.account-list{display:flex;flex-direction:column;gap:8px}--.account-item{-- display:flex;align-items:center;gap:12px;padding:12px;border-radius:10px;-- background:var(--bg-input);border:1px solid var(--border);--}--.account-item.active{border-color:var(--accent)}--.account-item img{width:36px;height:36px;border-radius:50%}--.account-item .name{font-size:13px;font-weight:600}--.account-item .meta{font-size:11px;color:var(--text-muted)}--.account-item .actions{margin-left:auto;display:flex;gap:6px}----/* Notification pref grid */--.notif-grid{display:grid;grid-template-columns:1fr auto auto auto auto;gap:8px;align-items:center;font-size:12px}--.notif-grid .cat{font-weight:600;text-transform:capitalize}--.notif-grid .toggle{width:36px;height:20px;border-radius:10px;border:none;cursor:pointer;position:relative;transition:background .2s}--.notif-grid .toggle::after{content:'';position:absolute;width:16px;height:16px;border-radius:50%;background:#fff;top:2px;left:2px;transition:transform .2s}--.notif-grid .toggle.on{background:var(--accent)}--.notif-grid .toggle.on::after{transform:translateX(16px)}--.notif-grid .toggle.off{background:var(--border)}----/* Linked services */--.linked-item{display:flex;align-items:center;gap:12px;padding:12px;border-radius:8px;background:var(--bg-input);margin-bottom:8px;text-decoration:none;color:inherit;transition:transform .15s,background .15s}--a.linked-item:hover{background:var(--bg-hover,var(--bg-card));transform:translateX(2px)}--.linked-item .svc-meta{margin-left:auto;text-align:right;font-size:11px;color:var(--text-muted);white-space:nowrap}--.linked-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:8px;margin-top:14px}--.linked-grid a{display:flex;align-items:center;gap:8px;padding:10px 12px;border-radius:8px;border:1px dashed var(--border);font-size:12px;color:var(--text-secondary);text-decoration:none;transition:border-color .15s,color .15s}--.linked-grid a:hover{border-color:var(--accent);color:var(--text)}--.hist-toolbar{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin-bottom:12px}--.hist-chips{display:flex;flex-wrap:wrap;gap:6px}--.hist-chips button{padding:5px 10px;border-radius:999px;border:1px solid var(--border);background:transparent;color:var(--text-secondary);font-size:12px;cursor:pointer;transition:all .15s}--.hist-chips button.active,.hist-chips button:hover{border-color:var(--accent);color:var(--accent-light);background:rgba(var(--accent-rgb,139,92,246),.08)}--.hist-search{flex:1;min-width:160px;padding:7px 10px;border-radius:8px;border:1px solid var(--border);background:var(--bg-input);color:var(--text);font-size:13px}--.hist-pause{display:inline-flex;align-items:center;gap:6px;font-size:12px;color:var(--text-secondary);cursor:pointer}--.btn-small{padding:6px 10px;font-size:12px}--.hist-day{font-size:11px;font-weight:700;letter-spacing:.6px;text-transform:uppercase;color:var(--text-muted);margin:14px 0 6px}--.hist-item{display:flex;align-items:center;gap:12px;padding:9px 10px;border-radius:8px;text-decoration:none;color:inherit;transition:background .12s;animation:sec-in .25s ease both}--.hist-item:hover{background:var(--bg-input)}--.hist-item .hi{width:34px;height:34px;border-radius:9px;display:grid;place-items:center;background:rgba(var(--accent-rgb,139,92,246),.12);color:var(--accent-light);flex:none}--.hist-item .ht{font-size:13px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}--.hist-item .hs{font-size:11px;color:var(--text-muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}--.hist-item .hx{margin-left:auto;display:flex;align-items:center;gap:10px;flex:none;font-size:11px;color:var(--text-muted)}--.hist-item .hx button{background:none;border:none;color:var(--text-muted);cursor:pointer;padding:4px;opacity:0;transition:opacity .15s}--.hist-item:hover .hx button{opacity:1}--.hist-item .hx button:hover{color:var(--danger,#ef4444)}--.hist-empty{padding:26px;text-align:center;color:var(--text-muted);font-size:13px}--.linked-item .svc-icon{font-size:20px;width:32px;text-align:center}--.linked-item .svc-name{font-size:13px;font-weight:600}--.linked-item .svc-user{font-size:11px;color:var(--text-muted)}--.linked-item .svc-status{margin-left:auto;font-size:11px;font-weight:600;color:var(--success)}----/* Themes */--.theme-hero{display:flex;justify-content:space-between;gap:16px;align-items:flex-start;flex-wrap:wrap;margin-bottom:18px}--.theme-hero-copy{max-width:560px}--.theme-hero-copy p{margin-top:8px}--.theme-current{display:flex;align-items:center;gap:10px;padding:12px 14px;border-radius:10px;background:var(--bg-input);border:1px solid var(--border);min-width:220px}--.theme-current-swatch{display:flex;gap:4px}--.theme-current-swatch span,.theme-preview-swatch span{width:18px;height:18px;border-radius:50%;border:1px solid rgba(255,255,255,.08);display:inline-block}--.theme-current .label{font-size:10px;color:var(--text-muted);text-transform:uppercase;letter-spacing:.08em}--.theme-current .name{font-size:13px;font-weight:700;margin-top:2px}--.theme-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:12px}--.theme-custom-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:10px}--.theme-custom-row{display:flex;align-items:center;gap:8px;font-size:13px;color:var(--text-secondary);background:var(--bg-input);border:1px solid var(--border);border-radius:10px;padding:8px 10px;cursor:pointer}--.theme-custom-row input[type=color]{width:34px;height:34px;border:none;border-radius:8px;background:none;cursor:pointer;padding:0}--.theme-custom-row input[type=color]::-webkit-color-swatch{border:1px solid var(--border);border-radius:6px}--.theme-custom-row input[type=color]::-webkit-color-swatch-wrapper{padding:0}--.theme-card{background:var(--bg-input);border:1px solid var(--border);border-radius:14px;padding:14px;display:flex;flex-direction:column;gap:12px;transition:border-color .15s,transform .15s,box-shadow .15s}--.theme-card:hover{transform:translateY(-2px);border-color:var(--accent)}--.theme-card.active{border-color:var(--accent);box-shadow:0 0 0 1px rgba(139,92,246,.25) inset}--.theme-preview{border-radius:12px;padding:14px;background:linear-gradient(135deg,var(--theme-bg-1,#202331),var(--theme-bg-2,#181a24));border:1px solid rgba(255,255,255,.06)}--.theme-preview-top{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:12px}--.theme-preview-chip{padding:5px 9px;border-radius:999px;background:rgba(255,255,255,.08);color:#fff;font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.08em}--.theme-preview-bars{display:grid;gap:7px}--.theme-preview-bars span{display:block;height:8px;border-radius:999px;background:rgba(255,255,255,.16)}--.theme-preview-bars span:first-child{width:72%}--.theme-preview-bars span:last-child{width:46%}--.theme-card h4{font-size:14px;font-weight:700}--.theme-card p{font-size:12px;color:var(--text-secondary);line-height:1.55}--.theme-card-footer{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-top:auto}--.theme-badges{display:flex;gap:6px;flex-wrap:wrap}--.theme-badge{font-size:10px;padding:4px 8px;border-radius:999px;background:rgba(255,255,255,.05);color:var(--text-muted);text-transform:uppercase;letter-spacing:.08em;font-weight:700}--.theme-empty{padding:16px;border-radius:12px;background:var(--bg-input);border:1px dashed var(--border);text-align:center;color:var(--text-muted);font-size:13px}----/* Anon info card */--.anon-card{text-align:center;padding:32px 20px}--.anon-card .number{font-size:48px;font-weight:800;color:var(--accent);margin-bottom:8px}--.anon-card .label{font-size:12px;color:var(--text-muted);margin-bottom:16px}--.anon-card .stats{display:flex;justify-content:center;gap:24px}--.anon-card .stat{text-align:center}--.anon-card .stat .val{font-size:20px;font-weight:700}--.anon-card .stat .key{font-size:10px;color:var(--text-muted);text-transform:uppercase}----/* Loading */--.loading-spinner{text-align:center;padding:40px;color:var(--text-muted);font-size:13px}--.acct-retry-btn{display:inline-block;background:var(--accent,#8b5cf6);color:#0b0d10;border:none;border-radius:6px;padding:3px 12px;font-weight:600;font-size:0.85em;cursor:pointer;vertical-align:baseline}--.acct-retry-btn:hover{filter:brightness(1.08)}----@media(max-width:600px){-- .page{padding:12px}-- .page-header{flex-direction:column;text-align:center}-- .page-header .info .badges{justify-content:center}-- .notif-grid{grid-template-columns:1fr auto auto}-- .notif-grid .col-email,.notif-grid .col-toast{display:none}--}----/* Dashboard Showcase */--@keyframes float { 0%, 100% { transform: translateY(0px); } 50% { transform: translateY(-10px); } }--@keyframes fadeInScale { from { opacity: 0; transform: scale(0.95); } to { opacity: 1; transform: scale(1); } }--.dashboard-showcase { margin: 32px 0; padding: 28px; border-radius: 12px; background: var(--bg-card); border: 1px solid var(--border); }--.showcase-header { text-align: center; margin-bottom: 24px; }--.showcase-header h2 { font-size: 1.3rem; font-weight: 800; margin-bottom: 6px; }--.showcase-header p { font-size: 12px; color: var(--text-muted); }--.showcase-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; margin-bottom: 20px; }--.showcase-card { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 16px; border-radius: 10px; background: var(--bg-input); border: 1px solid var(--border); cursor: pointer; text-decoration: none; color: inherit; gap: 8px; animation: float ease-in-out infinite; transition: all 0.3s ease; }--.showcase-card:nth-child(1) { animation-delay: 0s; animation-duration: 3s; }--.showcase-card:nth-child(2) { animation-delay: 0.15s; animation-duration: 3.3s; }--.showcase-card:nth-child(3) { animation-delay: 0.3s; animation-duration: 3.6s; }--.showcase-card:nth-child(4) { animation-delay: 0.45s; animation-duration: 3.2s; }--.showcase-card:hover { transform: translateY(-4px); border-color: var(--accent); background: rgba(139,92,246,0.08); }--.showcase-card-icon { font-size: 1.4rem; color: var(--accent-light); }--.showcase-card-label { font-size: 11px; font-weight: 700; text-align: center; }--.quick-access { display: grid; grid-template-columns: repeat(6, 1fr); gap: 8px; }--.quick-link { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 10px; border-radius: 8px; background: var(--bg); border: 1px solid var(--border); text-decoration: none; color: var(--text-secondary); font-size: 10px; font-weight: 600; text-align: center; transition: all 0.25s ease; animation: fadeInScale 0.5s ease both; }--.quick-link i { font-size: 1rem; margin-bottom: 4px; }--.quick-link:nth-child(1) { animation-delay: 0.1s; }--.quick-link:nth-child(2) { animation-delay: 0.15s; }--.quick-link:nth-child(3) { animation-delay: 0.2s; }--.quick-link:nth-child(4) { animation-delay: 0.25s; }--.quick-link:nth-child(5) { animation-delay: 0.3s; }--.quick-link:nth-child(6) { animation-delay: 0.35s; }--.quick-link:hover { border-color: var(--accent); color: var(--accent-light); background: rgba(139,92,246,0.08); }--@media(max-width:600px){ .showcase-grid { grid-template-columns: repeat(2, 1fr); } .quick-access { grid-template-columns: repeat(3, 1fr); } }--.recent-tools { margin-top: 14px; }--.recent-tools h3 { font-size: 12px; font-weight: 700; color: var(--text-secondary); margin: 0 0 8px; display: flex; gap: 6px; align-items: center; }--.recent-tools-list { display: flex; flex-wrap: wrap; gap: 6px; }--.recent-tool { padding: 6px 10px; border-radius: 999px; background: var(--bg); border: 1px solid var(--border); color: var(--text-secondary); font-size: 12px; font-weight: 600; text-decoration: none; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }--.recent-tool:hover { border-color: var(--accent); color: var(--accent-light); }--.recent-tools-status { margin: 6px 0 0; font-size: 12px; color: var(--text-secondary); }--.recent-tools-status:empty { display: none; }--.recent-tool-item { display: inline-flex; align-items: center; gap: 2px; max-width: 100%; }--.recent-tool-star { border: 0; background: none; color: var(--text-secondary); cursor: pointer; padding: 6px; font-size: 12px; border-radius: 999px; }--.recent-tool-star[aria-pressed="true"] { color: var(--accent-light); }--.recent-tool-star:hover, .recent-tool-star:focus-visible { color: var(--accent-light); }--</style>++<link rel="stylesheet" href="/css/my.css"></head><body>@@@ -20,9 +20,11 @@ assert.ok(toggles.length >= 3 && toggles.every((b) => /aria-label="Show passwordconst loginJs = fs.readFileSync(path.join(__dirname, '..', 'public', 'js', 'login.js'), 'utf8');assert.match(loginJs, /btn\.setAttribute\('aria-label', input\.type === 'password' \? 'Show password' : 'Hide password'\)/);for (const [, body] of login.matchAll(/<script\b[^>]*>([\s\S]*?)<\/script>/g)) assert.ok(body.length <= 200, 'login.html keeps no inline script body over 200 characters');--const token = (name) => login.match(new RegExp(`--${name}:(#[0-9a-f]{6})`, 'i'))[1];++// The sign-in page's CSS was extracted to /css/login.css (plan T2 step 1); the tokens and rules live there now.++const loginCss = fs.readFileSync(path.join(__dirname, '..', 'public', 'css', 'login.css'), 'utf8');++const token = (name) => loginCss.match(new RegExp(`--${name}:(#[0-9a-f]{6})`, 'i'))[1];for (const bg of ['bg', 'bg-card', 'bg-input']) assert.ok(ratio(token('text-muted'), token(bg)) >= 4.5, `muted text on --${bg} reads at 4.5:1`);--assert.match(login, /\.tab-bar button\.active\{background:var\(--accent-dark\);color:#fff;/);++assert.match(loginCss, /\.tab-bar button\.active\{background:var\(--accent-dark\);color:#fff;/);assert.ok(ratio(token('accent-dark'), '#ffffff') >= 4.5, 'the active tab reads at 4.5:1');const updates = fs.readFileSync(path.join(__dirname, '..', 'server', 'updates', 'routes.js'), 'utf8');nnew file mode 100644@@@ -0,0 +1,22 @@++'use strict';++// T2 step 1: the three pages' inline <style> monoliths moved to public/css/<page>.css, linked exactly once.++// The theme-flash guard stays inline on /admin and /my: a stylesheet would load too late to stop the flash.++// node test/page-styles.test.js++const assert = require('assert');++const fs = require('fs');++const path = require('path');++++const pub = path.join(__dirname, '..', 'public');++for (const [page, css] of [['admin.html', 'admin.css'], ['my.html', 'my.css'], ['login.html', 'login.css']]) {++ const html = fs.readFileSync(path.join(pub, page), 'utf8');++ const links = [...html.matchAll(/<link\b[^>]*rel="stylesheet"[^>]*href="\/css\/([^"]+)"/g)].map((m) => m[1]);++ assert.deepStrictEqual(links, [css], `${page} links /css/${css} exactly once`);++ assert.ok(!/<style\b/.test(html), `${page} keeps no inline <style> block`);++ const body = fs.readFileSync(path.join(pub, 'css', css), 'utf8');++ assert.ok(body.length > 1000, `public/css/${css} carries the page styles`);++}++for (const page of ['admin.html', 'my.html']) {++ const html = fs.readFileSync(path.join(pub, page), 'utf8');++ assert.match(html, /localStorage\.getItem\('ov_theme'\)/, `${page} keeps the inline theme bootstrap`);++}++console.log('page styles: all checks passed');@@@ -17,6 +17,7 @@ const { ACCOUNT_HUB_PATHS } = require('../server/not-found');'/', '/login', '/forgot-password', '/reset-password', '/admin', '/admin/settings', '/admin/analytics/overview','/verify-email', '/status', '/sso/fanout', '/terms', '/privacy', '/dmca', '/llms.txt','/robots.txt', '/sitemap.xml', '/manifest.webmanifest', '/assets/logo.svg', '/js/login.js', '/shared/navbar.js', '/shared/v1/navbar.js',++ '/css/login.css', '/css/my.css', '/css/admin.css','/openvibe-sw.js', '/release.json', '/.well-known/openvibe', '/.well-known/web-identity', '/api/health', '/api/brand','/api/v1/registry/services', '/api/v1/registry/categories', '/api/v1/registry/featured', '/api/.well-known/jwks',...ACCOUNT_HUB_PATHS.filter(p => p !== '/my.html'),
@@ -245,7 +245,7 @@ agent's name and the summary as plain text, never the details; a standing rule's
@@ -245,7 +245,7 @@ agent's name and the summary as plain text, never the details; a standing rule's
- **Never in it:** secrets or their hashes, client ids, redirect URIs, credentials, members other than the owner, or who requested or decided a grant.
- **Never in it:** secrets or their hashes, client ids, redirect URIs, credentials, members other than the owner, or who requested or decided a grant.
- **Archived projects** still read, with `archived_at` set and every app revoked.
- **Archived projects** still read, with `archived_at` set and every app revoked.
- **Errors:** an unknown or malformed id is `404 project.not_found`; no token is `401 token.missing`; a token without the capability is `403`.
- **Errors:** an unknown or malformed id is `404 project.not_found`; no token is `401 token.missing`; a token without the capability is `403`.
-- **Catalog.** `network.project.read` is `planned` in openvibe-contracts until Contracts makes it `active`. The guard accepts `planned` and refuses only `retired`.
+- **Catalog.** `network.project.read` has been `active` in openvibe-contracts since v0.88.0 (first-party). The guard accepts `active` and `planned` and refuses only `retired`.
Redirect URIs must be https. `http://localhost`, `127.0.0.1` and `[::1]` are allowed for sandbox apps only. Fragments and embedded credentials are refused. At most 10 per app. A public app needs at least one. A project has at most `DEV_MAX_APPS_PER_PROJECT` active apps.
Redirect URIs must be https. `http://localhost`, `127.0.0.1` and `[::1]` are allowed for sandbox apps only. Fragments and embedded credentials are refused. At most 10 per app. A public app needs at least one. A project has at most `DEV_MAX_APPS_PER_PROJECT` active apps.
@@ -308,7 +308,7 @@ Until the relay is turned on in an environment, nobody else receives these event
@@ -308,7 +308,7 @@ Until the relay is turned on in an environment, nobody else receives these event
## Known gaps (next steps)
## Known gaps (next steps)
- **Consent screen (backend built; page next).** The account chooser shows the app's name, not the capabilities it asks for. Codes and Network need a consent step before third-party production apps go live, and it must list the capability ids the app asks for, marking the ones `capability@1` marks `sensitive`; a request without a `scope` must not consent to capabilities the screen did not name. This screen is the app path's compensation for not using the agent confirmation flow — an authorization-code app acts for a person who is present and authorizes it capability by capability, so it carries no `cap_confirm` (decided 2026-10-04, `docs/t2-projects-and-grants.md` section 9). **The backend half is built:** `GET /oauth/client-info` with the authorize request's `scope` returns `capabilities: [{ id, name, description, sensitive }]` — the app-grantable requested ids, in the order requested, with `description` and `sensitive` from the `capability@1` catalog (`name` is the catalog id, since `capability@1` has no separate display name) — and `refused: [id]` for the unknown or ungrantable ones the page must not name; without a `scope` it returns `capabilities: []`. The authorization code an app receives now carries the explicit consented set (`tokens.issueCode`), so an absent or empty `scope` consents to nothing: the code stores an empty set and the token exchange mints a token with `cap: []` (the person is still identified by `on_behalf_of`), and asking the token endpoint for an id outside that set is `invalid_scope`. Apps have no refresh tokens, so no refreshed token can widen the set either. What remains is the API design session's page: render `capabilities` from `client-info`, mark the `sensitive` ones, and say the `refused` ids will not be granted (`test/consent-screen.test.js`).
- **Consent screen (backend built; page next).** The account chooser shows the app's name, not the capabilities it asks for. Codes and Network need a consent step before third-party production apps go live, and it must list the capability ids the app asks for, marking the ones `capability@1` marks `sensitive`; a request without a `scope` must not consent to capabilities the screen did not name. This screen is the app path's compensation for not using the agent confirmation flow — an authorization-code app acts for a person who is present and authorizes it capability by capability, so it carries no `cap_confirm` (decided 2026-10-04, `docs/t2-projects-and-grants.md` section 9). **The backend half is built:** `GET /oauth/client-info` with the authorize request's `scope` returns `capabilities: [{ id, name, description, sensitive }]` — the app-grantable requested ids, in the order requested, with `description` and `sensitive` from the `capability@1` catalog (`name` is the catalog id, since `capability@1` has no separate display name) — and `refused: [id]` for the unknown or ungrantable ones the page must not name; without a `scope` it returns `capabilities: []`. The authorization code an app receives now carries the explicit consented set (`tokens.issueCode`), so an absent or empty `scope` consents to nothing: the code stores an empty set and the token exchange mints a token with `cap: []` (the person is still identified by `on_behalf_of`), and asking the token endpoint for an id outside that set is `invalid_scope`. Apps have no refresh tokens, so no refreshed token can widen the set either. What remains is the API design session's page: render `capabilities` from `client-info`, mark the `sensitive` ones, and say the `refused` ids will not be granted (`test/consent-screen.test.js`).
-- **Service-side project reads.** The route is built, but no service calls it yet. Host holds the grant. Contracts still has to make `network.project.read` `active`.
+- **Service-side project reads.** The route is built, but no service calls it yet. Host holds the grant. `network.project.read` has been `active` since openvibe-contracts v0.88.0.
- **PowerChat as the first project** (ADR-014 migration). This is not done. PowerChat's existing OAuth client is unchanged.
- **PowerChat as the first project** (ADR-014 migration). This is not done. PowerChat's existing OAuth client is unchanged.
- **Receivers.** Every default sandbox audience must refuse `env: sandbox` except on routes that keep sandbox data apart, and must key tenancy by `project_id`. Media and Events do this in Wave 20. Tools does not yet (see [Sandbox tokens](#sandbox-tokens)).
- **Receivers.** Every default sandbox audience must refuse `env: sandbox` except on routes that keep sandbox data apart, and must key tenancy by `project_id`. Media and Events do this in Wave 20. Tools does not yet (see [Sandbox tokens](#sandbox-tokens)).
- **Revocation fan-out.** The relayed `network.app.revoked` event is the signal receivers can use to disable an app's subscriptions or tenants early. OpenVibe.Events acts on it (and on `network.grant.changed` withdrawing `events.app.subscribe`): it disables the app's subscriptions and refuses its tokens issued before the revocation. That needs this relay switched on. A receiver that does not act on it only stops the app when its issued tokens expire (within 5 minutes); state the app created there (for example a Media object) is untouched by Network.
- **Revocation fan-out.** The relayed `network.app.revoked` event is the signal receivers can use to disable an app's subscriptions or tenants early. OpenVibe.Events acts on it (and on `network.grant.changed` withdrawing `events.app.subscribe`): it disables the app's subscriptions and refuses its tokens issued before the revocation. That needs this relay switched on. A receiver that does not act on it only stops the app when its issued tokens expire (within 5 minutes); state the app created there (for example a Media object) is untouched by Network.
@@ -36,7 +36,7 @@ allowances changes.
@@ -36,7 +36,7 @@ allowances changes.
| Subjects | `server/identity/subjects.js` resolves `usr_` and `gst_` only | no `agt_` |
| Subjects | `server/identity/subjects.js` resolves `usr_` and `gst_` only | no `agt_` |
| Capability guard | `principals.js:347` `guard(capability)` throws at boot on an id the catalog does not have | built |
| Capability guard | `principals.js:347` `guard(capability)` throws at boot on an id the catalog does not have | built |
| Delegated client capability | `docs/capabilities-proposal/network.project.manage.json` (in the catalog at v0.85.0 as `public`, `planned`) | proposed, unused |
| Delegated client capability | `docs/capabilities-proposal/network.project.manage.json` (in the catalog at v0.85.0 as `public`, `planned`) | proposed, unused |
-| Service-side project read | `network.project.read` (catalog: `first-party`, `planned`); `GET /internal/projects/:project_id`, granted to Host only | built (lane B step 3); no caller yet; Contracts still has to make it `active` |
+| Service-side project read | `network.project.read` (catalog: `first-party`, `active` since v0.88.0); `GET /internal/projects/:project_id`, granted to Host only | built (lane B step 3); no caller yet |
| Sensitive capabilities, confirmations, budgets, agents | — | missing (agents: built by slice 2) |
| Sensitive capabilities, confirmations, budgets, agents | — | missing (agents: built by slice 2) |
## 2. Contract vocabulary (v0.85.0, used verbatim)
## 2. Contract vocabulary (v0.85.0, used verbatim)
@@ -305,7 +305,7 @@ inside (the `docs/t2-resource-registry.md` §4 rule).
@@ -305,7 +305,7 @@ inside (the `docs/t2-resource-registry.md` §4 rule).
| Method and path | Capability | Body → result |
| Method and path | Capability | Body → result |
|---|---|---|
|---|---|---|
-| `GET /internal/agents/:agent` (built, slice 6) | `network.project.read` (exists, first-party) | `{ agent, grants, budgets }`, grants and budgets filtered to the caller's own audience (`openvibe.<caller>`) |
+| `GET /internal/agents/:agent` (built, slice 6) | `network.project.read` (`active`, first-party since v0.88.0) | `{ agent, grants, budgets }`, grants and budgets filtered to the caller's own audience (`openvibe.<caller>`) |
| `POST /internal/confirmations` (built, slice 7) | **`network.confirmation.manage`** (published in v0.89.0, `status: planned`) | `{ requested_by: {type:'agent', id}, capability, summary, details?, resources?, request_digest, session_id?, ttl_s? }` → `201 { confirmation }` (pending) or `200 { confirmation }` (approved by a standing rule) |
| `POST /internal/confirmations` (built, slice 7) | **`network.confirmation.manage`** (published in v0.89.0, `status: planned`) | `{ requested_by: {type:'agent', id}, capability, summary, details?, resources?, request_digest, session_id?, ttl_s? }` → `201 { confirmation }` (pending) or `200 { confirmation }` (approved by a standing rule) |
| `GET /internal/confirmations/:id` | same | `{ confirmation, used_at }` |
| `GET /internal/confirmations/:id` | same | `{ confirmation, used_at }` |
| `POST /internal/confirmations/:id/consume` | same | `{ request_digest }` → `200 { confirmation, used_at }` |
| `POST /internal/confirmations/:id/consume` | same | `{ request_digest }` → `200 { confirmation, used_at }` |
@@ -17,165 +17,7 @@
@@ -17,165 +17,7 @@
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
</script>
</script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
-<style>
-*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
-:root{
- --bg:#0d0d14;--bg-secondary:#151520;--bg-card:#1a1a26;--bg-input:#1c1c28;--bg-tertiary:#222233;
- --border:#2a2a3a;--border-focus:#8b5cf6;
- --accent:#3b82f6;--accent-glow:rgba(139,92,246,0.4);--accent-dark:#1d4ed8;--accent-light:#60a5fa;
- --text:#e0e0e0;--text-secondary:#b0b0b8;--text-muted:#707080;
- --live-red:#ef4444;--success:#22c55e;--warning:#f59e0b;--danger:#ef4444;
- --shadow:0 4px 20px rgba(0,0,0,0.3);
-}
-html{height:100%;background:var(--bg)}
-body{min-height:100%;color:var(--text);font-family:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;padding-bottom:40px}
-
-/* Layout */
-.page{max-width:1200px;margin:0 auto;padding:20px}
-.page-title{display:flex;align-items:center;gap:12px;margin-bottom:24px}
-.page-title h1{font-size:22px;font-weight:800}
-.page-title .badge{font-size:10px;padding:3px 10px;border-radius:4px;font-weight:600;text-transform:uppercase;letter-spacing:.5px;background:rgba(231,76,60,0.15);color:#e74c3c}
-
-/* Section tabs */
-.section-tabs-shell{display:flex;align-items:center;gap:8px;margin-bottom:24px}
-.section-tabs-wrap{flex:1;min-width:0;position:relative;display:flex;align-items:center}
-.section-tabs{
- display:flex;gap:4px;background:var(--bg-secondary);border-radius:10px;padding:4px;
- overflow-x:auto;-webkit-overflow-scrolling:touch;scrollbar-width:none;flex:1;min-width:0;
-}
-.section-tabs::-webkit-scrollbar{height:0}
-.section-tabs button{
- flex-shrink:0;padding:8px 14px;border:none;border-radius:8px;font-size:11px;font-weight:600;
- background:transparent;color:var(--text-muted);cursor:pointer;transition:all .15s;white-space:nowrap;
-}
-.section-tabs button.active{background:var(--bg-card);color:var(--accent-light)}
-.section-tabs button:hover:not(.active){color:var(--text)}
-.tabs-scroll-btn{
- width:34px;height:34px;flex:0 0 34px;border:none;border-radius:10px;display:flex;align-items:center;justify-content:center;
- background:var(--bg-secondary);color:var(--text-muted);cursor:pointer;transition:all .15s;opacity:0;pointer-events:none;
-}
-.tabs-scroll-btn.visible{opacity:1;pointer-events:auto}
-.tabs-scroll-btn:hover{color:var(--accent-light);background:var(--bg-card)}
-.tabs-scroll-btn:disabled{opacity:.3;pointer-events:none}
-
-/* Section */
-.section{display:none;animation:sec-in .2s ease}
-.section.active{display:block}
-@keyframes sec-in{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}
-
-/* Stats grid */
-.stats-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:12px;margin-bottom:24px}
-.stat-card{background:var(--bg-card);border:1px solid var(--border);border-radius:10px;padding:16px;text-align:center}
-.stat-card .value{font-size:24px;font-weight:800;color:var(--accent-light)}
-.stat-card .label{font-size:11px;color:var(--text-muted);margin-top:4px;display:flex;align-items:center;justify-content:center;gap:6px}
-
-/* Cards */
-.card{background:var(--bg-card);border:1px solid var(--border);border-radius:12px;padding:20px;margin-bottom:16px}
-.card h3{font-size:14px;font-weight:700;margin-bottom:12px;display:flex;align-items:center;gap:8px}
-.card .chart-wrap{position:relative;width:100%;max-height:320px}
-.card canvas{max-height:320px}
-
-/* Tables */
-.admin-table{width:100%;border-collapse:collapse;font-size:13px}
-.admin-table thead{border-bottom:2px solid var(--border)}
-.admin-table th{padding:10px 8px;text-align:left;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.5px;color:var(--text-muted)}
-.admin-table td{padding:10px 8px;border-bottom:1px solid var(--border)}
-.admin-table tbody tr:hover{background:rgba(139,92,246,0.03)}
-
-/* Buttons */
-.btn{padding:8px 16px;border:none;border-radius:8px;font-size:12px;font-weight:600;cursor:pointer;transition:all .15s;display:inline-flex;align-items:center;gap:6px}
-.btn-primary{background:var(--accent);color:var(--on-accent,#fff)}
-.btn-primary:hover{background:var(--accent-dark)}
-.btn-outline{background:transparent;border:1px solid var(--border);color:var(--text)}
-.btn-outline:hover{border-color:var(--accent-dark);color:var(--accent-light)}
-.btn-danger{background:rgba(231,76,60,0.1);color:var(--live-red);border:1px solid rgba(231,76,60,0.2)}
-.btn-danger:hover{background:rgba(231,76,60,0.2)}
-.btn-success{background:rgba(46,204,113,0.1);color:var(--success);border:1px solid rgba(46,204,113,0.2)}
-.btn-success:hover{background:rgba(46,204,113,0.2)}
-.btn-sm{padding:5px 10px;font-size:11px}
-.btn:disabled{opacity:.4;cursor:not-allowed}
-
-/* Form fields */
-.form-field{margin-bottom:14px}
-.form-field label{display:block;font-size:11px;font-weight:600;color:var(--text-muted);text-transform:uppercase;letter-spacing:.5px;margin-bottom:5px}
-.form-field input,.form-field select,.form-field textarea{
- width:100%;padding:10px 12px;background:var(--bg-input);border:1px solid var(--border);
- border-radius:8px;color:var(--text);font-size:13px;outline:none;transition:border-color .2s;
-}
-.form-field input:focus,.form-field select:focus,.form-field textarea:focus{border-color:var(--accent)}
-
-/* Input inline */
-.input-row{display:flex;gap:8px;align-items:center;margin-bottom:12px;flex-wrap:wrap}
-.input-row input,.input-row select{
- padding:8px 12px;background:var(--bg-input);border:1px solid var(--border);border-radius:8px;
- color:var(--text);font-size:13px;outline:none;flex:1;min-width:160px;
-}
-.input-row input:focus{border-color:var(--accent)}
-
-/* Badges */
-.badge{display:inline-block;padding:2px 8px;border-radius:4px;font-size:10px;font-weight:600;text-transform:uppercase}
-.badge-success{background:rgba(46,204,113,0.15);color:#2ecc71}
-.badge-danger{background:rgba(231,76,60,0.15);color:#e74c3c}
-.badge-warning{background:rgba(245,158,11,0.15);color:#f59e0b}
-.badge-info{background:rgba(52,152,219,0.15);color:#3498db}
-.badge-admin{background:rgba(231,76,60,0.15);color:#e74c3c}
-.badge-global_mod{background:rgba(155,89,182,0.15);color:#9b59b6}
-.badge-streamer{background:rgba(155,89,182,0.15);color:#9b59b6}
-.badge-user{background:rgba(52,152,219,0.15);color:#3498db}
-
-/* Loading */
-.muted{color:var(--text-muted);font-size:13px}
-.loading{text-align:center;padding:40px;color:var(--text-muted)}
-
-/* Toast */
-.toast-container{position:fixed;top:60px;right:16px;z-index:200000;display:flex;flex-direction:column;gap:8px}
-.toast{padding:12px 18px;border-radius:8px;font-size:13px;font-weight:600;color:#fff;animation:toast-in .25s ease;box-shadow:var(--shadow)}
-.toast-success{background:#2ecc71}
-.toast-error{background:#e74c3c}
-.toast-info{background:#3498db}
-.toast-warning{background:#f59e0b}
-@keyframes toast-in{from{opacity:0;transform:translateX(20px)}to{opacity:1;transform:translateX(0)}}
-
-/* Sub-tabs */
-.sub-tabs{display:flex;gap:4px;margin-bottom:16px;overflow-x:auto;-webkit-overflow-scrolling:touch;flex-wrap:wrap}
-.sub-tabs button{padding:6px 12px;border:1px solid var(--border);border-radius:6px;background:transparent;color:var(--text-muted);font-size:11px;font-weight:600;cursor:pointer}
-.sub-tabs button.active{background:var(--bg-card);color:var(--accent-light);border-color:var(--accent-dark)}
-
-/* Setting rows */
-.setting-row{display:flex;align-items:center;gap:12px;padding:10px 0;border-bottom:1px solid var(--border)}
-.setting-row label{flex:1}
-.setting-row label strong{font-size:13px}
-.setting-row label small{font-size:11px;color:var(--text-muted)}
-
-/* Storage bars */
-.storage-bar{background:var(--bg-tertiary);border-radius:4px;height:14px;overflow:hidden;position:relative}
-.storage-bar-fill{height:100%;border-radius:4px;transition:width .5s}
-
-/* Sensitive fields */
-.sensitive-field{display:flex;gap:6px;align-items:center;flex-wrap:wrap}
-.sensitive-field input{flex:1}
-.sensitive-actions{display:flex;gap:4px}
-
-@media(max-width:800px){
- .page{padding:12px}
- .stats-grid{grid-template-columns:repeat(2,1fr)}
- #analytics-overview > div[style*="grid-template-columns:1fr 1fr"],
- #analytics-service-detail > div[style*="grid-template-columns:1fr 1fr"],
- #analytics-service-detail > div[style*="grid-template-columns:1fr 1fr 1fr"]{display:block!important}
- #analytics-overview > div[style*="grid-template-columns"] > .card,
- #analytics-service-detail > div[style*="grid-template-columns"] > .card{margin-bottom:12px}
-}
-@media(max-width:500px){
- .stats-grid{grid-template-columns:1fr}
- .admin-table{font-size:11px}
- .section-tabs-shell{gap:6px}
- .tabs-scroll-btn{width:30px;height:30px;flex-basis:30px;border-radius:8px}
-}
-.ssh-prompt{background:var(--bg-elev,#111);border:1px solid var(--border,#2a2a2a);border-radius:8px;padding:14px;white-space:pre-wrap;word-break:break-word;font-family:ui-monospace,Menlo,Consolas,monospace;font-size:0.8rem;line-height:1.55;color:var(--text,#ddd);max-height:520px;overflow:auto;margin:0}
-#sec-ssh code{background:var(--bg-elev,#111);border:1px solid var(--border,#2a2a2a);border-radius:5px;padding:2px 7px;font-family:ui-monospace,Menlo,Consolas,monospace;font-size:0.8rem;display:inline-block;margin-top:3px;word-break:break-all}
-#sec-ssh .staff-pill{font-size:0.62rem;font-weight:700;text-transform:uppercase;letter-spacing:.04em;padding:3px 8px;border-radius:999px;vertical-align:middle;margin-left:8px;color:var(--warning,#f0b74a);background:color-mix(in srgb,var(--warning,#f0b74a) 18%,transparent);border:1px solid color-mix(in srgb,var(--warning,#f0b74a) 40%,transparent)}
-#sec-ssh .staff-pill.user-pill{color:var(--accent,#6ea8ff);background:color-mix(in srgb,var(--accent,#6ea8ff) 18%,transparent);border-color:color-mix(in srgb,var(--accent,#6ea8ff) 40%,transparent)}
-</style>
+<link rel="stylesheet" href="/css/admin.css">
</head>
</head>
<body>
<body>
new file mode 100644
new file mode 100644
@@ -0,0 +1,158 @@
@@ -0,0 +1,158 @@
+
+*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
+:root{
+ --bg:#0d0d14;--bg-secondary:#151520;--bg-card:#1a1a26;--bg-input:#1c1c28;--bg-tertiary:#222233;
+ --border:#2a2a3a;--border-focus:#8b5cf6;
+ --accent:#3b82f6;--accent-glow:rgba(139,92,246,0.4);--accent-dark:#1d4ed8;--accent-light:#60a5fa;
+ --text:#e0e0e0;--text-secondary:#b0b0b8;--text-muted:#707080;
+ --live-red:#ef4444;--success:#22c55e;--warning:#f59e0b;--danger:#ef4444;
+ --shadow:0 4px 20px rgba(0,0,0,0.3);
+}
+html{height:100%;background:var(--bg)}
+body{min-height:100%;color:var(--text);font-family:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;padding-bottom:40px}
+
+/* Layout */
+.page{max-width:1200px;margin:0 auto;padding:20px}
+.page-title{display:flex;align-items:center;gap:12px;margin-bottom:24px}
+.page-title h1{font-size:22px;font-weight:800}
+.page-title .badge{font-size:10px;padding:3px 10px;border-radius:4px;font-weight:600;text-transform:uppercase;letter-spacing:.5px;background:rgba(231,76,60,0.15);color:#e74c3c}
+
+/* Section tabs */
+.section-tabs-shell{display:flex;align-items:center;gap:8px;margin-bottom:24px}
+.section-tabs-wrap{flex:1;min-width:0;position:relative;display:flex;align-items:center}
+.section-tabs{
+ display:flex;gap:4px;background:var(--bg-secondary);border-radius:10px;padding:4px;
+ overflow-x:auto;-webkit-overflow-scrolling:touch;scrollbar-width:none;flex:1;min-width:0;
+}
+.section-tabs::-webkit-scrollbar{height:0}
+.section-tabs button{
+ flex-shrink:0;padding:8px 14px;border:none;border-radius:8px;font-size:11px;font-weight:600;
+ background:transparent;color:var(--text-muted);cursor:pointer;transition:all .15s;white-space:nowrap;
+}
+.section-tabs button.active{background:var(--bg-card);color:var(--accent-light)}
+.section-tabs button:hover:not(.active){color:var(--text)}
+.tabs-scroll-btn{
+ width:34px;height:34px;flex:0 0 34px;border:none;border-radius:10px;display:flex;align-items:center;justify-content:center;
+ background:var(--bg-secondary);color:var(--text-muted);cursor:pointer;transition:all .15s;opacity:0;pointer-events:none;
+}
+.tabs-scroll-btn.visible{opacity:1;pointer-events:auto}
+.tabs-scroll-btn:hover{color:var(--accent-light);background:var(--bg-card)}
+.tabs-scroll-btn:disabled{opacity:.3;pointer-events:none}
+
+/* Section */
+.section{display:none;animation:sec-in .2s ease}
+.section.active{display:block}
+@keyframes sec-in{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}
+
+/* Stats grid */
+.stats-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:12px;margin-bottom:24px}
+.stat-card{background:var(--bg-card);border:1px solid var(--border);border-radius:10px;padding:16px;text-align:center}
+.stat-card .value{font-size:24px;font-weight:800;color:var(--accent-light)}
+.stat-card .label{font-size:11px;color:var(--text-muted);margin-top:4px;display:flex;align-items:center;justify-content:center;gap:6px}
+
+/* Cards */
+.card{background:var(--bg-card);border:1px solid var(--border);border-radius:12px;padding:20px;margin-bottom:16px}
+.card h3{font-size:14px;font-weight:700;margin-bottom:12px;display:flex;align-items:center;gap:8px}
+.card .chart-wrap{position:relative;width:100%;max-height:320px}
+.card canvas{max-height:320px}
+
+/* Tables */
+.admin-table{width:100%;border-collapse:collapse;font-size:13px}
+.admin-table thead{border-bottom:2px solid var(--border)}
+.admin-table th{padding:10px 8px;text-align:left;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.5px;color:var(--text-muted)}
+.admin-table td{padding:10px 8px;border-bottom:1px solid var(--border)}
+.admin-table tbody tr:hover{background:rgba(139,92,246,0.03)}
+
+/* Buttons */
+.btn{padding:8px 16px;border:none;border-radius:8px;font-size:12px;font-weight:600;cursor:pointer;transition:all .15s;display:inline-flex;align-items:center;gap:6px}
+.btn-primary{background:var(--accent);color:var(--on-accent,#fff)}
+.btn-primary:hover{background:var(--accent-dark)}
+.btn-outline{background:transparent;border:1px solid var(--border);color:var(--text)}
+.btn-outline:hover{border-color:var(--accent-dark);color:var(--accent-light)}
+.btn-danger{background:rgba(231,76,60,0.1);color:var(--live-red);border:1px solid rgba(231,76,60,0.2)}
+.btn-danger:hover{background:rgba(231,76,60,0.2)}
+.btn-success{background:rgba(46,204,113,0.1);color:var(--success);border:1px solid rgba(46,204,113,0.2)}
+.btn-success:hover{background:rgba(46,204,113,0.2)}
+.btn-sm{padding:5px 10px;font-size:11px}
+.btn:disabled{opacity:.4;cursor:not-allowed}
+
+/* Form fields */
+.form-field{margin-bottom:14px}
+.form-field label{display:block;font-size:11px;font-weight:600;color:var(--text-muted);text-transform:uppercase;letter-spacing:.5px;margin-bottom:5px}
+.form-field input,.form-field select,.form-field textarea{
+ width:100%;padding:10px 12px;background:var(--bg-input);border:1px solid var(--border);
+ border-radius:8px;color:var(--text);font-size:13px;outline:none;transition:border-color .2s;
+}
+.form-field input:focus,.form-field select:focus,.form-field textarea:focus{border-color:var(--accent)}
+
+/* Input inline */
+.input-row{display:flex;gap:8px;align-items:center;margin-bottom:12px;flex-wrap:wrap}
+.input-row input,.input-row select{
+ padding:8px 12px;background:var(--bg-input);border:1px solid var(--border);border-radius:8px;
+ color:var(--text);font-size:13px;outline:none;flex:1;min-width:160px;
+}
+.input-row input:focus{border-color:var(--accent)}
+
+/* Badges */
+.badge{display:inline-block;padding:2px 8px;border-radius:4px;font-size:10px;font-weight:600;text-transform:uppercase}
+.badge-success{background:rgba(46,204,113,0.15);color:#2ecc71}
+.badge-danger{background:rgba(231,76,60,0.15);color:#e74c3c}
+.badge-warning{background:rgba(245,158,11,0.15);color:#f59e0b}
+.badge-info{background:rgba(52,152,219,0.15);color:#3498db}
+.badge-admin{background:rgba(231,76,60,0.15);color:#e74c3c}
+.badge-global_mod{background:rgba(155,89,182,0.15);color:#9b59b6}
+.badge-streamer{background:rgba(155,89,182,0.15);color:#9b59b6}
+.badge-user{background:rgba(52,152,219,0.15);color:#3498db}
+
+/* Loading */
+.muted{color:var(--text-muted);font-size:13px}
+.loading{text-align:center;padding:40px;color:var(--text-muted)}
+
+/* Toast */
+.toast-container{position:fixed;top:60px;right:16px;z-index:200000;display:flex;flex-direction:column;gap:8px}
+.toast{padding:12px 18px;border-radius:8px;font-size:13px;font-weight:600;color:#fff;animation:toast-in .25s ease;box-shadow:var(--shadow)}
+.toast-success{background:#2ecc71}
+.toast-error{background:#e74c3c}
+.toast-info{background:#3498db}
+.toast-warning{background:#f59e0b}
+@keyframes toast-in{from{opacity:0;transform:translateX(20px)}to{opacity:1;transform:translateX(0)}}
+
+/* Sub-tabs */
+.sub-tabs{display:flex;gap:4px;margin-bottom:16px;overflow-x:auto;-webkit-overflow-scrolling:touch;flex-wrap:wrap}
+.sub-tabs button{padding:6px 12px;border:1px solid var(--border);border-radius:6px;background:transparent;color:var(--text-muted);font-size:11px;font-weight:600;cursor:pointer}
+.sub-tabs button.active{background:var(--bg-card);color:var(--accent-light);border-color:var(--accent-dark)}
+
+/* Setting rows */
+.setting-row{display:flex;align-items:center;gap:12px;padding:10px 0;border-bottom:1px solid var(--border)}
+.setting-row label{flex:1}
+.setting-row label strong{font-size:13px}
+.setting-row label small{font-size:11px;color:var(--text-muted)}
+
+/* Storage bars */
+.storage-bar{background:var(--bg-tertiary);border-radius:4px;height:14px;overflow:hidden;position:relative}
+.storage-bar-fill{height:100%;border-radius:4px;transition:width .5s}
+
+/* Sensitive fields */
+.sensitive-field{display:flex;gap:6px;align-items:center;flex-wrap:wrap}
+.sensitive-field input{flex:1}
+.sensitive-actions{display:flex;gap:4px}
+
+@media(max-width:800px){
+ .page{padding:12px}
+ .stats-grid{grid-template-columns:repeat(2,1fr)}
+ #analytics-overview > div[style*="grid-template-columns:1fr 1fr"],
+ #analytics-service-detail > div[style*="grid-template-columns:1fr 1fr"],
+ #analytics-service-detail > div[style*="grid-template-columns:1fr 1fr 1fr"]{display:block!important}
+ #analytics-overview > div[style*="grid-template-columns"] > .card,
+ #analytics-service-detail > div[style*="grid-template-columns"] > .card{margin-bottom:12px}
+}
+@media(max-width:500px){
+ .stats-grid{grid-template-columns:1fr}
+ .admin-table{font-size:11px}
+ .section-tabs-shell{gap:6px}
+ .tabs-scroll-btn{width:30px;height:30px;flex-basis:30px;border-radius:8px}
+}
+.ssh-prompt{background:var(--bg-elev,#111);border:1px solid var(--border,#2a2a2a);border-radius:8px;padding:14px;white-space:pre-wrap;word-break:break-word;font-family:ui-monospace,Menlo,Consolas,monospace;font-size:0.8rem;line-height:1.55;color:var(--text,#ddd);max-height:520px;overflow:auto;margin:0}
+#sec-ssh code{background:var(--bg-elev,#111);border:1px solid var(--border,#2a2a2a);border-radius:5px;padding:2px 7px;font-family:ui-monospace,Menlo,Consolas,monospace;font-size:0.8rem;display:inline-block;margin-top:3px;word-break:break-all}
+#sec-ssh .staff-pill{font-size:0.62rem;font-weight:700;text-transform:uppercase;letter-spacing:.04em;padding:3px 8px;border-radius:999px;vertical-align:middle;margin-left:8px;color:var(--warning,#f0b74a);background:color-mix(in srgb,var(--warning,#f0b74a) 18%,transparent);border:1px solid color-mix(in srgb,var(--warning,#f0b74a) 40%,transparent)}
+#sec-ssh .staff-pill.user-pill{color:var(--accent,#6ea8ff);background:color-mix(in srgb,var(--accent,#6ea8ff) 18%,transparent);border-color:color-mix(in srgb,var(--accent,#6ea8ff) 40%,transparent)}
new file mode 100644
new file mode 100644
@@ -0,0 +1,236 @@
@@ -0,0 +1,236 @@
+
+*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
+:root{
+ --bg:#0d0d14;--bg-card:#16161f;--bg-input:#1c1c28;
+ --border:#2a2a3a;--border-focus:#8b5cf6;
+ --accent:#3b82f6;--accent-glow:rgba(139,92,246,0.4);--accent-dark:#1d4ed8;
+ --text:#e0e0e0;--text-muted:#8e8ea0;--text-dim:#505060;
+ --live-red:#ef4444;--success:#22c55e;
+}
+html{min-height:100%;background:var(--bg);color:var(--text);font-family:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;overflow-x:hidden;overflow-y:auto}
+body{min-height:100dvh;background:var(--bg);color:var(--text);font-family:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;overflow-x:hidden;overflow-y:auto}
+
+/* Animated background */
+.bg-canvas{position:fixed;inset:0;z-index:0}
+.bg-canvas .orb{
+ position:absolute;border-radius:50%;filter:blur(100px);opacity:0.15;
+ animation:orb-drift 20s ease-in-out infinite alternate;
+}
+.bg-canvas .orb:nth-child(1){width:400px;height:400px;background:#8b5cf6;top:-100px;left:-100px;animation-delay:0s}
+.bg-canvas .orb:nth-child(2){width:500px;height:500px;background:#e74c3c;bottom:-150px;right:-150px;animation-delay:-5s}
+.bg-canvas .orb:nth-child(3){width:300px;height:300px;background:#9b59b6;top:40%;left:60%;animation-delay:-10s}
+.bg-canvas .orb:nth-child(4){width:250px;height:250px;background:#3498db;bottom:20%;left:10%;animation-delay:-15s}
+@keyframes orb-drift{
+ 0%{transform:translate(0,0) scale(1)}25%{transform:translate(40px,-30px) scale(1.1)}
+ 50%{transform:translate(-20px,50px) scale(0.95)}75%{transform:translate(30px,20px) scale(1.05)}
+ 100%{transform:translate(-40px,-40px) scale(1)}
+}
+
+/* Particle field */
+.particles{position:fixed;inset:0;z-index:1;pointer-events:none;overflow:hidden}
+.particles .p{
+ position:absolute;width:2px;height:2px;background:rgba(139,92,246,0.3);border-radius:50%;
+ animation:particle-float linear infinite;
+}
+@keyframes particle-float{
+ from{transform:translateY(100vh) scale(0);opacity:0}
+ 10%{opacity:1;transform:translateY(90vh) scale(1)}
+ 90%{opacity:.6}
+ to{transform:translateY(-5vh) scale(0.5);opacity:0}
+}
+
+/* Grid lines */
+.grid-overlay{
+ position:fixed;inset:0;z-index:0;
+ background-image:
+ linear-gradient(rgba(139,92,246,0.02) 1px,transparent 1px),
+ linear-gradient(90deg,rgba(139,92,246,0.02) 1px,transparent 1px);
+ background-size:60px 60px;
+ animation:grid-scroll 30s linear infinite;
+}
+@keyframes grid-scroll{from{background-position:0 0}to{background-position:60px 60px}}
+
+/* Container */
+.login-container{
+ position:relative;z-index:10;min-height:100dvh;display:flex;align-items:center;justify-content:center;padding:20px;
+}
+
+/* Card */
+.login-card{
+ width:100%;max-width:420px;background:var(--bg-card);border:1px solid var(--border);
+ border-radius:20px;padding:40px 36px;position:relative;overflow:hidden;
+ box-shadow:0 20px 80px rgba(0,0,0,0.5), 0 0 60px rgba(139,92,246,0.03);
+ animation:card-entrance .6s cubic-bezier(.34,1.56,.64,1);
+}
+@keyframes card-entrance{from{transform:scale(.92) translateY(20px);opacity:0}to{transform:scale(1) translateY(0);opacity:1}}
+
+.login-card::before{
+ content:'';position:absolute;top:0;left:0;right:0;height:3px;
+ background:linear-gradient(90deg,transparent,var(--accent),transparent);
+ animation:shimmer 3s ease-in-out infinite;
+}
+@keyframes shimmer{0%,100%{opacity:0.3}50%{opacity:1}}
+
+/* Logo */
+.logo{text-align:center;margin-bottom:28px}
+.logo .flame{display:block;margin:0 auto 8px;width:72px;height:72px;color:var(--accent-light,var(--accent,#a78bfa))}
+@keyframes flame-pulse{0%,100%{transform:scale(1);filter:brightness(1)}50%{transform:scale(1.08);filter:brightness(1.2)}}
+.logo h1{font-size:22px;font-weight:800;letter-spacing:-.5px;margin-bottom:4px}
+.logo p{font-size:12px;color:var(--text-muted);font-weight:500}
+
+/* Tabs */
+.tab-bar{display:flex;gap:4px;background:var(--bg-input);border-radius:10px;padding:4px;margin-bottom:24px}
+.tab-bar button{
+ flex:1;padding:8px;border:none;border-radius:8px;font-size:13px;font-weight:600;
+ background:transparent;color:var(--text-muted);cursor:pointer;transition:all .2s;
+}
+.tab-bar button.active{background:var(--accent-dark);color:#fff;box-shadow:0 2px 8px rgba(139,92,246,0.3)}
+.tab-bar button:hover:not(.active){color:var(--text)}
+
+/* Forms */
+.form-panel{display:none;animation:panel-in .3s ease}
+.form-panel.active{display:block}
+@keyframes panel-in{from{transform:translateX(10px);opacity:0}to{transform:translateX(0);opacity:1}}
+
+.field{margin-bottom:16px;position:relative}
+.field label{display:block;font-size:11px;font-weight:600;color:var(--text-muted);text-transform:uppercase;letter-spacing:.5px;margin-bottom:6px}
+.field input{
+ width:100%;padding:12px 14px;background:var(--bg-input);border:1px solid var(--border);
+ border-radius:10px;color:var(--text);font-size:14px;outline:none;
+ transition:border-color .2s, box-shadow .2s;
+}
+.field input:focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-glow)}
+.field input::placeholder{color:var(--text-dim)}
+
+.field .toggle-password{
+ position:absolute;right:12px;top:30px;background:none;border:none;
+ color:var(--text-muted);cursor:pointer;font-size:14px;padding:4px;
+}
+
+.submit-btn{
+ width:100%;padding:14px;border:none;border-radius:10px;font-size:14px;font-weight:700;
+ background:linear-gradient(135deg,var(--accent),var(--accent-dark));color:#fff;cursor:pointer;
+ transition:all .2s;position:relative;overflow:hidden;
+}
+.submit-btn:hover{transform:translateY(-1px);box-shadow:0 6px 20px rgba(139,92,246,0.35)}
+.submit-btn:active{transform:translateY(0)}
+.submit-btn.loading{pointer-events:none;opacity:.7}
+.submit-btn .ripple{
+ position:absolute;border-radius:50%;background:rgba(255,255,255,0.3);
+ transform:scale(0);animation:btn-ripple .6s ease-out;
+}
+@keyframes btn-ripple{to{transform:scale(4);opacity:0}}
+
+/* Messages */
+.message{padding:10px 14px;border-radius:8px;font-size:12px;font-weight:500;margin-bottom:16px;display:none;animation:msg-in .3s ease}
+.message.visible{display:block}
+.message.error{background:rgba(231,76,60,0.1);border:1px solid rgba(231,76,60,0.3);color:#e74c3c}
+.message.success{background:rgba(46,204,113,0.1);border:1px solid rgba(46,204,113,0.3);color:#2ecc71}
+@keyframes msg-in{from{transform:translateY(-4px);opacity:0}to{transform:translateY(0);opacity:1}}
+
+/* Extras */
+.divider{text-align:center;position:relative;margin:24px 0}
+.divider::before{content:'';position:absolute;top:50%;left:0;right:0;height:1px;background:var(--border)}
+.divider span{position:relative;background:var(--bg-card);padding:0 12px;font-size:11px;color:var(--text-dim);font-weight:500}
+
+.anon-btn{
+ width:100%;padding:12px;border:1px dashed var(--border);border-radius:10px;
+ background:transparent;color:var(--text-muted);font-size:13px;font-weight:500;
+ cursor:pointer;transition:all .2s;
+}
+.anon-btn:hover{border-color:var(--accent-dark);color:var(--text);background:rgba(139,92,246,0.04)}
+
+.services{text-align:center;margin-top:24px;padding-top:16px;border-top:1px solid var(--border)}
+.services p{font-size:11px;color:var(--text-dim);margin-bottom:10px}
+.services .icons{display:flex;justify-content:center;gap:10px}
+.services .icons span{font-size:16px;opacity:0.4;transition:opacity .2s}
+.services .icons span:hover{opacity:0.8}
+.inline-link{display:inline-flex;align-items:center;gap:6px;margin-top:12px;font-size:12px;color:var(--accent);cursor:pointer;text-decoration:none;font-weight:600}
+.inline-link:hover{color:#a78bfa}
+.panel-note{font-size:12px;color:var(--text-muted);line-height:1.5;margin-bottom:16px}
+
+/* Toast */
+.login-toast{
+ position:fixed;top:20px;right:20px;z-index:100;padding:14px 20px;
+ border-radius:10px;font-size:13px;font-weight:500;
+ transform:translateX(120%);transition:transform .3s cubic-bezier(.34,1.56,.64,1);
+}
+.login-toast.show{transform:translateX(0)}
+.login-toast.error{background:rgba(231,76,60,0.9);color:#fff}
+.login-toast.success{background:rgba(46,204,113,0.9);color:#fff}
+
+/* ── Account Chooser ───────────────────────────────────── */
+.chooser-subtitle{font-size:13px;color:var(--text-muted);text-align:center;margin-bottom:20px;line-height:1.5}
+.chooser-subtitle .app-name{color:var(--accent);font-weight:700}
+.oauth-consent{margin:0 0 20px;padding:14px 16px;border:1px solid var(--border);border-radius:12px;background:var(--bg-input);font-size:13px;line-height:1.5;text-align:left}
+.oauth-consent .consent-head{margin:0 0 10px;color:var(--text)}
+.oauth-consent .consent-head b{color:var(--accent)}
+.oauth-consent .consent-host{color:var(--text-muted)}
+.oauth-consent ul{list-style:none;margin:0;padding:0;display:grid;gap:8px}
+.oauth-consent li{padding:8px 10px;border-left:3px solid var(--border);border-radius:6px;background:rgba(255,255,255,0.02)}
+.oauth-consent li.sensitive{border-left-color:#f59e0b}
+.oauth-consent .consent-name{font-weight:600;color:var(--text)}
+.oauth-consent .consent-badge{display:inline-block;margin-left:6px;padding:1px 7px;border-radius:999px;font-size:11px;font-weight:700;color:#1a1204;background:#f59e0b;vertical-align:1px}
+.oauth-consent small{display:block;margin-top:2px;color:var(--text-muted);font-size:12px}
+.oauth-consent .consent-none,.oauth-consent .consent-refused{margin:0;color:var(--text-muted)}
+.oauth-consent .consent-refused{margin-top:10px;font-size:12px}
+.oauth-consent code{font-size:11.5px}
+
+.account-list{display:flex;flex-direction:column;gap:8px;margin-bottom:16px}
+.account-item{
+ display:flex;align-items:center;gap:12px;padding:12px 14px;
+ background:var(--bg-input);border:1px solid var(--border);border-radius:12px;
+ cursor:pointer;transition:all .2s;position:relative;
+}
+.account-item:hover{border-color:var(--accent);background:rgba(139,92,246,0.06);transform:translateX(2px)}
+.account-item.active-account{border-color:var(--accent);box-shadow:0 0 0 2px var(--accent-glow)}
+.account-item .avatar{
+ width:40px;height:40px;border-radius:50%;background:var(--border);
+ display:flex;align-items:center;justify-content:center;font-size:18px;
+ flex-shrink:0;overflow:hidden;
+}
+.account-item .avatar img{width:100%;height:100%;object-fit:cover}
+.account-item .info{flex:1;min-width:0}
+.account-item .info .name{font-size:14px;font-weight:600;color:var(--text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
+.account-item .info .detail{font-size:11px;color:var(--text-muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
+.account-item .arrow{color:var(--text-dim);font-size:14px;flex-shrink:0;transition:color .2s}
+.account-item:hover .arrow{color:var(--accent)}
+.account-item .remove-btn{
+ position:absolute;top:6px;right:6px;width:20px;height:20px;border:none;
+ background:rgba(231,76,60,0.15);border-radius:50%;color:#e74c3c;font-size:10px;
+ cursor:pointer;display:none;align-items:center;justify-content:center;
+ transition:background .2s;line-height:1;
+}
+.account-item:hover .remove-btn{display:flex}
+.account-item .remove-btn:hover{background:rgba(231,76,60,0.3)}
+
+.chooser-actions{display:flex;flex-direction:column;gap:8px}
+.chooser-actions .action-btn{
+ display:flex;align-items:center;justify-content:center;gap:8px;padding:12px;
+ border:1px dashed var(--border);border-radius:10px;background:transparent;
+ color:var(--text-muted);font-size:13px;font-weight:500;cursor:pointer;transition:all .2s;
+}
+.chooser-actions .action-btn:hover{border-color:var(--accent-dark);color:var(--text);background:rgba(139,92,246,0.04)}
+
+.back-link{
+ display:block;text-align:center;margin-top:16px;font-size:12px;
+ color:var(--text-muted);cursor:pointer;text-decoration:none;transition:color .2s;
+}
+.back-link:hover{color:var(--accent)}
+
+@media(max-width:480px){
+ .login-card{padding:28px 24px;border-radius:16px}
+ .logo .flame{width:56px;height:56px}
+ .logo .flame .ov-mark{width:56px!important;height:56px!important}
+ .logo h1{font-size:18px}
+ .login-container{padding:16px}
+}
+
+@media(max-height:820px){
+ .login-container{align-items:flex-start;padding-top:24px;padding-bottom:24px}
+}
+
+@media(max-width:380px){
+ .login-card{padding:24px 18px;border-radius:14px}
+ .tab-bar button{font-size:12px;padding:8px 6px}
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,248 @@
@@ -0,0 +1,248 @@
+
+*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
+:root{
+ --bg:#0d0d14;--bg-secondary:#151520;--bg-card:#1a1a26;--bg-input:#1c1c28;
+ --border:#2a2a3a;--border-focus:#8b5cf6;
+ --accent:#3b82f6;--accent-glow:rgba(139,92,246,0.4);--accent-dark:#1d4ed8;--accent-light:#60a5fa;
+ --text:#e0e0e0;--text-secondary:#b0b0b8;--text-muted:#707080;
+ --live-red:#ef4444;--success:#22c55e;
+ --shadow:0 4px 20px rgba(0,0,0,0.3);
+}
+html{height:100%;background:var(--bg)}
+body{min-height:100%;color:var(--text);font-family:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;padding-bottom:40px}
+
+/* Layout */
+.page{max-width:900px;margin:0 auto;padding:20px}
+.page-header{display:flex;align-items:center;gap:16px;margin-bottom:32px}
+.page-header .avatar{width:72px;height:72px;border-radius:50%;border:3px solid var(--accent);object-fit:cover}
+.page-header .info{flex:1}
+.page-header .info h1{font-size:22px;font-weight:800;margin-bottom:2px}
+.page-header .info .subtitle{font-size:13px;color:var(--text-muted)}
+.page-header .info .badges{display:flex;gap:6px;margin-top:6px}
+.page-header .info .badges .badge{
+ font-size:10px;padding:3px 10px;border-radius:4px;font-weight:600;text-transform:uppercase;letter-spacing:.5px;
+}
+.badge-admin{background:rgba(231,76,60,0.15);color:#e74c3c}
+.badge-anon{background:rgba(139,92,246,0.15);color:var(--accent-light)}
+.badge-streamer{background:rgba(155,89,182,0.15);color:#9b59b6}
+.badge-user{background:rgba(52,152,219,0.15);color:#3498db}
+
+/* Section tabs */
+.section-tabs{display:flex;gap:4px;margin-bottom:24px;background:var(--bg-secondary);border-radius:10px;padding:4px;overflow-x:auto}
+.section-tabs button{
+ flex-shrink:0;padding:8px 16px;border:none;border-radius:8px;font-size:12px;font-weight:600;
+ background:transparent;color:var(--text-muted);cursor:pointer;transition:all .15s;white-space:nowrap;
+}
+.section-tabs button.active{background:var(--bg-card);color:var(--accent-light)}
+.section-tabs button:hover:not(.active){color:var(--text)}
+
+/* Approvals (my/approvals.js): one row per waiting request, and the count on its tab */
+.section-tabs .tab-badge{display:inline-block;min-width:18px;padding:0 6px;border-radius:9px;background:var(--accent);color:#fff;font-size:11px;line-height:18px;text-align:center}
+.approval{border:1px solid var(--border);border-radius:10px;padding:14px 16px;margin-bottom:10px;background:var(--bg-secondary)}
+.approval-summary{font-weight:600;color:var(--text);margin-bottom:6px;overflow-wrap:anywhere}
+.approval-meta{font-size:12px;color:var(--text-secondary);margin-bottom:10px;overflow-wrap:anywhere}
+.approval-who{color:var(--text);font-weight:600}
+.approval-cap{font-size:11px;padding:1px 6px;border-radius:5px;background:var(--bg-input);border:1px solid var(--border)}
+.approval-resources{list-style:none;display:flex;flex-wrap:wrap;gap:6px;margin:0 0 10px;padding:0}
+.approval-resources li{font-size:11px;padding:2px 8px;border-radius:6px;background:var(--bg-input);color:var(--text-secondary);overflow-wrap:anywhere}
+.approval .btn-row{display:flex;gap:8px;flex-wrap:wrap}
+.approvals-empty{color:var(--text-secondary);font-size:13px}
+
+/* Section */
+.section{display:none;animation:sec-in .2s ease}
+.section.active{display:block}
+@keyframes sec-in{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}
+
+/* Cards */
+.card{background:var(--bg-card);border:1px solid var(--border);border-radius:12px;padding:20px;margin-bottom:16px}
+.card h3{font-size:14px;font-weight:700;margin-bottom:12px;display:flex;align-items:center;gap:8px}
+.card h3 .icon{font-size:16px}
+.card p{font-size:13px;color:var(--text-secondary);line-height:1.6}
+
+/* Form fields */
+.form-field{margin-bottom:14px}
+.form-field label{display:block;font-size:11px;font-weight:600;color:var(--text-muted);text-transform:uppercase;letter-spacing:.5px;margin-bottom:5px}
+/* Every text field on the page follows the active theme: its input colour, a hover tint and an accent focus ring. */
+:where(input:not([type]),input[type=text],input[type=url],input[type=email],input[type=search],input[type=password],input[type=number],select,textarea){
+ padding:10px 12px;background:var(--bg-input);border:1px solid var(--border);border-radius:10px;
+ color:var(--text);font:inherit;font-size:13px;outline:none;min-height:40px;
+ transition:border-color .18s ease,box-shadow .18s ease,background-color .18s ease;
+}
+:where(input:not([type]),input[type=text],input[type=url],input[type=email],input[type=search],input[type=password],input[type=number],select,textarea):hover{border-color:color-mix(in srgb,var(--accent) 40%,var(--border))}
+:where(input:not([type]),input[type=text],input[type=url],input[type=email],input[type=search],input[type=password],input[type=number],select,textarea):focus{
+ border-color:var(--accent);box-shadow:0 0 0 3px color-mix(in srgb,var(--accent) 24%,transparent);
+ background:color-mix(in srgb,var(--accent) 5%,var(--bg-input));
+}
+:where(input,textarea)::placeholder{color:var(--text-muted);opacity:1}
+:where(input,select,textarea):user-invalid{border-color:var(--danger,#ef4444)}
+.form-field input,.form-field select,.form-field textarea{width:100%}
+.form-field textarea{resize:vertical;min-height:60px}
+.form-field .hint{font-size:10px;color:var(--text-muted);margin-top:3px}
+.theme-submit{display:grid;grid-template-columns:repeat(2,minmax(0,1fr)) auto;gap:12px;align-items:start;margin-top:16px}
+.theme-submit .form-field{margin:0}
+.theme-submit .btn{margin-top:21px;min-height:40px}
+@media (max-width:640px){.theme-submit{grid-template-columns:1fr}.theme-submit .btn{margin-top:0}}
+
+.btn{
+ padding:8px 20px;border:none;border-radius:8px;font-size:12px;font-weight:600;cursor:pointer;transition:all .15s;
+}
+.btn-primary{background:var(--accent);color:var(--on-accent,#fff)}
+.btn-primary:hover{background:var(--accent-dark)}
+.btn-outline{background:transparent;border:1px solid var(--border);color:var(--text)}
+.btn-outline:hover{border-color:var(--accent-dark);color:var(--accent-light)}
+.btn-danger{background:rgba(231,76,60,0.1);color:var(--live-red);border:1px solid rgba(231,76,60,0.2)}
+.btn-danger:hover{background:rgba(231,76,60,0.2)}
+.btn-row{display:flex;gap:8px;margin-top:12px}
+.data-check{display:flex;gap:8px;align-items:flex-start;font-size:13px;color:var(--text-secondary);line-height:1.5}
+.data-check input{margin-top:3px}
+.data-status{align-self:center;font-size:12px;color:var(--text-secondary)}
+.module-row{display:flex;gap:12px;align-items:flex-start;padding:10px 0;border-top:1px solid var(--border)}
+.module-row:first-child{border-top:0}
+.module-row-main{flex:1;min-width:0}
+.module-row-main strong{display:block;font-size:13px}
+.module-row-main small{display:block;font-size:12px;color:var(--text-muted);line-height:1.5}
+.module-row-main code{display:block;margin-top:4px;font-size:11px;color:var(--text-secondary);white-space:pre-wrap;word-break:break-word;max-height:7.5em;overflow:auto}
+
+/* Account list */
+.account-list{display:flex;flex-direction:column;gap:8px}
+.account-item{
+ display:flex;align-items:center;gap:12px;padding:12px;border-radius:10px;
+ background:var(--bg-input);border:1px solid var(--border);
+}
+.account-item.active{border-color:var(--accent)}
+.account-item img{width:36px;height:36px;border-radius:50%}
+.account-item .name{font-size:13px;font-weight:600}
+.account-item .meta{font-size:11px;color:var(--text-muted)}
+.account-item .actions{margin-left:auto;display:flex;gap:6px}
+
+/* Notification pref grid */
+.notif-grid{display:grid;grid-template-columns:1fr auto auto auto auto;gap:8px;align-items:center;font-size:12px}
+.notif-grid .cat{font-weight:600;text-transform:capitalize}
+.notif-grid .toggle{width:36px;height:20px;border-radius:10px;border:none;cursor:pointer;position:relative;transition:background .2s}
+.notif-grid .toggle::after{content:'';position:absolute;width:16px;height:16px;border-radius:50%;background:#fff;top:2px;left:2px;transition:transform .2s}
+.notif-grid .toggle.on{background:var(--accent)}
+.notif-grid .toggle.on::after{transform:translateX(16px)}
+.notif-grid .toggle.off{background:var(--border)}
+
+/* Linked services */
+.linked-item{display:flex;align-items:center;gap:12px;padding:12px;border-radius:8px;background:var(--bg-input);margin-bottom:8px;text-decoration:none;color:inherit;transition:transform .15s,background .15s}
+a.linked-item:hover{background:var(--bg-hover,var(--bg-card));transform:translateX(2px)}
+.linked-item .svc-meta{margin-left:auto;text-align:right;font-size:11px;color:var(--text-muted);white-space:nowrap}
+.linked-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:8px;margin-top:14px}
+.linked-grid a{display:flex;align-items:center;gap:8px;padding:10px 12px;border-radius:8px;border:1px dashed var(--border);font-size:12px;color:var(--text-secondary);text-decoration:none;transition:border-color .15s,color .15s}
+.linked-grid a:hover{border-color:var(--accent);color:var(--text)}
+.hist-toolbar{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin-bottom:12px}
+.hist-chips{display:flex;flex-wrap:wrap;gap:6px}
+.hist-chips button{padding:5px 10px;border-radius:999px;border:1px solid var(--border);background:transparent;color:var(--text-secondary);font-size:12px;cursor:pointer;transition:all .15s}
+.hist-chips button.active,.hist-chips button:hover{border-color:var(--accent);color:var(--accent-light);background:rgba(var(--accent-rgb,139,92,246),.08)}
+.hist-search{flex:1;min-width:160px;padding:7px 10px;border-radius:8px;border:1px solid var(--border);background:var(--bg-input);color:var(--text);font-size:13px}
+.hist-pause{display:inline-flex;align-items:center;gap:6px;font-size:12px;color:var(--text-secondary);cursor:pointer}
+.btn-small{padding:6px 10px;font-size:12px}
+.hist-day{font-size:11px;font-weight:700;letter-spacing:.6px;text-transform:uppercase;color:var(--text-muted);margin:14px 0 6px}
+.hist-item{display:flex;align-items:center;gap:12px;padding:9px 10px;border-radius:8px;text-decoration:none;color:inherit;transition:background .12s;animation:sec-in .25s ease both}
+.hist-item:hover{background:var(--bg-input)}
+.hist-item .hi{width:34px;height:34px;border-radius:9px;display:grid;place-items:center;background:rgba(var(--accent-rgb,139,92,246),.12);color:var(--accent-light);flex:none}
+.hist-item .ht{font-size:13px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
+.hist-item .hs{font-size:11px;color:var(--text-muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
+.hist-item .hx{margin-left:auto;display:flex;align-items:center;gap:10px;flex:none;font-size:11px;color:var(--text-muted)}
+.hist-item .hx button{background:none;border:none;color:var(--text-muted);cursor:pointer;padding:4px;opacity:0;transition:opacity .15s}
+.hist-item:hover .hx button{opacity:1}
+.hist-item .hx button:hover{color:var(--danger,#ef4444)}
+.hist-empty{padding:26px;text-align:center;color:var(--text-muted);font-size:13px}
+.linked-item .svc-icon{font-size:20px;width:32px;text-align:center}
+.linked-item .svc-name{font-size:13px;font-weight:600}
+.linked-item .svc-user{font-size:11px;color:var(--text-muted)}
+.linked-item .svc-status{margin-left:auto;font-size:11px;font-weight:600;color:var(--success)}
+
+/* Themes */
+.theme-hero{display:flex;justify-content:space-between;gap:16px;align-items:flex-start;flex-wrap:wrap;margin-bottom:18px}
+.theme-hero-copy{max-width:560px}
+.theme-hero-copy p{margin-top:8px}
+.theme-current{display:flex;align-items:center;gap:10px;padding:12px 14px;border-radius:10px;background:var(--bg-input);border:1px solid var(--border);min-width:220px}
+.theme-current-swatch{display:flex;gap:4px}
+.theme-current-swatch span,.theme-preview-swatch span{width:18px;height:18px;border-radius:50%;border:1px solid rgba(255,255,255,.08);display:inline-block}
+.theme-current .label{font-size:10px;color:var(--text-muted);text-transform:uppercase;letter-spacing:.08em}
+.theme-current .name{font-size:13px;font-weight:700;margin-top:2px}
+.theme-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:12px}
+.theme-custom-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:10px}
+.theme-custom-row{display:flex;align-items:center;gap:8px;font-size:13px;color:var(--text-secondary);background:var(--bg-input);border:1px solid var(--border);border-radius:10px;padding:8px 10px;cursor:pointer}
+.theme-custom-row input[type=color]{width:34px;height:34px;border:none;border-radius:8px;background:none;cursor:pointer;padding:0}
+.theme-custom-row input[type=color]::-webkit-color-swatch{border:1px solid var(--border);border-radius:6px}
+.theme-custom-row input[type=color]::-webkit-color-swatch-wrapper{padding:0}
+.theme-card{background:var(--bg-input);border:1px solid var(--border);border-radius:14px;padding:14px;display:flex;flex-direction:column;gap:12px;transition:border-color .15s,transform .15s,box-shadow .15s}
+.theme-card:hover{transform:translateY(-2px);border-color:var(--accent)}
+.theme-card.active{border-color:var(--accent);box-shadow:0 0 0 1px rgba(139,92,246,.25) inset}
+.theme-preview{border-radius:12px;padding:14px;background:linear-gradient(135deg,var(--theme-bg-1,#202331),var(--theme-bg-2,#181a24));border:1px solid rgba(255,255,255,.06)}
+.theme-preview-top{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:12px}
+.theme-preview-chip{padding:5px 9px;border-radius:999px;background:rgba(255,255,255,.08);color:#fff;font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.08em}
+.theme-preview-bars{display:grid;gap:7px}
+.theme-preview-bars span{display:block;height:8px;border-radius:999px;background:rgba(255,255,255,.16)}
+.theme-preview-bars span:first-child{width:72%}
+.theme-preview-bars span:last-child{width:46%}
+.theme-card h4{font-size:14px;font-weight:700}
+.theme-card p{font-size:12px;color:var(--text-secondary);line-height:1.55}
+.theme-card-footer{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-top:auto}
+.theme-badges{display:flex;gap:6px;flex-wrap:wrap}
+.theme-badge{font-size:10px;padding:4px 8px;border-radius:999px;background:rgba(255,255,255,.05);color:var(--text-muted);text-transform:uppercase;letter-spacing:.08em;font-weight:700}
+.theme-empty{padding:16px;border-radius:12px;background:var(--bg-input);border:1px dashed var(--border);text-align:center;color:var(--text-muted);font-size:13px}
+
+/* Anon info card */
+.anon-card{text-align:center;padding:32px 20px}
+.anon-card .number{font-size:48px;font-weight:800;color:var(--accent);margin-bottom:8px}
+.anon-card .label{font-size:12px;color:var(--text-muted);margin-bottom:16px}
+.anon-card .stats{display:flex;justify-content:center;gap:24px}
+.anon-card .stat{text-align:center}
+.anon-card .stat .val{font-size:20px;font-weight:700}
+.anon-card .stat .key{font-size:10px;color:var(--text-muted);text-transform:uppercase}
+
+/* Loading */
+.loading-spinner{text-align:center;padding:40px;color:var(--text-muted);font-size:13px}
+.acct-retry-btn{display:inline-block;background:var(--accent,#8b5cf6);color:#0b0d10;border:none;border-radius:6px;padding:3px 12px;font-weight:600;font-size:0.85em;cursor:pointer;vertical-align:baseline}
+.acct-retry-btn:hover{filter:brightness(1.08)}
+
+@media(max-width:600px){
+ .page{padding:12px}
+ .page-header{flex-direction:column;text-align:center}
+ .page-header .info .badges{justify-content:center}
+ .notif-grid{grid-template-columns:1fr auto auto}
+ .notif-grid .col-email,.notif-grid .col-toast{display:none}
+}
+
+/* Dashboard Showcase */
+@keyframes float { 0%, 100% { transform: translateY(0px); } 50% { transform: translateY(-10px); } }
+@keyframes fadeInScale { from { opacity: 0; transform: scale(0.95); } to { opacity: 1; transform: scale(1); } }
+.dashboard-showcase { margin: 32px 0; padding: 28px; border-radius: 12px; background: var(--bg-card); border: 1px solid var(--border); }
+.showcase-header { text-align: center; margin-bottom: 24px; }
+.showcase-header h2 { font-size: 1.3rem; font-weight: 800; margin-bottom: 6px; }
+.showcase-header p { font-size: 12px; color: var(--text-muted); }
+.showcase-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; margin-bottom: 20px; }
+.showcase-card { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 16px; border-radius: 10px; background: var(--bg-input); border: 1px solid var(--border); cursor: pointer; text-decoration: none; color: inherit; gap: 8px; animation: float ease-in-out infinite; transition: all 0.3s ease; }
+.showcase-card:nth-child(1) { animation-delay: 0s; animation-duration: 3s; }
+.showcase-card:nth-child(2) { animation-delay: 0.15s; animation-duration: 3.3s; }
+.showcase-card:nth-child(3) { animation-delay: 0.3s; animation-duration: 3.6s; }
+.showcase-card:nth-child(4) { animation-delay: 0.45s; animation-duration: 3.2s; }
+.showcase-card:hover { transform: translateY(-4px); border-color: var(--accent); background: rgba(139,92,246,0.08); }
+.showcase-card-icon { font-size: 1.4rem; color: var(--accent-light); }
+.showcase-card-label { font-size: 11px; font-weight: 700; text-align: center; }
+.quick-access { display: grid; grid-template-columns: repeat(6, 1fr); gap: 8px; }
+.quick-link { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 10px; border-radius: 8px; background: var(--bg); border: 1px solid var(--border); text-decoration: none; color: var(--text-secondary); font-size: 10px; font-weight: 600; text-align: center; transition: all 0.25s ease; animation: fadeInScale 0.5s ease both; }
+.quick-link i { font-size: 1rem; margin-bottom: 4px; }
+.quick-link:nth-child(1) { animation-delay: 0.1s; }
+.quick-link:nth-child(2) { animation-delay: 0.15s; }
+.quick-link:nth-child(3) { animation-delay: 0.2s; }
+.quick-link:nth-child(4) { animation-delay: 0.25s; }
+.quick-link:nth-child(5) { animation-delay: 0.3s; }
+.quick-link:nth-child(6) { animation-delay: 0.35s; }
+.quick-link:hover { border-color: var(--accent); color: var(--accent-light); background: rgba(139,92,246,0.08); }
+@media(max-width:600px){ .showcase-grid { grid-template-columns: repeat(2, 1fr); } .quick-access { grid-template-columns: repeat(3, 1fr); } }
+.recent-tools { margin-top: 14px; }
+.recent-tools h3 { font-size: 12px; font-weight: 700; color: var(--text-secondary); margin: 0 0 8px; display: flex; gap: 6px; align-items: center; }
+.recent-tools-list { display: flex; flex-wrap: wrap; gap: 6px; }
+.recent-tool { padding: 6px 10px; border-radius: 999px; background: var(--bg); border: 1px solid var(--border); color: var(--text-secondary); font-size: 12px; font-weight: 600; text-decoration: none; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+.recent-tool:hover { border-color: var(--accent); color: var(--accent-light); }
+.recent-tools-status { margin: 6px 0 0; font-size: 12px; color: var(--text-secondary); }
+.recent-tools-status:empty { display: none; }
+.recent-tool-item { display: inline-flex; align-items: center; gap: 2px; max-width: 100%; }
+.recent-tool-star { border: 0; background: none; color: var(--text-secondary); cursor: pointer; padding: 6px; font-size: 12px; border-radius: 999px; }
+.recent-tool-star[aria-pressed="true"] { color: var(--accent-light); }
+.recent-tool-star:hover, .recent-tool-star:focus-visible { color: var(--accent-light); }
@@ -12,243 +12,7 @@
@@ -12,243 +12,7 @@
<link rel="manifest" href="/manifest.webmanifest">
<link rel="manifest" href="/manifest.webmanifest">
<meta name="description" content="Sign in to OpenVibe — one account across OpenVibe.Live, OpenVibe.Tools, OpenVibe.Games, and OpenVibe.Media.">
<meta name="description" content="Sign in to OpenVibe — one account across OpenVibe.Live, OpenVibe.Tools, OpenVibe.Games, and OpenVibe.Media.">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
-<style>
-*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
-:root{
- --bg:#0d0d14;--bg-card:#16161f;--bg-input:#1c1c28;
- --border:#2a2a3a;--border-focus:#8b5cf6;
- --accent:#3b82f6;--accent-glow:rgba(139,92,246,0.4);--accent-dark:#1d4ed8;
- --text:#e0e0e0;--text-muted:#8e8ea0;--text-dim:#505060;
- --live-red:#ef4444;--success:#22c55e;
-}
-html{min-height:100%;background:var(--bg);color:var(--text);font-family:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;overflow-x:hidden;overflow-y:auto}
-body{min-height:100dvh;background:var(--bg);color:var(--text);font-family:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;overflow-x:hidden;overflow-y:auto}
-
-/* Animated background */
-.bg-canvas{position:fixed;inset:0;z-index:0}
-.bg-canvas .orb{
- position:absolute;border-radius:50%;filter:blur(100px);opacity:0.15;
- animation:orb-drift 20s ease-in-out infinite alternate;
-}
-.bg-canvas .orb:nth-child(1){width:400px;height:400px;background:#8b5cf6;top:-100px;left:-100px;animation-delay:0s}
-.bg-canvas .orb:nth-child(2){width:500px;height:500px;background:#e74c3c;bottom:-150px;right:-150px;animation-delay:-5s}
-.bg-canvas .orb:nth-child(3){width:300px;height:300px;background:#9b59b6;top:40%;left:60%;animation-delay:-10s}
-.bg-canvas .orb:nth-child(4){width:250px;height:250px;background:#3498db;bottom:20%;left:10%;animation-delay:-15s}
-@keyframes orb-drift{
- 0%{transform:translate(0,0) scale(1)}25%{transform:translate(40px,-30px) scale(1.1)}
- 50%{transform:translate(-20px,50px) scale(0.95)}75%{transform:translate(30px,20px) scale(1.05)}
- 100%{transform:translate(-40px,-40px) scale(1)}
-}
-
-/* Particle field */
-.particles{position:fixed;inset:0;z-index:1;pointer-events:none;overflow:hidden}
-.particles .p{
- position:absolute;width:2px;height:2px;background:rgba(139,92,246,0.3);border-radius:50%;
- animation:particle-float linear infinite;
-}
-@keyframes particle-float{
- from{transform:translateY(100vh) scale(0);opacity:0}
- 10%{opacity:1;transform:translateY(90vh) scale(1)}
- 90%{opacity:.6}
- to{transform:translateY(-5vh) scale(0.5);opacity:0}
-}
-
-/* Grid lines */
-.grid-overlay{
- position:fixed;inset:0;z-index:0;
- background-image:
- linear-gradient(rgba(139,92,246,0.02) 1px,transparent 1px),
- linear-gradient(90deg,rgba(139,92,246,0.02) 1px,transparent 1px);
- background-size:60px 60px;
- animation:grid-scroll 30s linear infinite;
-}
-@keyframes grid-scroll{from{background-position:0 0}to{background-position:60px 60px}}
-
-/* Container */
-.login-container{
- position:relative;z-index:10;min-height:100dvh;display:flex;align-items:center;justify-content:center;padding:20px;
-}
-
-/* Card */
-.login-card{
- width:100%;max-width:420px;background:var(--bg-card);border:1px solid var(--border);
- border-radius:20px;padding:40px 36px;position:relative;overflow:hidden;
- box-shadow:0 20px 80px rgba(0,0,0,0.5), 0 0 60px rgba(139,92,246,0.03);
- animation:card-entrance .6s cubic-bezier(.34,1.56,.64,1);
-}
-@keyframes card-entrance{from{transform:scale(.92) translateY(20px);opacity:0}to{transform:scale(1) translateY(0);opacity:1}}
-
-.login-card::before{
- content:'';position:absolute;top:0;left:0;right:0;height:3px;
- background:linear-gradient(90deg,transparent,var(--accent),transparent);
- animation:shimmer 3s ease-in-out infinite;
-}
-@keyframes shimmer{0%,100%{opacity:0.3}50%{opacity:1}}
-
-/* Logo */
-.logo{text-align:center;margin-bottom:28px}
-.logo .flame{display:block;margin:0 auto 8px;width:72px;height:72px;color:var(--accent-light,var(--accent,#a78bfa))}
-@keyframes flame-pulse{0%,100%{transform:scale(1);filter:brightness(1)}50%{transform:scale(1.08);filter:brightness(1.2)}}
-.logo h1{font-size:22px;font-weight:800;letter-spacing:-.5px;margin-bottom:4px}
-.logo p{font-size:12px;color:var(--text-muted);font-weight:500}
-
-/* Tabs */
-.tab-bar{display:flex;gap:4px;background:var(--bg-input);border-radius:10px;padding:4px;margin-bottom:24px}
-.tab-bar button{
- flex:1;padding:8px;border:none;border-radius:8px;font-size:13px;font-weight:600;
- background:transparent;color:var(--text-muted);cursor:pointer;transition:all .2s;
-}
-.tab-bar button.active{background:var(--accent-dark);color:#fff;box-shadow:0 2px 8px rgba(139,92,246,0.3)}
-.tab-bar button:hover:not(.active){color:var(--text)}
-
-/* Forms */
-.form-panel{display:none;animation:panel-in .3s ease}
-.form-panel.active{display:block}
-@keyframes panel-in{from{transform:translateX(10px);opacity:0}to{transform:translateX(0);opacity:1}}
-
-.field{margin-bottom:16px;position:relative}
-.field label{display:block;font-size:11px;font-weight:600;color:var(--text-muted);text-transform:uppercase;letter-spacing:.5px;margin-bottom:6px}
-.field input{
- width:100%;padding:12px 14px;background:var(--bg-input);border:1px solid var(--border);
- border-radius:10px;color:var(--text);font-size:14px;outline:none;
- transition:border-color .2s, box-shadow .2s;
-}
-.field input:focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-glow)}
-.field input::placeholder{color:var(--text-dim)}
-
-.field .toggle-password{
- position:absolute;right:12px;top:30px;background:none;border:none;
- color:var(--text-muted);cursor:pointer;font-size:14px;padding:4px;
-}
-
-.submit-btn{
- width:100%;padding:14px;border:none;border-radius:10px;font-size:14px;font-weight:700;
- background:linear-gradient(135deg,var(--accent),var(--accent-dark));color:#fff;cursor:pointer;
- transition:all .2s;position:relative;overflow:hidden;
-}
-.submit-btn:hover{transform:translateY(-1px);box-shadow:0 6px 20px rgba(139,92,246,0.35)}
-.submit-btn:active{transform:translateY(0)}
-.submit-btn.loading{pointer-events:none;opacity:.7}
-.submit-btn .ripple{
- position:absolute;border-radius:50%;background:rgba(255,255,255,0.3);
- transform:scale(0);animation:btn-ripple .6s ease-out;
-}
-@keyframes btn-ripple{to{transform:scale(4);opacity:0}}
-
-/* Messages */
-.message{padding:10px 14px;border-radius:8px;font-size:12px;font-weight:500;margin-bottom:16px;display:none;animation:msg-in .3s ease}
-.message.visible{display:block}
-.message.error{background:rgba(231,76,60,0.1);border:1px solid rgba(231,76,60,0.3);color:#e74c3c}
-.message.success{background:rgba(46,204,113,0.1);border:1px solid rgba(46,204,113,0.3);color:#2ecc71}
-@keyframes msg-in{from{transform:translateY(-4px);opacity:0}to{transform:translateY(0);opacity:1}}
-
-/* Extras */
-.divider{text-align:center;position:relative;margin:24px 0}
-.divider::before{content:'';position:absolute;top:50%;left:0;right:0;height:1px;background:var(--border)}
-.divider span{position:relative;background:var(--bg-card);padding:0 12px;font-size:11px;color:var(--text-dim);font-weight:500}
-
-.anon-btn{
- width:100%;padding:12px;border:1px dashed var(--border);border-radius:10px;
- background:transparent;color:var(--text-muted);font-size:13px;font-weight:500;
- cursor:pointer;transition:all .2s;
-}
-.anon-btn:hover{border-color:var(--accent-dark);color:var(--text);background:rgba(139,92,246,0.04)}
-
-.services{text-align:center;margin-top:24px;padding-top:16px;border-top:1px solid var(--border)}
-.services p{font-size:11px;color:var(--text-dim);margin-bottom:10px}
-.services .icons{display:flex;justify-content:center;gap:10px}
-.services .icons span{font-size:16px;opacity:0.4;transition:opacity .2s}
-.services .icons span:hover{opacity:0.8}
-.inline-link{display:inline-flex;align-items:center;gap:6px;margin-top:12px;font-size:12px;color:var(--accent);cursor:pointer;text-decoration:none;font-weight:600}
-.inline-link:hover{color:#a78bfa}
-.panel-note{font-size:12px;color:var(--text-muted);line-height:1.5;margin-bottom:16px}
-
-/* Toast */
-.login-toast{
- position:fixed;top:20px;right:20px;z-index:100;padding:14px 20px;
- border-radius:10px;font-size:13px;font-weight:500;
- transform:translateX(120%);transition:transform .3s cubic-bezier(.34,1.56,.64,1);
-}
-.login-toast.show{transform:translateX(0)}
-.login-toast.error{background:rgba(231,76,60,0.9);color:#fff}
-.login-toast.success{background:rgba(46,204,113,0.9);color:#fff}
-
-/* ── Account Chooser ───────────────────────────────────── */
-.chooser-subtitle{font-size:13px;color:var(--text-muted);text-align:center;margin-bottom:20px;line-height:1.5}
-.chooser-subtitle .app-name{color:var(--accent);font-weight:700}
-.oauth-consent{margin:0 0 20px;padding:14px 16px;border:1px solid var(--border);border-radius:12px;background:var(--bg-input);font-size:13px;line-height:1.5;text-align:left}
-.oauth-consent .consent-head{margin:0 0 10px;color:var(--text)}
-.oauth-consent .consent-head b{color:var(--accent)}
-.oauth-consent .consent-host{color:var(--text-muted)}
-.oauth-consent ul{list-style:none;margin:0;padding:0;display:grid;gap:8px}
-.oauth-consent li{padding:8px 10px;border-left:3px solid var(--border);border-radius:6px;background:rgba(255,255,255,0.02)}
-.oauth-consent li.sensitive{border-left-color:#f59e0b}
-.oauth-consent .consent-name{font-weight:600;color:var(--text)}
-.oauth-consent .consent-badge{display:inline-block;margin-left:6px;padding:1px 7px;border-radius:999px;font-size:11px;font-weight:700;color:#1a1204;background:#f59e0b;vertical-align:1px}
-.oauth-consent small{display:block;margin-top:2px;color:var(--text-muted);font-size:12px}
-.oauth-consent .consent-none,.oauth-consent .consent-refused{margin:0;color:var(--text-muted)}
-.oauth-consent .consent-refused{margin-top:10px;font-size:12px}
-.oauth-consent code{font-size:11.5px}
-
-.account-list{display:flex;flex-direction:column;gap:8px;margin-bottom:16px}
-.account-item{
- display:flex;align-items:center;gap:12px;padding:12px 14px;
- background:var(--bg-input);border:1px solid var(--border);border-radius:12px;
- cursor:pointer;transition:all .2s;position:relative;
-}
-.account-item:hover{border-color:var(--accent);background:rgba(139,92,246,0.06);transform:translateX(2px)}
-.account-item.active-account{border-color:var(--accent);box-shadow:0 0 0 2px var(--accent-glow)}
-.account-item .avatar{
- width:40px;height:40px;border-radius:50%;background:var(--border);
- display:flex;align-items:center;justify-content:center;font-size:18px;
- flex-shrink:0;overflow:hidden;
-}
-.account-item .avatar img{width:100%;height:100%;object-fit:cover}
-.account-item .info{flex:1;min-width:0}
-.account-item .info .name{font-size:14px;font-weight:600;color:var(--text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
-.account-item .info .detail{font-size:11px;color:var(--text-muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
-.account-item .arrow{color:var(--text-dim);font-size:14px;flex-shrink:0;transition:color .2s}
-.account-item:hover .arrow{color:var(--accent)}
-.account-item .remove-btn{
- position:absolute;top:6px;right:6px;width:20px;height:20px;border:none;
- background:rgba(231,76,60,0.15);border-radius:50%;color:#e74c3c;font-size:10px;
- cursor:pointer;display:none;align-items:center;justify-content:center;
- transition:background .2s;line-height:1;
-}
-.account-item:hover .remove-btn{display:flex}
-.account-item .remove-btn:hover{background:rgba(231,76,60,0.3)}
-
-.chooser-actions{display:flex;flex-direction:column;gap:8px}
-.chooser-actions .action-btn{
- display:flex;align-items:center;justify-content:center;gap:8px;padding:12px;
- border:1px dashed var(--border);border-radius:10px;background:transparent;
- color:var(--text-muted);font-size:13px;font-weight:500;cursor:pointer;transition:all .2s;
-}
-.chooser-actions .action-btn:hover{border-color:var(--accent-dark);color:var(--text);background:rgba(139,92,246,0.04)}
-
-.back-link{
- display:block;text-align:center;margin-top:16px;font-size:12px;
- color:var(--text-muted);cursor:pointer;text-decoration:none;transition:color .2s;
-}
-.back-link:hover{color:var(--accent)}
-
-@media(max-width:480px){
- .login-card{padding:28px 24px;border-radius:16px}
- .logo .flame{width:56px;height:56px}
- .logo .flame .ov-mark{width:56px!important;height:56px!important}
- .logo h1{font-size:18px}
- .login-container{padding:16px}
-}
-
-@media(max-height:820px){
- .login-container{align-items:flex-start;padding-top:24px;padding-bottom:24px}
-}
-
-@media(max-width:380px){
- .login-card{padding:24px 18px;border-radius:14px}
- .tab-bar button{font-size:12px;padding:8px 6px}
-}
-</style>
+<link rel="stylesheet" href="/css/login.css">
</head>
</head>
<body>
<body>
@@ -30,255 +30,7 @@
@@ -30,255 +30,7 @@
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
(function(){try{var raw=localStorage.getItem('ov_theme');if(!raw)return;var t=JSON.parse(raw),v=t&&t.variables;if(!v)return;var el=document.documentElement;for(var k in v)if(k.charAt(0)==='-')el.style.setProperty(k,v[k]);if(t.id)el.setAttribute('data-theme',t.id);}catch(_){}})();
</script>
</script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
-<style>
-*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
-:root{
- --bg:#0d0d14;--bg-secondary:#151520;--bg-card:#1a1a26;--bg-input:#1c1c28;
- --border:#2a2a3a;--border-focus:#8b5cf6;
- --accent:#3b82f6;--accent-glow:rgba(139,92,246,0.4);--accent-dark:#1d4ed8;--accent-light:#60a5fa;
- --text:#e0e0e0;--text-secondary:#b0b0b8;--text-muted:#707080;
- --live-red:#ef4444;--success:#22c55e;
- --shadow:0 4px 20px rgba(0,0,0,0.3);
-}
-html{height:100%;background:var(--bg)}
-body{min-height:100%;color:var(--text);font-family:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;padding-bottom:40px}
-
-/* Layout */
-.page{max-width:900px;margin:0 auto;padding:20px}
-.page-header{display:flex;align-items:center;gap:16px;margin-bottom:32px}
-.page-header .avatar{width:72px;height:72px;border-radius:50%;border:3px solid var(--accent);object-fit:cover}
-.page-header .info{flex:1}
-.page-header .info h1{font-size:22px;font-weight:800;margin-bottom:2px}
-.page-header .info .subtitle{font-size:13px;color:var(--text-muted)}
-.page-header .info .badges{display:flex;gap:6px;margin-top:6px}
-.page-header .info .badges .badge{
- font-size:10px;padding:3px 10px;border-radius:4px;font-weight:600;text-transform:uppercase;letter-spacing:.5px;
-}
-.badge-admin{background:rgba(231,76,60,0.15);color:#e74c3c}
-.badge-anon{background:rgba(139,92,246,0.15);color:var(--accent-light)}
-.badge-streamer{background:rgba(155,89,182,0.15);color:#9b59b6}
-.badge-user{background:rgba(52,152,219,0.15);color:#3498db}
-
-/* Section tabs */
-.section-tabs{display:flex;gap:4px;margin-bottom:24px;background:var(--bg-secondary);border-radius:10px;padding:4px;overflow-x:auto}
-.section-tabs button{
- flex-shrink:0;padding:8px 16px;border:none;border-radius:8px;font-size:12px;font-weight:600;
- background:transparent;color:var(--text-muted);cursor:pointer;transition:all .15s;white-space:nowrap;
-}
-.section-tabs button.active{background:var(--bg-card);color:var(--accent-light)}
-.section-tabs button:hover:not(.active){color:var(--text)}
-
-/* Approvals (my/approvals.js): one row per waiting request, and the count on its tab */
-.section-tabs .tab-badge{display:inline-block;min-width:18px;padding:0 6px;border-radius:9px;background:var(--accent);color:#fff;font-size:11px;line-height:18px;text-align:center}
-.approval{border:1px solid var(--border);border-radius:10px;padding:14px 16px;margin-bottom:10px;background:var(--bg-secondary)}
-.approval-summary{font-weight:600;color:var(--text);margin-bottom:6px;overflow-wrap:anywhere}
-.approval-meta{font-size:12px;color:var(--text-secondary);margin-bottom:10px;overflow-wrap:anywhere}
-.approval-who{color:var(--text);font-weight:600}
-.approval-cap{font-size:11px;padding:1px 6px;border-radius:5px;background:var(--bg-input);border:1px solid var(--border)}
-.approval-resources{list-style:none;display:flex;flex-wrap:wrap;gap:6px;margin:0 0 10px;padding:0}
-.approval-resources li{font-size:11px;padding:2px 8px;border-radius:6px;background:var(--bg-input);color:var(--text-secondary);overflow-wrap:anywhere}
-.approval .btn-row{display:flex;gap:8px;flex-wrap:wrap}
-.approvals-empty{color:var(--text-secondary);font-size:13px}
-
-/* Section */
-.section{display:none;animation:sec-in .2s ease}
-.section.active{display:block}
-@keyframes sec-in{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}
-
-/* Cards */
-.card{background:var(--bg-card);border:1px solid var(--border);border-radius:12px;padding:20px;margin-bottom:16px}
-.card h3{font-size:14px;font-weight:700;margin-bottom:12px;display:flex;align-items:center;gap:8px}
-.card h3 .icon{font-size:16px}
-.card p{font-size:13px;color:var(--text-secondary);line-height:1.6}
-
-/* Form fields */
-.form-field{margin-bottom:14px}
-.form-field label{display:block;font-size:11px;font-weight:600;color:var(--text-muted);text-transform:uppercase;letter-spacing:.5px;margin-bottom:5px}
-/* Every text field on the page follows the active theme: its input colour, a hover tint and an accent focus ring. */
-:where(input:not([type]),input[type=text],input[type=url],input[type=email],input[type=search],input[type=password],input[type=number],select,textarea){
- padding:10px 12px;background:var(--bg-input);border:1px solid var(--border);border-radius:10px;
- color:var(--text);font:inherit;font-size:13px;outline:none;min-height:40px;
- transition:border-color .18s ease,box-shadow .18s ease,background-color .18s ease;
-}
-:where(input:not([type]),input[type=text],input[type=url],input[type=email],input[type=search],input[type=password],input[type=number],select,textarea):hover{border-color:color-mix(in srgb,var(--accent) 40%,var(--border))}
-:where(input:not([type]),input[type=text],input[type=url],input[type=email],input[type=search],input[type=password],input[type=number],select,textarea):focus{
- border-color:var(--accent);box-shadow:0 0 0 3px color-mix(in srgb,var(--accent) 24%,transparent);
- background:color-mix(in srgb,var(--accent) 5%,var(--bg-input));
-}
-:where(input,textarea)::placeholder{color:var(--text-muted);opacity:1}
-:where(input,select,textarea):user-invalid{border-color:var(--danger,#ef4444)}
-.form-field input,.form-field select,.form-field textarea{width:100%}
-.form-field textarea{resize:vertical;min-height:60px}
-.form-field .hint{font-size:10px;color:var(--text-muted);margin-top:3px}
-.theme-submit{display:grid;grid-template-columns:repeat(2,minmax(0,1fr)) auto;gap:12px;align-items:start;margin-top:16px}
-.theme-submit .form-field{margin:0}
-.theme-submit .btn{margin-top:21px;min-height:40px}
-@media (max-width:640px){.theme-submit{grid-template-columns:1fr}.theme-submit .btn{margin-top:0}}
-
-.btn{
- padding:8px 20px;border:none;border-radius:8px;font-size:12px;font-weight:600;cursor:pointer;transition:all .15s;
-}
-.btn-primary{background:var(--accent);color:var(--on-accent,#fff)}
-.btn-primary:hover{background:var(--accent-dark)}
-.btn-outline{background:transparent;border:1px solid var(--border);color:var(--text)}
-.btn-outline:hover{border-color:var(--accent-dark);color:var(--accent-light)}
-.btn-danger{background:rgba(231,76,60,0.1);color:var(--live-red);border:1px solid rgba(231,76,60,0.2)}
-.btn-danger:hover{background:rgba(231,76,60,0.2)}
-.btn-row{display:flex;gap:8px;margin-top:12px}
-.data-check{display:flex;gap:8px;align-items:flex-start;font-size:13px;color:var(--text-secondary);line-height:1.5}
-.data-check input{margin-top:3px}
-.data-status{align-self:center;font-size:12px;color:var(--text-secondary)}
-.module-row{display:flex;gap:12px;align-items:flex-start;padding:10px 0;border-top:1px solid var(--border)}
-.module-row:first-child{border-top:0}
-.module-row-main{flex:1;min-width:0}
-.module-row-main strong{display:block;font-size:13px}
-.module-row-main small{display:block;font-size:12px;color:var(--text-muted);line-height:1.5}
-.module-row-main code{display:block;margin-top:4px;font-size:11px;color:var(--text-secondary);white-space:pre-wrap;word-break:break-word;max-height:7.5em;overflow:auto}
-
-/* Account list */
-.account-list{display:flex;flex-direction:column;gap:8px}
-.account-item{
- display:flex;align-items:center;gap:12px;padding:12px;border-radius:10px;
- background:var(--bg-input);border:1px solid var(--border);
-}
-.account-item.active{border-color:var(--accent)}
-.account-item img{width:36px;height:36px;border-radius:50%}
-.account-item .name{font-size:13px;font-weight:600}
-.account-item .meta{font-size:11px;color:var(--text-muted)}
-.account-item .actions{margin-left:auto;display:flex;gap:6px}
-
-/* Notification pref grid */
-.notif-grid{display:grid;grid-template-columns:1fr auto auto auto auto;gap:8px;align-items:center;font-size:12px}
-.notif-grid .cat{font-weight:600;text-transform:capitalize}
-.notif-grid .toggle{width:36px;height:20px;border-radius:10px;border:none;cursor:pointer;position:relative;transition:background .2s}
-.notif-grid .toggle::after{content:'';position:absolute;width:16px;height:16px;border-radius:50%;background:#fff;top:2px;left:2px;transition:transform .2s}
-.notif-grid .toggle.on{background:var(--accent)}
-.notif-grid .toggle.on::after{transform:translateX(16px)}
-.notif-grid .toggle.off{background:var(--border)}
-
-/* Linked services */
-.linked-item{display:flex;align-items:center;gap:12px;padding:12px;border-radius:8px;background:var(--bg-input);margin-bottom:8px;text-decoration:none;color:inherit;transition:transform .15s,background .15s}
-a.linked-item:hover{background:var(--bg-hover,var(--bg-card));transform:translateX(2px)}
-.linked-item .svc-meta{margin-left:auto;text-align:right;font-size:11px;color:var(--text-muted);white-space:nowrap}
-.linked-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:8px;margin-top:14px}
-.linked-grid a{display:flex;align-items:center;gap:8px;padding:10px 12px;border-radius:8px;border:1px dashed var(--border);font-size:12px;color:var(--text-secondary);text-decoration:none;transition:border-color .15s,color .15s}
-.linked-grid a:hover{border-color:var(--accent);color:var(--text)}
-.hist-toolbar{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin-bottom:12px}
-.hist-chips{display:flex;flex-wrap:wrap;gap:6px}
-.hist-chips button{padding:5px 10px;border-radius:999px;border:1px solid var(--border);background:transparent;color:var(--text-secondary);font-size:12px;cursor:pointer;transition:all .15s}
-.hist-chips button.active,.hist-chips button:hover{border-color:var(--accent);color:var(--accent-light);background:rgba(var(--accent-rgb,139,92,246),.08)}
-.hist-search{flex:1;min-width:160px;padding:7px 10px;border-radius:8px;border:1px solid var(--border);background:var(--bg-input);color:var(--text);font-size:13px}
-.hist-pause{display:inline-flex;align-items:center;gap:6px;font-size:12px;color:var(--text-secondary);cursor:pointer}
-.btn-small{padding:6px 10px;font-size:12px}
-.hist-day{font-size:11px;font-weight:700;letter-spacing:.6px;text-transform:uppercase;color:var(--text-muted);margin:14px 0 6px}
-.hist-item{display:flex;align-items:center;gap:12px;padding:9px 10px;border-radius:8px;text-decoration:none;color:inherit;transition:background .12s;animation:sec-in .25s ease both}
-.hist-item:hover{background:var(--bg-input)}
-.hist-item .hi{width:34px;height:34px;border-radius:9px;display:grid;place-items:center;background:rgba(var(--accent-rgb,139,92,246),.12);color:var(--accent-light);flex:none}
-.hist-item .ht{font-size:13px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
-.hist-item .hs{font-size:11px;color:var(--text-muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
-.hist-item .hx{margin-left:auto;display:flex;align-items:center;gap:10px;flex:none;font-size:11px;color:var(--text-muted)}
-.hist-item .hx button{background:none;border:none;color:var(--text-muted);cursor:pointer;padding:4px;opacity:0;transition:opacity .15s}
-.hist-item:hover .hx button{opacity:1}
-.hist-item .hx button:hover{color:var(--danger,#ef4444)}
-.hist-empty{padding:26px;text-align:center;color:var(--text-muted);font-size:13px}
-.linked-item .svc-icon{font-size:20px;width:32px;text-align:center}
-.linked-item .svc-name{font-size:13px;font-weight:600}
-.linked-item .svc-user{font-size:11px;color:var(--text-muted)}
-.linked-item .svc-status{margin-left:auto;font-size:11px;font-weight:600;color:var(--success)}
-
-/* Themes */
-.theme-hero{display:flex;justify-content:space-between;gap:16px;align-items:flex-start;flex-wrap:wrap;margin-bottom:18px}
-.theme-hero-copy{max-width:560px}
-.theme-hero-copy p{margin-top:8px}
-.theme-current{display:flex;align-items:center;gap:10px;padding:12px 14px;border-radius:10px;background:var(--bg-input);border:1px solid var(--border);min-width:220px}
-.theme-current-swatch{display:flex;gap:4px}
-.theme-current-swatch span,.theme-preview-swatch span{width:18px;height:18px;border-radius:50%;border:1px solid rgba(255,255,255,.08);display:inline-block}
-.theme-current .label{font-size:10px;color:var(--text-muted);text-transform:uppercase;letter-spacing:.08em}
-.theme-current .name{font-size:13px;font-weight:700;margin-top:2px}
-.theme-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:12px}
-.theme-custom-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:10px}
-.theme-custom-row{display:flex;align-items:center;gap:8px;font-size:13px;color:var(--text-secondary);background:var(--bg-input);border:1px solid var(--border);border-radius:10px;padding:8px 10px;cursor:pointer}
-.theme-custom-row input[type=color]{width:34px;height:34px;border:none;border-radius:8px;background:none;cursor:pointer;padding:0}
-.theme-custom-row input[type=color]::-webkit-color-swatch{border:1px solid var(--border);border-radius:6px}
-.theme-custom-row input[type=color]::-webkit-color-swatch-wrapper{padding:0}
-.theme-card{background:var(--bg-input);border:1px solid var(--border);border-radius:14px;padding:14px;display:flex;flex-direction:column;gap:12px;transition:border-color .15s,transform .15s,box-shadow .15s}
-.theme-card:hover{transform:translateY(-2px);border-color:var(--accent)}
-.theme-card.active{border-color:var(--accent);box-shadow:0 0 0 1px rgba(139,92,246,.25) inset}
-.theme-preview{border-radius:12px;padding:14px;background:linear-gradient(135deg,var(--theme-bg-1,#202331),var(--theme-bg-2,#181a24));border:1px solid rgba(255,255,255,.06)}
-.theme-preview-top{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:12px}
-.theme-preview-chip{padding:5px 9px;border-radius:999px;background:rgba(255,255,255,.08);color:#fff;font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.08em}
-.theme-preview-bars{display:grid;gap:7px}
-.theme-preview-bars span{display:block;height:8px;border-radius:999px;background:rgba(255,255,255,.16)}
-.theme-preview-bars span:first-child{width:72%}
-.theme-preview-bars span:last-child{width:46%}
-.theme-card h4{font-size:14px;font-weight:700}
-.theme-card p{font-size:12px;color:var(--text-secondary);line-height:1.55}
-.theme-card-footer{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-top:auto}
-.theme-badges{display:flex;gap:6px;flex-wrap:wrap}
-.theme-badge{font-size:10px;padding:4px 8px;border-radius:999px;background:rgba(255,255,255,.05);color:var(--text-muted);text-transform:uppercase;letter-spacing:.08em;font-weight:700}
-.theme-empty{padding:16px;border-radius:12px;background:var(--bg-input);border:1px dashed var(--border);text-align:center;color:var(--text-muted);font-size:13px}
-
-/* Anon info card */
-.anon-card{text-align:center;padding:32px 20px}
-.anon-card .number{font-size:48px;font-weight:800;color:var(--accent);margin-bottom:8px}
-.anon-card .label{font-size:12px;color:var(--text-muted);margin-bottom:16px}
-.anon-card .stats{display:flex;justify-content:center;gap:24px}
-.anon-card .stat{text-align:center}
-.anon-card .stat .val{font-size:20px;font-weight:700}
-.anon-card .stat .key{font-size:10px;color:var(--text-muted);text-transform:uppercase}
-
-/* Loading */
-.loading-spinner{text-align:center;padding:40px;color:var(--text-muted);font-size:13px}
-.acct-retry-btn{display:inline-block;background:var(--accent,#8b5cf6);color:#0b0d10;border:none;border-radius:6px;padding:3px 12px;font-weight:600;font-size:0.85em;cursor:pointer;vertical-align:baseline}
-.acct-retry-btn:hover{filter:brightness(1.08)}
-
-@media(max-width:600px){
- .page{padding:12px}
- .page-header{flex-direction:column;text-align:center}
- .page-header .info .badges{justify-content:center}
- .notif-grid{grid-template-columns:1fr auto auto}
- .notif-grid .col-email,.notif-grid .col-toast{display:none}
-}
-
-/* Dashboard Showcase */
-@keyframes float { 0%, 100% { transform: translateY(0px); } 50% { transform: translateY(-10px); } }
-@keyframes fadeInScale { from { opacity: 0; transform: scale(0.95); } to { opacity: 1; transform: scale(1); } }
-.dashboard-showcase { margin: 32px 0; padding: 28px; border-radius: 12px; background: var(--bg-card); border: 1px solid var(--border); }
-.showcase-header { text-align: center; margin-bottom: 24px; }
-.showcase-header h2 { font-size: 1.3rem; font-weight: 800; margin-bottom: 6px; }
-.showcase-header p { font-size: 12px; color: var(--text-muted); }
-.showcase-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; margin-bottom: 20px; }
-.showcase-card { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 16px; border-radius: 10px; background: var(--bg-input); border: 1px solid var(--border); cursor: pointer; text-decoration: none; color: inherit; gap: 8px; animation: float ease-in-out infinite; transition: all 0.3s ease; }
-.showcase-card:nth-child(1) { animation-delay: 0s; animation-duration: 3s; }
-.showcase-card:nth-child(2) { animation-delay: 0.15s; animation-duration: 3.3s; }
-.showcase-card:nth-child(3) { animation-delay: 0.3s; animation-duration: 3.6s; }
-.showcase-card:nth-child(4) { animation-delay: 0.45s; animation-duration: 3.2s; }
-.showcase-card:hover { transform: translateY(-4px); border-color: var(--accent); background: rgba(139,92,246,0.08); }
-.showcase-card-icon { font-size: 1.4rem; color: var(--accent-light); }
-.showcase-card-label { font-size: 11px; font-weight: 700; text-align: center; }
-.quick-access { display: grid; grid-template-columns: repeat(6, 1fr); gap: 8px; }
-.quick-link { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 10px; border-radius: 8px; background: var(--bg); border: 1px solid var(--border); text-decoration: none; color: var(--text-secondary); font-size: 10px; font-weight: 600; text-align: center; transition: all 0.25s ease; animation: fadeInScale 0.5s ease both; }
-.quick-link i { font-size: 1rem; margin-bottom: 4px; }
-.quick-link:nth-child(1) { animation-delay: 0.1s; }
-.quick-link:nth-child(2) { animation-delay: 0.15s; }
-.quick-link:nth-child(3) { animation-delay: 0.2s; }
-.quick-link:nth-child(4) { animation-delay: 0.25s; }
-.quick-link:nth-child(5) { animation-delay: 0.3s; }
-.quick-link:nth-child(6) { animation-delay: 0.35s; }
-.quick-link:hover { border-color: var(--accent); color: var(--accent-light); background: rgba(139,92,246,0.08); }
-@media(max-width:600px){ .showcase-grid { grid-template-columns: repeat(2, 1fr); } .quick-access { grid-template-columns: repeat(3, 1fr); } }
-.recent-tools { margin-top: 14px; }
-.recent-tools h3 { font-size: 12px; font-weight: 700; color: var(--text-secondary); margin: 0 0 8px; display: flex; gap: 6px; align-items: center; }
-.recent-tools-list { display: flex; flex-wrap: wrap; gap: 6px; }
-.recent-tool { padding: 6px 10px; border-radius: 999px; background: var(--bg); border: 1px solid var(--border); color: var(--text-secondary); font-size: 12px; font-weight: 600; text-decoration: none; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
-.recent-tool:hover { border-color: var(--accent); color: var(--accent-light); }
-.recent-tools-status { margin: 6px 0 0; font-size: 12px; color: var(--text-secondary); }
-.recent-tools-status:empty { display: none; }
-.recent-tool-item { display: inline-flex; align-items: center; gap: 2px; max-width: 100%; }
-.recent-tool-star { border: 0; background: none; color: var(--text-secondary); cursor: pointer; padding: 6px; font-size: 12px; border-radius: 999px; }
-.recent-tool-star[aria-pressed="true"] { color: var(--accent-light); }
-.recent-tool-star:hover, .recent-tool-star:focus-visible { color: var(--accent-light); }
-</style>
+<link rel="stylesheet" href="/css/my.css">
</head>
</head>
<body>
<body>
@@ -20,9 +20,11 @@ assert.ok(toggles.length >= 3 && toggles.every((b) => /aria-label="Show password
@@ -20,9 +20,11 @@ assert.ok(toggles.length >= 3 && toggles.every((b) => /aria-label="Show password
const loginJs = fs.readFileSync(path.join(__dirname, '..', 'public', 'js', 'login.js'), 'utf8');
const loginJs = fs.readFileSync(path.join(__dirname, '..', 'public', 'js', 'login.js'), 'utf8');
assert.match(loginJs, /btn\.setAttribute\('aria-label', input\.type === 'password' \? 'Show password' : 'Hide password'\)/);
assert.match(loginJs, /btn\.setAttribute\('aria-label', input\.type === 'password' \? 'Show password' : 'Hide password'\)/);
for (const [, body] of login.matchAll(/<script\b[^>]*>([\s\S]*?)<\/script>/g)) assert.ok(body.length <= 200, 'login.html keeps no inline script body over 200 characters');
for (const [, body] of login.matchAll(/<script\b[^>]*>([\s\S]*?)<\/script>/g)) assert.ok(body.length <= 200, 'login.html keeps no inline script body over 200 characters');
-const token = (name) => login.match(new RegExp(`--${name}:(#[0-9a-f]{6})`, 'i'))[1];
+// The sign-in page's CSS was extracted to /css/login.css (plan T2 step 1); the tokens and rules live there now.
+const loginCss = fs.readFileSync(path.join(__dirname, '..', 'public', 'css', 'login.css'), 'utf8');
+const token = (name) => loginCss.match(new RegExp(`--${name}:(#[0-9a-f]{6})`, 'i'))[1];
for (const bg of ['bg', 'bg-card', 'bg-input']) assert.ok(ratio(token('text-muted'), token(bg)) >= 4.5, `muted text on --${bg} reads at 4.5:1`);
for (const bg of ['bg', 'bg-card', 'bg-input']) assert.ok(ratio(token('text-muted'), token(bg)) >= 4.5, `muted text on --${bg} reads at 4.5:1`);
-assert.match(login, /\.tab-bar button\.active\{background:var\(--accent-dark\);color:#fff;/);
+assert.match(loginCss, /\.tab-bar button\.active\{background:var\(--accent-dark\);color:#fff;/);
assert.ok(ratio(token('accent-dark'), '#ffffff') >= 4.5, 'the active tab reads at 4.5:1');
assert.ok(ratio(token('accent-dark'), '#ffffff') >= 4.5, 'the active tab reads at 4.5:1');
const updates = fs.readFileSync(path.join(__dirname, '..', 'server', 'updates', 'routes.js'), 'utf8');
const updates = fs.readFileSync(path.join(__dirname, '..', 'server', 'updates', 'routes.js'), 'utf8');
new file mode 100644
new file mode 100644
@@ -0,0 +1,22 @@
@@ -0,0 +1,22 @@
+'use strict';
+// T2 step 1: the three pages' inline <style> monoliths moved to public/css/<page>.css, linked exactly once.
+// The theme-flash guard stays inline on /admin and /my: a stylesheet would load too late to stop the flash.
+// node test/page-styles.test.js
+const assert = require('assert');
+const fs = require('fs');
+const path = require('path');
+
+const pub = path.join(__dirname, '..', 'public');
+for (const [page, css] of [['admin.html', 'admin.css'], ['my.html', 'my.css'], ['login.html', 'login.css']]) {
+ const html = fs.readFileSync(path.join(pub, page), 'utf8');
+ const links = [...html.matchAll(/<link\b[^>]*rel="stylesheet"[^>]*href="\/css\/([^"]+)"/g)].map((m) => m[1]);
+ assert.deepStrictEqual(links, [css], `${page} links /css/${css} exactly once`);
+ assert.ok(!/<style\b/.test(html), `${page} keeps no inline <style> block`);
+ const body = fs.readFileSync(path.join(pub, 'css', css), 'utf8');
+ assert.ok(body.length > 1000, `public/css/${css} carries the page styles`);
+}
+for (const page of ['admin.html', 'my.html']) {
+ const html = fs.readFileSync(path.join(pub, page), 'utf8');
+ assert.match(html, /localStorage\.getItem\('ov_theme'\)/, `${page} keeps the inline theme bootstrap`);
+}
+console.log('page styles: all checks passed');
@@ -17,6 +17,7 @@ const { ACCOUNT_HUB_PATHS } = require('../server/not-found');
@@ -17,6 +17,7 @@ const { ACCOUNT_HUB_PATHS } = require('../server/not-found');
'/', '/login', '/forgot-password', '/reset-password', '/admin', '/admin/settings', '/admin/analytics/overview',
'/', '/login', '/forgot-password', '/reset-password', '/admin', '/admin/settings', '/admin/analytics/overview',
'/verify-email', '/status', '/sso/fanout', '/terms', '/privacy', '/dmca', '/llms.txt',
'/verify-email', '/status', '/sso/fanout', '/terms', '/privacy', '/dmca', '/llms.txt',
'/robots.txt', '/sitemap.xml', '/manifest.webmanifest', '/assets/logo.svg', '/js/login.js', '/shared/navbar.js', '/shared/v1/navbar.js',
'/robots.txt', '/sitemap.xml', '/manifest.webmanifest', '/assets/logo.svg', '/js/login.js', '/shared/navbar.js', '/shared/v1/navbar.js',
+ '/css/login.css', '/css/my.css', '/css/admin.css',
'/openvibe-sw.js', '/release.json', '/.well-known/openvibe', '/.well-known/web-identity', '/api/health', '/api/brand',
'/openvibe-sw.js', '/release.json', '/.well-known/openvibe', '/.well-known/web-identity', '/api/health', '/api/brand',
'/api/v1/registry/services', '/api/v1/registry/categories', '/api/v1/registry/featured', '/api/.well-known/jwks',
'/api/v1/registry/services', '/api/v1/registry/categories', '/api/v1/registry/featured', '/api/.well-known/jwks',
...ACCOUNT_HUB_PATHS.filter(p => p !== '/my.html'),
...ACCOUNT_HUB_PATHS.filter(p => p !== '/my.html'),