openvibers/openvibe.network Commits

Move admin, my and login styles into /css files; refresh stale pin and project.read notes (#57)

OP OpenVibers committed 5 Oct 2026 · a0310b6ecf · 11 files, +676 −651

Files changed
- `public/css/admin.css` (new) — the `<style>` bytes moved verbatim out of `admin.html` (`*,*::…` through the `#sec-ssh` rules), 9618 bytes.
- `public/css/my.css` (new) — the `<style>` bytes moved verbatim out of `my.html`, 19375 bytes.
- `public/css/login.css` (new) — the `<style>` bytes moved verbatim out of `login.html`, including the `.oauth-consent…` block that was added since the brief’s line numbers, 12318 bytes.
- `public/admin.html` — `<style>…</style>` replaced with `<link rel="stylesheet" href="/css/admin.css">`; the inline theme-boot script is untouched.
- `public/my.html` — replaced with `<link rel="stylesheet" href="/css/my.css">`; theme-boot script untouched.
- `public/login.html` — replaced with `<link rel="stylesheet" href="/css/login.css">`; no inline script bodies remain.
- `test/page-checks.test.js` — `token()` and the `.tab-bar button.active{…}` assertion now read `public/css/login.css`; the login.html reads (canonical, noscript, toggles, inline-script size) stay on the HTML.
- `test/page-styles.test.js` (new) — asserts each page links its `/css/<page>.css` exactly once, keeps no inline `<style>`, the css file carries the styles, and `/admin` + `/my` keep the inline theme bootstrap.
- `test/unknown-paths.test.js` — added `/css/login.css`, `/css/my.css`, `/css/admin.css` to the real-200 list, so booting the server and GETting them is covered.
- `docs/developer-projects.md` — the two `network.project.read`: "planned / Contracts still has to make it active" notes now say `active` since openvibe-contracts v0.88.0.
- `docs/t2-projects-and-grants.md` — same correction in the capability table and the agent-read row.
- `README.md` / `STATUS.json` — no change needed: both already point at the current pins (`openvibe-contracts` v0.97.0 / `openvibe-sdk` v0.28.0 / `openvibe-shared` v2.9.0), matching `package.json`; commit fab6225 (#53) had already refreshed them, so the brief’s "still says v0.80.0/v1.29.2" no longer applies. There is no `CHANGELOG.md` in this repo. The proposal artifacts under `docs/contracts-proposal/` and `docs/capabilities-proposal/` still say `planned`; I left those frozen proposal records alone.

Tests run
- `ov test test/page-styles.test.js test/page-checks.test.js test/admin-modules.test.js test/my-modules.test.js test/consent-ui.test.js test/operator-checklist.test.js test/blocks.test.js test/unknown-paths.test.js` → **8 passed in 24 s**. `unknown-paths` boots the real server and GETs `/login`, `/my`, `/admin` and the three `/css/*.css` files — all 200.
- `node scripts/contracts-drift.js` → exit 0; 25 warning-only lines (other services behind latest; Network itself warns `pins v0.97.0, latest v0.99.0`, unchanged by this PR).
- Byte-verbatim check (Node): reconstructed each page from `git show HEAD:…` and confirmed `css file === inner bytes` and `html === original with the `<link>` swapped in` for all three.

Not done / why
- Did not run full `npm test` or any whole suite — the environment directive says to run only the files the change reaches via `ov test <files>`; CI runs the full suite after push.
- Did not commit, push, deploy or install packages, per instructions. (A `chmod 644 public/css/*.css` attempt was denied by the permission mode; git records only the executable bit, so the new files commit as ordinary non-executable 100644 blobs.)

Changes

ddiff --git a/docs/developer-projects.md b/docs/developer-projects.md
iindex 0ce2b95e..09e3ba61 100644
---- a/docs/developer-projects.md
++++ b/docs/developer-projects.md
@@@ -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.
ddiff --git a/docs/t2-projects-and-grants.md b/docs/t2-projects-and-grants.md
iindex 537b3738..216de651 100644
---- a/docs/t2-projects-and-grants.md
++++ b/docs/t2-projects-and-grants.md
@@@ -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 }` |
ddiff --git a/public/admin.html b/public/admin.html
iindex 22cbfef2..a1f56d15 100644
---- a/public/admin.html
++++ b/public/admin.html
@@@ -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>
ddiff --git a/public/css/admin.css b/public/css/admin.css
nnew file mode 100644
iindex 00000000..ee143c87
---- /dev/null
++++ b/public/css/admin.css
@@@ -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)}
ddiff --git a/public/css/login.css b/public/css/login.css
nnew file mode 100644
iindex 00000000..cc6fc2a4
---- /dev/null
++++ b/public/css/login.css
@@@ -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}
++}
ddiff --git a/public/css/my.css b/public/css/my.css
nnew file mode 100644
iindex 00000000..e070eea9
---- /dev/null
++++ b/public/css/my.css
@@@ -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); }
ddiff --git a/public/login.html b/public/login.html
iindex a013f4f3..47a0a75e 100644
---- a/public/login.html
++++ b/public/login.html
@@@ -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>
ddiff --git a/public/my.html b/public/my.html
iindex 89c28c4a..2b5a450a 100644
---- a/public/my.html
++++ b/public/my.html
@@@ -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>
ddiff --git a/test/page-checks.test.js b/test/page-checks.test.js
iindex 32ca740b..809804b1 100644
---- a/test/page-checks.test.js
++++ b/test/page-checks.test.js
@@@ -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');
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');
ddiff --git a/test/page-styles.test.js b/test/page-styles.test.js
nnew file mode 100644
iindex 00000000..7499af39
---- /dev/null
++++ b/test/page-styles.test.js
@@@ -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');
ddiff --git a/test/unknown-paths.test.js b/test/unknown-paths.test.js
iindex 6ad220ff..19a516c3 100644
---- a/test/unknown-paths.test.js
++++ b/test/unknown-paths.test.js
@@@ -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'),