<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover"><style>:root{color-scheme:light;padding-top:env(safe-area-inset-top,0px);padding-bottom:env(safe-area-inset-bottom,0px)}body{margin:0}img{max-width:100%}[hidden]{display:none!important}</style></head><body>
<title>StudyBoost Combined Prototype</title>
<meta name="claude:viewport-fit-sides" content="cover">
<link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin><link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,400..800&family=DM+Sans:ital,opsz,wght@0,9..40,400..800;1,9..40,400..600&family=DM+Mono:wght@400;500&family=Tinos:ital,wght@0,400;0,700;1,400&family=Caveat:wght@500;700&display=swap">
<style>
/* ===== 10-tokens.css ===== */
/* Tokens. Canonical names follow the Premium Themes prototype (shadcn-style), so each theme ports straight into globals.css.
Default = StudyBoost Light, following the viewer’s light/dark setting. The in-app theme picker sets data-sb on <html> and wins over both. */
:root{
--bg:#f6f8f8; --sidebar:#ffffff; --topbar:#ffffff; --card:#ffffff; --card-2:#f1f5f5;
--fg:#0f1b24; --muted:#56656c; --faint:#8a979c; --border:#e2e8e9; --border-2:#cdd7d8;
--primary:#1a8a7f; --on-primary:#ffffff; --primary-soft:#e2f3f0; --primary-ink:#137569;
--chip-on:#0f1b2d; --on-chip:#ffffff; --input:#ffffff; --shadow:rgba(15,27,36,.10); --glow:0 0 0 0 transparent; --blur:none;
--success:#159a7d; --warn:#e3a21a;
--hero:linear-gradient(118deg,#0e4b46 0%,#0b2c30 46%,#0a1421 100%); --hero-pattern:none;
--hero-fg:#ffffff; --hero-muted:#b8cacc; --hero-eyebrow:#43d3bf;
--hero-card:#211e3d; --hero-card-2:#2a2750; --hero-card-fg:#f3f1ff; --hero-card-label:#b0a5ff; --hero-line:rgba(255,255,255,.10);
--hero-num:#6b4fe0; --on-hero-num:#ffffff; --hero-ok:#43d3a6; --hero-wip:#f2b23a;
--hero-btn:#1a8a7f; --on-hero-btn:#ffffff; --hero-btn2:rgba(255,255,255,.30);
--app-icon:linear-gradient(145deg,#7a5cf0,#4b32b8); --on-app-icon:#ffffff;
--pin-bg:#fdf7e9; --pin-line:#f0d491; --pin-ink:#b45f0d; --pin-note:#fffbf1; --pin-btn:#f6cc63; --on-pin-btn:#6e3b05;
--wb-bg:#e9f6f3; --wb-line:#93d2c7;
--ta-bg:#fdeef1; --ta-line:#f4c6cf; --ta-ink:#b1174d;
--tb-bg:#e1f5ee; --tb-line:#b4e3d2; --tb-ink:#11775d;
--tc-bg:#efe9fd; --tc-line:#d5c9f7; --tc-ink:#5a33c2;
--td-bg:#e5f0fd; --td-line:#bdd6f5; --td-ink:#1c5dac;
--ask:#1a8a7f; --on-ask:#ffffff;
--amb-1:transparent; --amb-2:transparent; --amb-3:transparent; --amb-4:transparent;
color-scheme:light;
--danger:#d23b3b; --bubble:#eef3f2; --cream:#fff4e2; --star:#c2850a; --scrim:rgba(9,18,24,.44); --toast-bg:#0f1b24; --toast-fg:#eef5f3;
}
@media (prefers-color-scheme:dark){:root:not([data-theme="light"]){
--bg:#0b1214; --sidebar:#0e171a; --topbar:#0e171a; --card:#121d20; --card-2:#17252a;
--fg:#e4eded; --muted:#9bb0b0; --faint:#6a7f81; --border:#1f2e32; --border-2:#2c3f43;
--primary:#2db3a2; --on-primary:#03201b; --primary-soft:#11302c; --primary-ink:#4fd1bf;
--chip-on:#e4eded; --on-chip:#0b1214; --input:#0f1a1d; --shadow:rgba(0,0,0,.45); --glow:0 0 0 0 transparent; --blur:none;
--success:#3fcf9f; --warn:#f0b43a;
--hero:linear-gradient(118deg,#0f403b 0%,#0a2328 46%,#060c15 100%); --hero-pattern:none;
--hero-fg:#ffffff; --hero-muted:#a9bec0; --hero-eyebrow:#4fd9c5;
--hero-card:#1d1a36; --hero-card-2:#26234a; --hero-card-fg:#f3f1ff; --hero-card-label:#b0a5ff; --hero-line:rgba(255,255,255,.09);
--hero-num:#6b4fe0; --on-hero-num:#ffffff; --hero-ok:#4fd9a6; --hero-wip:#f0b43a;
--hero-btn:#2db3a2; --on-hero-btn:#03201b; --hero-btn2:rgba(255,255,255,.28);
--app-icon:linear-gradient(145deg,#7a5cf0,#4b32b8); --on-app-icon:#ffffff;
--pin-bg:#231d12; --pin-line:#4d3d1b; --pin-ink:#f0b452; --pin-note:#2a2316; --pin-btn:#d99d36; --on-pin-btn:#241503;
--wb-bg:#0f2628; --wb-line:#25574f;
--ta-bg:#301620; --ta-line:#552433; --ta-ink:#f59ab3;
--tb-bg:#0e2b24; --tb-line:#1b4e40; --tb-ink:#6fdfbc;
--tc-bg:#211a3b; --tc-line:#392d63; --tc-ink:#b9a7ff;
--td-bg:#122339; --td-line:#233f66; --td-ink:#90c0ff;
--ask:#2db3a2; --on-ask:#03201b;
--amb-1:transparent; --amb-2:transparent; --amb-3:transparent; --amb-4:transparent;
color-scheme:dark;
--danger:#f07a7a; --bubble:#18272a; --cream:#f4e7d0; --star:#f0b43a; --scrim:rgba(0,0,0,.55); --toast-bg:#e4eded; --toast-fg:#0b1214;
}}
:root[data-theme="dark"]{
--bg:#0b1214; --sidebar:#0e171a; --topbar:#0e171a; --card:#121d20; --card-2:#17252a;
--fg:#e4eded; --muted:#9bb0b0; --faint:#6a7f81; --border:#1f2e32; --border-2:#2c3f43;
--primary:#2db3a2; --on-primary:#03201b; --primary-soft:#11302c; --primary-ink:#4fd1bf;
--chip-on:#e4eded; --on-chip:#0b1214; --input:#0f1a1d; --shadow:rgba(0,0,0,.45); --glow:0 0 0 0 transparent; --blur:none;
--success:#3fcf9f; --warn:#f0b43a;
--hero:linear-gradient(118deg,#0f403b 0%,#0a2328 46%,#060c15 100%); --hero-pattern:none;
--hero-fg:#ffffff; --hero-muted:#a9bec0; --hero-eyebrow:#4fd9c5;
--hero-card:#1d1a36; --hero-card-2:#26234a; --hero-card-fg:#f3f1ff; --hero-card-label:#b0a5ff; --hero-line:rgba(255,255,255,.09);
--hero-num:#6b4fe0; --on-hero-num:#ffffff; --hero-ok:#4fd9a6; --hero-wip:#f0b43a;
--hero-btn:#2db3a2; --on-hero-btn:#03201b; --hero-btn2:rgba(255,255,255,.28);
--app-icon:linear-gradient(145deg,#7a5cf0,#4b32b8); --on-app-icon:#ffffff;
--pin-bg:#231d12; --pin-line:#4d3d1b; --pin-ink:#f0b452; --pin-note:#2a2316; --pin-btn:#d99d36; --on-pin-btn:#241503;
--wb-bg:#0f2628; --wb-line:#25574f;
--ta-bg:#301620; --ta-line:#552433; --ta-ink:#f59ab3;
--tb-bg:#0e2b24; --tb-line:#1b4e40; --tb-ink:#6fdfbc;
--tc-bg:#211a3b; --tc-line:#392d63; --tc-ink:#b9a7ff;
--td-bg:#122339; --td-line:#233f66; --td-ink:#90c0ff;
--ask:#2db3a2; --on-ask:#03201b;
--amb-1:transparent; --amb-2:transparent; --amb-3:transparent; --amb-4:transparent;
color-scheme:dark;
--danger:#f07a7a; --bubble:#18272a; --cream:#f4e7d0; --star:#f0b43a; --scrim:rgba(0,0,0,.55); --toast-bg:#e4eded; --toast-fg:#0b1214;
}
:root[data-sb="light"]{
--bg:#f6f8f8; --sidebar:#ffffff; --topbar:#ffffff; --card:#ffffff; --card-2:#f1f5f5;
--fg:#0f1b24; --muted:#56656c; --faint:#8a979c; --border:#e2e8e9; --border-2:#cdd7d8;
--primary:#1a8a7f; --on-primary:#ffffff; --primary-soft:#e2f3f0; --primary-ink:#137569;
--chip-on:#0f1b2d; --on-chip:#ffffff; --input:#ffffff; --shadow:rgba(15,27,36,.10); --glow:0 0 0 0 transparent; --blur:none;
--success:#159a7d; --warn:#e3a21a;
--hero:linear-gradient(118deg,#0e4b46 0%,#0b2c30 46%,#0a1421 100%); --hero-pattern:none;
--hero-fg:#ffffff; --hero-muted:#b8cacc; --hero-eyebrow:#43d3bf;
--hero-card:#211e3d; --hero-card-2:#2a2750; --hero-card-fg:#f3f1ff; --hero-card-label:#b0a5ff; --hero-line:rgba(255,255,255,.10);
--hero-num:#6b4fe0; --on-hero-num:#ffffff; --hero-ok:#43d3a6; --hero-wip:#f2b23a;
--hero-btn:#1a8a7f; --on-hero-btn:#ffffff; --hero-btn2:rgba(255,255,255,.30);
--app-icon:linear-gradient(145deg,#7a5cf0,#4b32b8); --on-app-icon:#ffffff;
--pin-bg:#fdf7e9; --pin-line:#f0d491; --pin-ink:#b45f0d; --pin-note:#fffbf1; --pin-btn:#f6cc63; --on-pin-btn:#6e3b05;
--wb-bg:#e9f6f3; --wb-line:#93d2c7;
--ta-bg:#fdeef1; --ta-line:#f4c6cf; --ta-ink:#b1174d;
--tb-bg:#e1f5ee; --tb-line:#b4e3d2; --tb-ink:#11775d;
--tc-bg:#efe9fd; --tc-line:#d5c9f7; --tc-ink:#5a33c2;
--td-bg:#e5f0fd; --td-line:#bdd6f5; --td-ink:#1c5dac;
--ask:#1a8a7f; --on-ask:#ffffff;
--amb-1:transparent; --amb-2:transparent; --amb-3:transparent; --amb-4:transparent;
color-scheme:light;
--danger:#d23b3b; --bubble:#eef3f2; --cream:#fff4e2; --star:#c2850a; --scrim:rgba(9,18,24,.44); --toast-bg:#0f1b24; --toast-fg:#eef5f3;
}
:root[data-sb="dark"]{
--bg:#0b1214; --sidebar:#0e171a; --topbar:#0e171a; --card:#121d20; --card-2:#17252a;
--fg:#e4eded; --muted:#9bb0b0; --faint:#6a7f81; --border:#1f2e32; --border-2:#2c3f43;
--primary:#2db3a2; --on-primary:#03201b; --primary-soft:#11302c; --primary-ink:#4fd1bf;
--chip-on:#e4eded; --on-chip:#0b1214; --input:#0f1a1d; --shadow:rgba(0,0,0,.45); --glow:0 0 0 0 transparent; --blur:none;
--success:#3fcf9f; --warn:#f0b43a;
--hero:linear-gradient(118deg,#0f403b 0%,#0a2328 46%,#060c15 100%); --hero-pattern:none;
--hero-fg:#ffffff; --hero-muted:#a9bec0; --hero-eyebrow:#4fd9c5;
--hero-card:#1d1a36; --hero-card-2:#26234a; --hero-card-fg:#f3f1ff; --hero-card-label:#b0a5ff; --hero-line:rgba(255,255,255,.09);
--hero-num:#6b4fe0; --on-hero-num:#ffffff; --hero-ok:#4fd9a6; --hero-wip:#f0b43a;
--hero-btn:#2db3a2; --on-hero-btn:#03201b; --hero-btn2:rgba(255,255,255,.28);
--app-icon:linear-gradient(145deg,#7a5cf0,#4b32b8); --on-app-icon:#ffffff;
--pin-bg:#231d12; --pin-line:#4d3d1b; --pin-ink:#f0b452; --pin-note:#2a2316; --pin-btn:#d99d36; --on-pin-btn:#241503;
--wb-bg:#0f2628; --wb-line:#25574f;
--ta-bg:#301620; --ta-line:#552433; --ta-ink:#f59ab3;
--tb-bg:#0e2b24; --tb-line:#1b4e40; --tb-ink:#6fdfbc;
--tc-bg:#211a3b; --tc-line:#392d63; --tc-ink:#b9a7ff;
--td-bg:#122339; --td-line:#233f66; --td-ink:#90c0ff;
--ask:#2db3a2; --on-ask:#03201b;
--amb-1:transparent; --amb-2:transparent; --amb-3:transparent; --amb-4:transparent;
color-scheme:dark;
--danger:#f07a7a; --bubble:#18272a; --cream:#f4e7d0; --star:#f0b43a; --scrim:rgba(0,0,0,.55); --toast-bg:#e4eded; --toast-fg:#0b1214;
}
:root[data-sb="matcha"]{
--bg:#eef1e6; --sidebar:#f7f8f1; --topbar:#f7f8f1; --card:#fbfcf7; --card-2:#eff3e6;
--fg:#1f2a1b; --muted:#5a6852; --faint:#8b9682; --border:#dce2cf; --border-2:#c6cfb4;
--primary:#4e7a39; --on-primary:#fbfcf3; --primary-soft:#e1ead2; --primary-ink:#41672e;
--chip-on:#2b3c25; --on-chip:#f3f6ea; --input:#fbfcf7; --shadow:rgba(46,60,36,.12); --glow:0 0 0 0 transparent; --blur:none;
--success:#4e8a3e; --warn:#c48a2a;
--hero:linear-gradient(125deg,#3a5a30 0%,#2a4123 52%,#1d2c18 100%); --hero-pattern:radial-gradient(circle at 80% 18%,rgba(214,230,170,.16),transparent 42%);
--hero-fg:#f7f4e8; --hero-muted:#c4cdb3; --hero-eyebrow:#cfe3a1;
--hero-card:#efe9d9; --hero-card-2:#fbf8ef; --hero-card-fg:#2b2b1f; --hero-card-label:#6a7f40; --hero-line:rgba(60,70,40,.13);
--hero-num:#4e7a39; --on-hero-num:#fbfcf3; --hero-ok:#4e8a3e; --hero-wip:#c48a2a;
--hero-btn:#e7efcf; --on-hero-btn:#24361d; --hero-btn2:rgba(247,244,232,.38);
--app-icon:linear-gradient(145deg,#86a865,#3f6430); --on-app-icon:#fbfcf3;
--pin-bg:#f6eee2; --pin-line:#e0cbad; --pin-ink:#8a582a; --pin-note:#fbf6ee; --pin-btn:#d8b688; --on-pin-btn:#4d2f12;
--wb-bg:#e4ecd6; --wb-line:#a8bd8a;
--ta-bg:#f6e5e1; --ta-line:#e8c8bf; --ta-ink:#97473a;
--tb-bg:#e2ebd2; --tb-line:#c3d5a5; --tb-ink:#47692d;
--tc-bg:#f3eed0; --tc-line:#e0d598; --tc-ink:#715f14;
--td-bg:#ebe5ef; --td-line:#d3c7de; --td-ink:#604c78;
--ask:#4e7a39; --on-ask:#fbfcf3;
--amb-1:transparent; --amb-2:transparent; --amb-3:transparent; --amb-4:transparent;
color-scheme:light;
--danger:#b4472f; --bubble:#eef2e4; --cream:#f6eee2; --star:#a7741f; --scrim:rgba(31,42,27,.42); --toast-bg:#1f2a1b; --toast-fg:#f3f6ea;
}
:root[data-sb="cloud"]{
--bg:#f2f1fb; --sidebar:rgba(255,255,255,.58); --topbar:rgba(255,255,255,.5); --card:rgba(255,255,255,.76); --card-2:rgba(238,236,255,.85);
--fg:#23214a; --muted:#5c5984; --faint:#8f8cb3; --border:rgba(110,100,200,.17); --border-2:rgba(110,100,200,.32);
--primary:#5b5bd6; --on-primary:#ffffff; --primary-soft:rgba(226,225,253,.92); --primary-ink:#4a49c2;
--chip-on:#2d2b5e; --on-chip:#ffffff; --input:rgba(255,255,255,.82); --shadow:rgba(70,60,160,.14); --glow:0 0 0 0 transparent; --blur:blur(18px) saturate(1.25);
--success:#2f9c84; --warn:#e0884f;
--hero:linear-gradient(115deg,#cbd3ff 0%,#e4d2ff 48%,#ffd8c8 100%); --hero-pattern:radial-gradient(circle at 86% 18%,rgba(255,255,255,.6),transparent 40%);
--hero-fg:#23214a; --hero-muted:#4e4b7a; --hero-eyebrow:#4a49c2;
--hero-card:rgba(255,255,255,.6); --hero-card-2:rgba(255,255,255,.92); --hero-card-fg:#23214a; --hero-card-label:#6556d4; --hero-line:rgba(110,100,200,.16);
--hero-num:#5b5bd6; --on-hero-num:#ffffff; --hero-ok:#2f9c84; --hero-wip:#e0884f;
--hero-btn:#5b5bd6; --on-hero-btn:#ffffff; --hero-btn2:rgba(35,33,74,.28);
--app-icon:linear-gradient(145deg,#9a97f7,#5b5bd6); --on-app-icon:#ffffff;
--pin-bg:rgba(255,240,232,.85); --pin-line:#f7cdb8; --pin-ink:#b0502a; --pin-note:rgba(255,250,246,.9); --pin-btn:#ffbf9f; --on-pin-btn:#6b2a10;
--wb-bg:rgba(226,225,253,.78); --wb-line:#c2c0f4;
--ta-bg:#fde7f0; --ta-line:#f5c4da; --ta-ink:#ab356d;
--tb-bg:#def4ee; --tb-line:#b2e3d6; --tb-ink:#1f7361;
--tc-bg:#ebe4ff; --tc-line:#d2c5fb; --tc-ink:#6544c4;
--td-bg:#e2eefe; --td-line:#bbd4fa; --td-ink:#2c5fb0;
--ask:#5b5bd6; --on-ask:#ffffff;
--amb-1:#aebcff; --amb-2:#e0c3ff; --amb-3:#ffcdb9; --amb-4:#bfeedd;
color-scheme:light;
--danger:#c2456a; --bubble:rgba(238,236,255,.9); --cream:#fff0e6; --star:#c9762f; --scrim:rgba(35,33,74,.36); --toast-bg:#23214a; --toast-fg:#f4f3ff;
}
:root[data-sb="midnight"]{
--bg:#15121e; --sidebar:#110e18; --topbar:#15121e; --card:#1d1929; --card-2:#241f33;
--fg:#efe8f5; --muted:#aaa1bf; --faint:#7a7190; --border:#2c273e; --border-2:#3d3655;
--primary:#f3b75f; --on-primary:#2a1906; --primary-soft:#2c2418; --primary-ink:#f6c37a;
--chip-on:#efe8f5; --on-chip:#15121e; --input:#1a1625; --shadow:rgba(0,0,0,.5); --glow:0 0 0 0 transparent; --blur:none;
--success:#8fd6b8; --warn:#f3b75f;
--hero:linear-gradient(125deg,#2b2040 0%,#1c1731 52%,#110e1c 100%); --hero-pattern:radial-gradient(circle at 82% 24%,rgba(243,183,95,.22),transparent 46%);
--hero-fg:#f8f1e6; --hero-muted:#b9afc8; --hero-eyebrow:#f3b75f;
--hero-card:#251f39; --hero-card-2:#2e2746; --hero-card-fg:#f2ebfa; --hero-card-label:#eaa3b8; --hero-line:rgba(255,255,255,.08);
--hero-num:#f3b75f; --on-hero-num:#2a1906; --hero-ok:#8fd6b8; --hero-wip:#f3b75f;
--hero-btn:#f3b75f; --on-hero-btn:#2a1906; --hero-btn2:rgba(248,241,230,.28);
--app-icon:linear-gradient(145deg,#f6c37a,#d97a6c); --on-app-icon:#2a1906;
--pin-bg:#2a1d27; --pin-line:#523344; --pin-ink:#f0a1b8; --pin-note:#31232e; --pin-btn:#c97591; --on-pin-btn:#250f18;
--wb-bg:#29211a; --wb-line:#5a4528;
--ta-bg:#33202b; --ta-line:#563247; --ta-ink:#f4a8bf;
--tb-bg:#162a2a; --tb-line:#294947; --tb-ink:#8ed7c6;
--tc-bg:#251f41; --tc-line:#3d356e; --tc-ink:#c3b6ff;
--td-bg:#30261a; --td-line:#56432a; --td-ink:#f4c47b;
--ask:#f3b75f; --on-ask:#2a1906;
--amb-1:rgba(243,183,95,.26); --amb-2:rgba(169,154,230,.16); --amb-3:transparent; --amb-4:transparent;
color-scheme:dark;
--danger:#f08aa0; --bubble:#262033; --cream:#f4e7d0; --star:#f3b75f; --scrim:rgba(0,0,0,.55); --toast-bg:#efe8f5; --toast-fg:#15121e;
}
:root[data-sb="citrus"]{
--bg:#fff9f1; --sidebar:#fff1d6; --topbar:#fffaf3; --card:#ffffff; --card-2:#fff4e4;
--fg:#2b1607; --muted:#6d5242; --faint:#a48772; --border:#f2dfc6; --border-2:#e8c79f;
--primary:#ff6b2c; --on-primary:#2a1200; --primary-soft:#ffe6d3; --primary-ink:#c4400a;
--chip-on:#2b1607; --on-chip:#fff4e6; --input:#ffffff; --shadow:rgba(140,70,10,.14); --glow:0 0 0 0 transparent; --blur:none;
--success:#3d8f1e; --warn:#ff6b2c;
--hero:linear-gradient(115deg,#ffd43f 0%,#ffa03a 46%,#ff5c72 100%); --hero-pattern:radial-gradient(circle at 12% 110%,rgba(255,255,255,.38),transparent 42%);
--hero-fg:#2a1206; --hero-muted:#4d2b14; --hero-eyebrow:#2a1206;
--hero-card:#2b1509; --hero-card-2:#3b1f10; --hero-card-fg:#fff3e3; --hero-card-label:#ffd43f; --hero-line:rgba(255,255,255,.09);
--hero-num:#ff6b2c; --on-hero-num:#2a1200; --hero-ok:#b8ef6a; --hero-wip:#ffd43f;
--hero-btn:#2a1206; --on-hero-btn:#fff3e3; --hero-btn2:rgba(42,18,6,.38);
--app-icon:linear-gradient(145deg,#ff8a3d,#ff4f6f); --on-app-icon:#ffffff;
--pin-bg:#e8f4ff; --pin-line:#b6dafa; --pin-ink:#1563ad; --pin-note:#f4faff; --pin-btn:#7dc2ff; --on-pin-btn:#06325d;
--wb-bg:#fff0e2; --wb-line:#ffbe92;
--ta-bg:#ffe5ef; --ta-line:#ffbdd4; --ta-ink:#c51e58;
--tb-bg:#ecf7d8; --tb-line:#cde89e; --tb-ink:#477609;
--tc-bg:#e2f2ff; --tc-line:#b4d9fa; --tc-ink:#1b65ae;
--td-bg:#fff2c2; --td-line:#fade86; --td-ink:#7d5800;
--ask:#ff6b2c; --on-ask:#2a1200;
--amb-1:transparent; --amb-2:transparent; --amb-3:transparent; --amb-4:transparent;
color-scheme:light;
--danger:#d1243c; --bubble:#fff1e0; --cream:#fff0d6; --star:#c46a00; --scrim:rgba(43,22,7,.4); --toast-bg:#2b1607; --toast-fg:#fff4e6;
}
:root[data-sb="arcade"]{
--bg:#0c0d1e; --sidebar:#090a17; --topbar:#0c0d1e; --card:#14162d; --card-2:#1a1d3a;
--fg:#f1f2ff; --muted:#a3a7d0; --faint:#6f73a2; --border:#242a4c; --border-2:#343a67;
--primary:#ff4f9a; --on-primary:#1a0612; --primary-soft:#2a1532; --primary-ink:#ff73b1;
--chip-on:#3de6ef; --on-chip:#061419; --input:#11132a; --shadow:rgba(0,0,0,.55); --glow:0 0 0 1px rgba(255,79,154,.5),0 12px 34px -10px rgba(255,79,154,.28); --blur:none;
--success:#b8f26b; --warn:#ffd84d;
--hero:linear-gradient(120deg,#22145a 0%,#140f3d 46%,#0a0b1f 100%);
--hero-pattern:linear-gradient(rgba(61,230,239,.07) 1px,transparent 1px) 0 0/100% 34px,linear-gradient(90deg,rgba(61,230,239,.07) 1px,transparent 1px) 0 0/34px 100%;
--hero-fg:#ffffff; --hero-muted:#b5b7e2; --hero-eyebrow:#3de6ef;
--hero-card:#15163a; --hero-card-2:#1f2152; --hero-card-fg:#f1f2ff; --hero-card-label:#ffd84d; --hero-line:rgba(61,230,239,.16);
--hero-num:#ff4f9a; --on-hero-num:#1a0612; --hero-ok:#b8f26b; --hero-wip:#ffd84d;
--hero-btn:#ff4f9a; --on-hero-btn:#1a0612; --hero-btn2:rgba(61,230,239,.5);
--app-icon:linear-gradient(145deg,#ff4f9a,#7b4dff); --on-app-icon:#ffffff;
--pin-bg:#24200f; --pin-line:#574a13; --pin-ink:#ffd84d; --pin-note:#2c2713; --pin-btn:#ffd84d; --on-pin-btn:#211a00;
--wb-bg:#0e2330; --wb-line:#1c5366;
--ta-bg:#2b1430; --ta-line:#572454; --ta-ink:#ff7db9;
--tb-bg:#0d2832; --tb-line:#18525e; --tb-ink:#62eff4;
--tc-bg:#1e1944; --tc-line:#392f7b; --tc-ink:#b49dff;
--td-bg:#1b2912; --td-line:#3a581c; --td-ink:#b9f26c;
--ask:#ff4f9a; --on-ask:#1a0612;
--amb-1:transparent; --amb-2:transparent; --amb-3:transparent; --amb-4:transparent;
color-scheme:dark;
--danger:#ff6b7f; --bubble:#1b1e3c; --cream:#fde9c8; --star:#ffd84d; --scrim:rgba(0,0,0,.6); --toast-bg:#3de6ef; --toast-fg:#061419;
}
/* Fixed and derived tokens. Derived names cover the vocabularies of the ported prototypes. */
:root{
--display:"Bricolage Grotesque","DM Sans",system-ui,sans-serif;
--body:"DM Sans",system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;
--mono:"DM Mono",ui-monospace,"SF Mono",Menlo,monospace;
--hoot-body:#9C6B43; --hoot-dark:#73482A; --hoot-sheen:rgba(255,255,255,.13); --hoot-belly:#F6E5C8; --hoot-scallop:#D4AE80;
--hoot-glass:#12897D; --hoot-eye:#FFFFFF; --hoot-pupil:#1C1F3B; --hoot-beak:#F49A1A; --hoot-beak-dark:#C8700A;
--line:var(--border); --line-2:var(--border-2); --surface:var(--card); --surface-2:var(--card-2); --sunken:var(--card-2);
--primary-line:color-mix(in srgb,var(--primary) 34%,transparent); --primary-fg:var(--on-primary);
--accent:var(--pin-btn); --accent-soft:var(--pin-bg); --accent-ink:var(--pin-ink); --accent-line:var(--pin-line);
--note:var(--pin-note); --note-line:var(--pin-line); --note-ink:var(--pin-ink);
--sh-1:0 1px 2px var(--shadow); --sh-2:0 1px 2px var(--shadow),0 8px 24px -10px var(--shadow); --sh-3:0 24px 60px -18px var(--shadow);
--r-sm:9px; --r:14px; --r-lg:20px;
--nav-w:252px; --rail-w:76px; --panel-w:272px; --top-h:64px;
}
/* course categories (Course Hub palette), with dark versions */
[data-c="1"]{--cg-deep:#3340A8;--cg-mid:#5B6BEA;--cg-fill:#C9CFFB;--cg-bg:#EEF0FF;--ct-deep:#3340A8;--ct-mid:#5B6BEA;--ct-light:#C9CFFB;--ct-tint:#EEF0FF;--ct-ink:#3340A8}
[data-c="2"]{--cg-deep:#5B36B0;--cg-mid:#8C66E0;--cg-fill:#DCCFFA;--cg-bg:#F3EEFF;--ct-deep:#5B36B0;--ct-mid:#8C66E0;--ct-light:#DCCFFA;--ct-tint:#F3EEFF;--ct-ink:#5B36B0}
[data-c="3"]{--cg-deep:#A3480B;--cg-mid:#F08A3C;--cg-fill:#FDD5B5;--cg-bg:#FFF2E7;--ct-deep:#A3480B;--ct-mid:#F08A3C;--ct-light:#FDD5B5;--ct-tint:#FFF2E7;--ct-ink:#A3480B}
[data-c="4"]{--cg-deep:#17703F;--cg-mid:#3FB477;--cg-fill:#BFE9CF;--cg-bg:#EAF7EF;--ct-deep:#17703F;--ct-mid:#3FB477;--ct-light:#BFE9CF;--ct-tint:#EAF7EF;--ct-ink:#17703F}
[data-c="5"]{--cg-deep:#7E5A00;--cg-mid:#E2AE1F;--cg-fill:#F8E2A0;--cg-bg:#FEF7E1;--ct-deep:#7E5A00;--ct-mid:#E2AE1F;--ct-light:#F8E2A0;--ct-tint:#FEF7E1;--ct-ink:#7E5A00}
[data-c="6"]{--cg-deep:#155F8A;--cg-mid:#3CA2DA;--cg-fill:#BFE2F5;--cg-bg:#EAF6FC;--ct-deep:#155F8A;--ct-mid:#3CA2DA;--ct-light:#BFE2F5;--ct-tint:#EAF6FC;--ct-ink:#155F8A}
[data-c="7"]{--cg-deep:#9A4127;--cg-mid:#D2704F;--cg-fill:#F4CDBE;--cg-bg:#FCEFEA;--ct-deep:#9A4127;--ct-mid:#D2704F;--ct-light:#F4CDBE;--ct-tint:#FCEFEA;--ct-ink:#9A4127}
[data-c="8"]{--cg-deep:#B32745;--cg-mid:#E8587A;--cg-fill:#F9C6D2;--cg-bg:#FDEDF1;--ct-deep:#B32745;--ct-mid:#E8587A;--ct-light:#F9C6D2;--ct-tint:#FDEDF1;--ct-ink:#B32745}
[data-c="9"]{--cg-deep:#6E4518;--cg-mid:#B98143;--cg-fill:#EBD3B4;--cg-bg:#F8F0E6;--ct-deep:#6E4518;--ct-mid:#B98143;--ct-light:#EBD3B4;--ct-tint:#F8F0E6;--ct-ink:#6E4518}
[data-c="10"]{--cg-deep:#8E2672;--cg-mid:#D25AAE;--cg-fill:#F2C6E4;--cg-bg:#FCEEF8;--ct-deep:#8E2672;--ct-mid:#D25AAE;--ct-light:#F2C6E4;--ct-tint:#FCEEF8;--ct-ink:#8E2672}
[data-c="11"]{--cg-deep:#0A6157;--cg-mid:#19A394;--cg-fill:#BCE8E1;--cg-bg:#E8F6F3;--ct-deep:#0A6157;--ct-mid:#19A394;--ct-light:#BCE8E1;--ct-tint:#E8F6F3;--ct-ink:#0A6157}
:root.dk [data-c="1"]{--cg-deep:#8f9af1;--cg-mid:#5B6BEA;--cg-fill:#273478;--cg-bg:#1c2849;--ct-deep:#8f9af1;--ct-mid:#5B6BEA;--ct-light:#24306b;--ct-tint:#19253e;--ct-ink:#a5aef3}
:root.dk [data-c="2"]{--cg-deep:#b197ea;--cg-mid:#8C66E0;--cg-fill:#412d7e;--cg-bg:#28244b;--ct-deep:#b197ea;--ct-mid:#8C66E0;--ct-light:#3a2b6f;--ct-tint:#222240;--ct-ink:#c0abee}
:root.dk [data-c="3"]{--cg-deep:#f5af7a;--cg-mid:#F08A3C;--cg-fill:#703912;--cg-bg:#3e2a1a;--ct-deep:#f5af7a;--ct-mid:#F08A3C;--ct-light:#623514;--ct-tint:#32261b;--ct-ink:#f7bf94}
:root.dk [data-c="4"]{--cg-deep:#7ccca3;--cg-mid:#3FB477;--cg-fill:#155334;--cg-bg:#143629;--ct-deep:#7ccca3;--ct-mid:#3FB477;--ct-light:#154b31;--ct-tint:#132f27;--ct-ink:#95d6b4}
:root.dk [data-c="5"]{--cg-deep:#ebc867;--cg-mid:#E2AE1F;--cg-fill:#58450b;--cg-bg:#322f16;--ct-deep:#ebc867;--ct-mid:#E2AE1F;--ct-light:#4d3f0e;--ct-tint:#2a2a19;--ct-ink:#efd284}
:root.dk [data-c="6"]{--cg-deep:#7ac0e6;--cg-mid:#3CA2DA;--cg-fill:#144865;--cg-bg:#133140;--ct-deep:#7ac0e6;--ct-mid:#3CA2DA;--ct-light:#14415a;--ct-tint:#132c37;--ct-ink:#94cceb}
:root.dk [data-c="7"]{--cg-deep:#e09e87;--cg-mid:#D2704F;--cg-fill:#6a3425;--cg-bg:#3b2822;--ct-deep:#e09e87;--ct-mid:#D2704F;--ct-light:#5d3124;--ct-tint:#302522;--ct-ink:#e6b09e}
:root.dk [data-c="8"]{--cg-deep:#ef8da5;--cg-mid:#E8587A;--cg-fill:#7b2438;--cg-bg:#42202b;--ct-deep:#ef8da5;--ct-mid:#E8587A;--ct-light:#6b2234;--ct-tint:#351f28;--ct-ink:#f2a3b6}
:root.dk [data-c="9"]{--cg-deep:#cfa97f;--cg-mid:#B98143;--cg-fill:#4e371b;--cg-bg:#2e291e;--ct-deep:#cfa97f;--ct-mid:#B98143;--ct-light:#45331c;--ct-tint:#26261e;--ct-ink:#d8ba98}
:root.dk [data-c="10"]{--cg-deep:#e08fc8;--cg-mid:#D25AAE;--cg-fill:#632355;--cg-bg:#372039;--ct-deep:#e08fc8;--ct-mid:#D25AAE;--ct-light:#56224d;--ct-tint:#2d1f32;--ct-ink:#e6a4d2}
:root.dk [data-c="11"]{--cg-deep:#63c0b6;--cg-mid:#19A394;--cg-fill:#0d4944;--cg-bg:#103130;--ct-deep:#63c0b6;--ct-mid:#19A394;--ct-light:#0e423e;--ct-tint:#102c2c;--ct-ink:#80ccc4}
/* ===== 20-base.css ===== */
/* ---------- base ---------- */
*,*::before,*::after{box-sizing:border-box}
html,body{height:100%}
body{margin:0;background:var(--bg);color:var(--fg);font:14px/1.45 var(--body);-webkit-font-smoothing:antialiased;overflow:hidden}
button{font:inherit;color:inherit;background:none;border:0;padding:0;cursor:pointer;text-align:inherit}
input,select,textarea{font:inherit;color:inherit}
a{color:inherit}
h1,h2,h3,h4,h5,h6,p{margin:0}
h1,h2,h3{text-wrap:balance}
:focus-visible{outline:2px solid var(--primary);outline-offset:2px}
[hidden]{display:none!important}
.i{width:20px;height:20px;flex:none;fill:none;stroke:currentColor;stroke-width:1.9;stroke-linecap:round;stroke-linejoin:round}
.i.sm{width:16px;height:16px}.i.xs{width:13px;height:13px}.i.lg{width:24px;height:24px}
.i.fill{fill:currentColor}
.sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}
.tnum{font-variant-numeric:tabular-nums}
@media (prefers-reduced-motion:reduce){*,*::before,*::after{animation-duration:.001ms!important;animation-iteration-count:1!important;transition-duration:.001ms!important;scroll-behavior:auto!important}}
/* ---------- shell-level components (s- prefix keeps them clear of the ported page styles) ---------- */
.s-btn{display:inline-flex;align-items:center;justify-content:center;gap:8px;height:38px;padding:0 14px;border-radius:10px;border:1px solid var(--border-2);background:var(--card);color:var(--fg);font-weight:650;font-size:13.5px;white-space:nowrap;transition:border-color .15s,background .15s,color .15s,transform .15s}
.s-btn:hover{border-color:var(--primary);color:var(--primary-ink)}
.s-btn.primary{background:var(--primary);border-color:var(--primary);color:var(--on-primary)}
.s-btn.primary:hover{background:var(--primary-ink);border-color:var(--primary-ink);color:var(--on-primary)}
.s-btn.quiet{border-color:transparent;background:transparent;color:var(--muted)}
.s-btn.quiet:hover{background:var(--card-2);color:var(--fg)}
.s-btn.sm{height:32px;padding:0 11px;font-size:12.5px;border-radius:9px}
.s-btn.lg{height:46px;padding:0 20px;font-size:15px;border-radius:12px}
.s-btn.wide{width:100%}
.s-btn:disabled{opacity:.5;cursor:default}
.s-ib{width:38px;height:38px;border-radius:10px;display:inline-grid;place-items:center;color:var(--muted);flex:none;position:relative;transition:background .15s,color .15s}
.s-ib:hover{background:var(--card-2);color:var(--fg)}
.s-ib.sm{width:32px;height:32px;border-radius:9px}
.s-ib[aria-expanded="true"]{background:var(--primary-soft);color:var(--primary-ink)}
.s-chip{display:inline-flex;align-items:center;gap:6px;height:30px;padding:0 12px;border-radius:999px;border:1px solid var(--border);background:var(--card);font-size:13px;font-weight:600;color:var(--fg);white-space:nowrap}
.s-chip:hover{border-color:var(--primary-line)}
.s-chip[aria-pressed="true"]{background:var(--chip-on);border-color:var(--chip-on);color:var(--on-chip)}
.s-k{font-size:11px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:var(--faint)}
.s-link{color:var(--primary-ink);font-weight:650;text-decoration:none}
.s-link:hover{text-decoration:underline}
.s-mark{width:34px;height:34px;flex:none;color:var(--primary)}
.s-av{width:34px;height:34px;border-radius:50%;display:inline-grid;place-items:center;flex:none;background:var(--primary-soft);color:var(--primary-ink);font-weight:800;font-size:12px;letter-spacing:.02em}
.s-pro{display:inline-flex;align-items:center;height:18px;padding:0 6px;border-radius:5px;font-size:10px;font-weight:800;letter-spacing:.06em;background:var(--pin-bg);color:var(--pin-ink);border:1px solid var(--pin-line)}
.s-input{height:40px;width:100%;border:1.5px solid var(--border-2);border-radius:10px;padding:0 12px;background:var(--input);color:var(--fg)}
.s-input:focus{outline:none;border-color:var(--primary);box-shadow:0 0 0 3px var(--primary-soft)}
.s-switch{width:36px;height:22px;border-radius:999px;background:var(--border-2);position:relative;flex:none;transition:background .2s}
.s-switch i{position:absolute;top:3px;left:3px;width:16px;height:16px;border-radius:50%;background:#fff;box-shadow:0 1px 2px rgba(0,0,0,.25);transition:transform .2s}
[aria-checked="true"]>.s-switch,.s-switch.on{background:var(--primary)}
[aria-checked="true"]>.s-switch i,.s-switch.on i{transform:translateX(14px)}
/* school monogram + logo tile (shared by Documents, Campus Hub, search) */
.mono{--s:32px;width:var(--s);height:var(--s);flex:none;border-radius:calc(var(--s) * .28);display:inline-grid;place-items:center;background:var(--m-deep,var(--primary));color:var(--m-on,#fff);font:800 calc(var(--s) * .34)/1 var(--display);letter-spacing:-.02em;position:relative;overflow:hidden;box-shadow:inset 0 -3px 0 var(--m-bright,transparent)}
.mono[data-len="4"]{font-size:calc(var(--s) * .28)}
.mono[data-len="5"],.mono[data-len="6"],.mono[data-len="7"]{font-size:calc(var(--s) * .22)}
/* ===== 30-shell.css ===== */
/* ---------- app frame ---------- */
.app{position:relative;height:100%;display:flex;isolation:isolate;background:var(--bg);color:var(--fg);overflow:hidden}
.app.theming,.app.theming *{transition:background-color .4s ease,border-color .4s ease,color .4s ease!important}
.main{flex:1;min-width:0;display:flex;flex-direction:column;position:relative;z-index:1}
.page{flex:1;min-height:0;overflow-y:auto;overflow-x:hidden;position:relative;outline:none;scrollbar-gutter:stable}
.page-in{padding-block:24px 96px;padding-inline:clamp(16px,3vw,36px);max-width:1360px;margin:0 auto;min-width:0}
/* ambient layers for Cloud Nine and Lo-fi Midnight (from the Premium Themes prototype) */
.amb{position:absolute;inset:0;z-index:0;pointer-events:none;overflow:hidden}
.amb i{position:absolute;border-radius:50%;display:none;will-change:transform,opacity}
.amb .b1{width:60vmax;height:60vmax;left:-14vmax;top:-22vmax;background:radial-gradient(circle,var(--amb-1),transparent 66%)}
.amb .b2{width:52vmax;height:52vmax;right:-16vmax;top:4vmax;background:radial-gradient(circle,var(--amb-2),transparent 66%)}
.amb .b3{width:50vmax;height:50vmax;left:24vw;bottom:-26vmax;background:radial-gradient(circle,var(--amb-3),transparent 66%)}
.amb .b4{width:34vmax;height:34vmax;left:6vw;bottom:-6vmax;background:radial-gradient(circle,var(--amb-4),transparent 66%)}
.amb .lw{width:72vmax;height:72vmax;right:-26vmax;top:-34vmax;background:radial-gradient(circle,var(--amb-1),transparent 62%)}
.amb .lc{width:62vmax;height:62vmax;left:-22vmax;bottom:-34vmax;background:radial-gradient(circle,var(--amb-2),transparent 62%)}
:root[data-sb="cloud"] .amb .b1,:root[data-sb="cloud"] .amb .b2,:root[data-sb="cloud"] .amb .b3,:root[data-sb="cloud"] .amb .b4{display:block;opacity:.9}
:root[data-sb="midnight"] .amb .lw,:root[data-sb="midnight"] .amb .lc{display:block;opacity:.8}
:root[data-sb="cloud"]:not(.still) .amb .b1{animation:drift1 44s ease-in-out infinite alternate}
:root[data-sb="cloud"]:not(.still) .amb .b2{animation:drift2 52s ease-in-out infinite alternate}
:root[data-sb="cloud"]:not(.still) .amb .b3{animation:drift3 48s ease-in-out infinite alternate}
:root[data-sb="cloud"]:not(.still) .amb .b4{animation:drift4 38s ease-in-out infinite alternate}
:root[data-sb="midnight"]:not(.still) .amb .lw{animation:breathe 9s ease-in-out infinite}
:root[data-sb="midnight"]:not(.still) .amb .lc{animation:breathe 13s ease-in-out -4s infinite}
@keyframes drift1{to{transform:translate(16vw,12vh) scale(1.1)}}
@keyframes drift2{to{transform:translate(-18vw,16vh) scale(.92)}}
@keyframes drift3{to{transform:translate(-14vw,-14vh) scale(1.08)}}
@keyframes drift4{to{transform:translate(22vw,-10vh) scale(1.15)}}
@keyframes breathe{0%,100%{opacity:.5;transform:scale(.96)}50%{opacity:1;transform:scale(1.04)}}
:root[data-sb="arcade"] .page{background-image:linear-gradient(rgba(61,230,239,.035) 1px,transparent 1px),linear-gradient(90deg,rgba(61,230,239,.035) 1px,transparent 1px);background-size:34px 34px}
/* ---------- main sidebar: full, or the Canva-style rail ---------- */
.nav{flex:none;width:var(--nav-w);background:var(--sidebar);border-right:1px solid var(--border);display:flex;flex-direction:column;position:relative;z-index:4;-webkit-backdrop-filter:var(--blur);backdrop-filter:var(--blur);padding-left:env(safe-area-inset-left,0px);transition:width .25s ease}
.nav.rail{width:var(--rail-w)}
.nav-brand{height:var(--top-h);flex:none;display:flex;align-items:center;gap:10px;padding:0 14px 0 18px}
.nav-brand .s-mark{width:32px;height:32px}
.nav-word{font:750 19px/1 var(--display);letter-spacing:-.03em;white-space:nowrap}
.nav-word i{font-style:normal;color:inherit}
.nav-x{margin-left:auto}
.nav.rail .nav-brand{justify-content:center;padding:0}
.nav.rail .nav-word,.nav.rail .nav-x{display:none}
.nav-exp{display:none;margin:0 auto 6px;width:40px;height:26px;border-radius:8px;border:1px dashed var(--primary-line);color:var(--primary);place-items:center}
.nav-exp:hover{background:var(--primary-soft)}
.nav.rail .nav-exp{display:grid}
.nav-scroll{flex:1;min-height:0;overflow-y:auto;overflow-x:hidden;padding:4px 12px 14px;display:flex;flex-direction:column;gap:20px;scrollbar-width:thin}
.nav.rail .nav-scroll{padding:4px 8px 14px;gap:14px}
.nav-main{display:flex;flex-direction:column;gap:2px}
.nav-item{display:flex;align-items:center;gap:12px;height:42px;padding:0 12px;border-radius:11px;color:var(--muted);font-weight:650;font-size:14.5px;white-space:nowrap;position:relative;width:100%;text-decoration:none}
.nav-item:hover{background:var(--card-2);color:var(--fg)}
.nav-item[aria-current="page"]{background:var(--primary-soft);color:var(--primary-ink)}
.nav-item .i{width:21px;height:21px}
.nav.rail .nav-item{flex-direction:column;justify-content:center;gap:3px;height:58px;padding:0;font-size:10.5px;font-weight:700}
.nav.rail .nav-item .long{display:none}
.nav-item .short{display:none}
.nav.rail .nav-item .short{display:inline}
.nav-h{display:flex;align-items:center;justify-content:space-between;padding:0 12px 6px}
.nav.rail .nav-h{display:none}
.nav-grp{display:flex;flex-direction:column;gap:1px}
.nav-row{display:flex;align-items:center;gap:10px;padding:6px 10px;border-radius:10px;width:100%;min-width:0;text-align:left}
.nav-row:hover{background:var(--card-2)}
.nav-row[aria-current="page"]{background:var(--primary-soft)}
.nav-row .ri{width:28px;height:28px;border-radius:8px;display:grid;place-items:center;flex:none;background:var(--card-2);color:var(--muted)}
.nav-row .ri .i{width:15px;height:15px}
.nav-row .rt{flex:1;min-width:0;line-height:1.25}
.nav-row .rt b{display:block;font-size:13px;font-weight:650;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.nav-row .rt small{display:block;font-size:11.5px;color:var(--faint);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.nav-row .s-bar{margin-top:4px}
.nav.rail .nav-row{justify-content:center;padding:6px 0}
.nav.rail .nav-row .rt{display:none}
.nav.rail .nav-grp+.nav-grp{border-top:1px solid var(--border);padding-top:10px}
.nav-dot{width:12px;height:12px;border-radius:4px}
.nav-school{display:flex;align-items:center;gap:10px;padding:9px 10px;border:1px solid var(--border);border-radius:12px;background:var(--card);width:100%;text-align:left}
.nav-school:hover{border-color:var(--primary-line)}
.nav-school .rt{flex:1;min-width:0;line-height:1.25}
.nav-school b{display:block;font-size:13px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.nav-school small{display:block;font-size:11.5px;color:var(--faint)}
.nav.rail .nav-school{justify-content:center;border:0;padding:4px 0;background:none}
.nav.rail .nav-school .rt,.nav.rail .nav-school>.i{display:none}
.nav-foot{flex:none;border-top:1px solid var(--border);padding:10px 12px 12px;display:flex;flex-direction:column;gap:8px}
.nav.rail .nav-foot{padding:10px 8px 12px;align-items:center}
.plan{display:flex;flex-direction:column;gap:7px;padding:11px 12px;border-radius:12px;background:var(--pin-bg);border:1px solid var(--pin-line);text-align:left;width:100%}
.plan .pt{display:flex;justify-content:space-between;align-items:center;font-size:12.5px;font-weight:650;color:var(--pin-ink)}
.plan .pt b{font-variant-numeric:tabular-nums}
.plan .pc{font-size:12px;color:var(--fg);font-weight:650;display:flex;align-items:center;gap:6px}
.nav.rail .plan{display:none}
.nav-up{display:none}
.nav.rail .nav-up{display:grid}
.acct{display:flex;align-items:center;gap:10px;padding:6px 6px;border-radius:12px;width:100%;text-align:left}
.acct:hover{background:var(--card-2)}
.acct .rt{flex:1;min-width:0;line-height:1.25}
.acct b{display:flex;align-items:center;gap:6px;font-size:13px}
.acct small{display:block;color:var(--faint);font-size:11.5px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.nav.rail .acct{justify-content:center;padding:4px}
.nav.rail .acct .rt,.nav.rail .acct>.i{display:none}
.s-bar{height:5px;border-radius:99px;background:var(--border);overflow:hidden;display:block}
.s-bar i{display:block;height:100%;background:var(--primary);border-radius:inherit}
.tip{position:fixed;z-index:90;pointer-events:none;background:var(--toast-bg);color:var(--toast-fg);font-size:12px;font-weight:650;padding:5px 9px;border-radius:7px;white-space:nowrap;box-shadow:var(--sh-2)}
/* ---------- room / shelf panel (from the shelves epic) ---------- */
.rpanel{flex:none;width:var(--panel-w);background:var(--sidebar);border-right:1px solid var(--border);display:flex;flex-direction:column;overflow:hidden;position:relative;z-index:3;-webkit-backdrop-filter:var(--blur);backdrop-filter:var(--blur)}
.rpanel.shut{width:38px}
.rp-in{height:100%;overflow-y:auto;padding:14px 12px 12px;display:flex;flex-direction:column;gap:14px;scrollbar-width:thin}
.rp-strip{display:flex;flex-direction:column;align-items:center;gap:10px;padding:14px 0;height:100%}
.rp-strip span{writing-mode:vertical-rl;font-size:10.5px;font-weight:700;letter-spacing:.12em;text-transform:uppercase;color:var(--faint)}
.rp-round{width:28px;height:28px;flex:none;border-radius:50%;border:1px solid var(--border);background:var(--card);display:grid;place-items:center;color:var(--muted)}
.rp-round:hover{color:var(--primary);border-color:var(--primary-line)}
.rp-round .i{width:14px;height:14px}
.rp-ws{display:flex;align-items:center;gap:8px;padding:2px 2px 2px 6px}
.rp-ws .t{flex:1;min-width:0;line-height:1.25;font:700 15px/1.2 var(--display);letter-spacing:-.01em}
.rp-ws .t small{display:block;color:var(--faint);font:500 11.5px/1.3 var(--body);letter-spacing:0;margin-top:2px}
.rp-search{display:flex;gap:8px;align-items:center;border:1px solid var(--border);background:var(--card-2);border-radius:10px;padding:0 10px;height:36px;color:var(--faint);font-size:13px}
.rp-search input{flex:1;min-width:0;border:0;background:none;outline:none;color:var(--fg)}
.rp-new{display:flex;align-items:center;justify-content:center;gap:6px;border:1px dashed var(--primary-line);background:var(--primary-soft);color:var(--primary-ink);border-radius:10px;height:36px;font-weight:700;font-size:13px;width:100%}
.rp-new:hover{border-style:solid}
.rp-sec{display:flex;flex-direction:column;gap:1px}
.rp-sech{display:flex;justify-content:space-between;align-items:center;padding:0 8px 5px;color:var(--faint);font-size:11px;font-weight:700;letter-spacing:.08em;text-transform:uppercase}
.rp-sech .i{width:14px;height:14px}
.tree,.tree ul,.tree li{list-style:none;margin:0;padding:0}
.tree li>ul{display:none;margin-left:14px;padding-left:5px;border-left:1px solid var(--border)}
.tree li.open>ul{display:block}
.it{display:flex;align-items:center;gap:8px;padding:6px 8px;border-radius:8px;min-width:0;line-height:1.3;color:var(--fg);font-weight:500;font-size:13px;text-align:left;width:100%}
.it:hover{background:var(--primary-soft)}
.it.on{background:var(--primary-soft);color:var(--primary-ink);font-weight:700}
.it .ic{width:15px;height:15px;color:var(--muted);flex:none}
.it.on .ic{color:var(--primary-ink)}
.it .t{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.it .c{font-size:11px;color:var(--faint);font-variant-numeric:tabular-nums;flex:none}
.it .acts{display:none;gap:2px;color:var(--faint)}
.it .acts .i{width:14px;height:14px}
.it:hover .acts{display:flex}
.it:hover .c{display:none}
.twi{width:13px;height:13px;color:var(--faint);transition:transform .15s;flex:none}
.tree li.open>.it .twi{transform:rotate(90deg)}
.twsp{width:13px;flex:none}
.sdot{width:10px;height:10px;border-radius:3px;flex:none;display:inline-block}
.wid{border:1px solid var(--border);border-radius:12px;padding:10px 11px;display:flex;flex-direction:column;gap:9px;background:var(--card)}
.wid.exam{background:var(--pin-bg);border-color:var(--pin-line)}
.widh{display:flex;justify-content:space-between;align-items:center;width:100%;font-size:11px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--muted)}
.wid.exam .widh{color:var(--pin-ink)}
.widh span{display:flex;align-items:center;gap:6px}
.widh .i{width:13px;height:13px}
.wid.shut .widb{display:none}
.wid.shut .tw2{transform:rotate(-90deg)}
.tw2{transition:transform .15s}
.widb{display:flex;flex-direction:column;gap:8px}
.exrow{display:flex;align-items:center;gap:12px}
.cd{font:800 32px/1 var(--display);color:var(--pin-ink);display:flex;flex-direction:column;align-items:center;min-width:44px}
.cd small{font:800 9.5px var(--body);letter-spacing:.12em;text-transform:uppercase;margin-top:3px}
.rp-sub{font-size:12px;color:var(--muted);line-height:1.45}
.meter{display:flex;justify-content:space-between;font-size:12px;color:var(--muted)}
.meter b{color:var(--fg);font-variant-numeric:tabular-nums}
.week{display:grid;grid-template-columns:repeat(7,1fr);gap:4px}
.wday{display:flex;flex-direction:column;align-items:center;gap:4px;font-size:10px;font-weight:800;color:var(--faint)}
.wday i{width:24px;height:24px;border-radius:7px;background:var(--card-2);border:1px solid var(--border);display:grid;place-items:center;font-style:normal;font-size:10.5px;color:var(--fg)}
.wday.today{color:var(--primary-ink)} .wday.today i{border:2px solid var(--primary)}
.wday.due i{background:var(--primary-soft);border-color:var(--primary-line);color:var(--primary-ink)}
.rp-foot{margin-top:auto;display:flex;flex-direction:column;gap:7px;padding:10px 6px 0;border-top:1px solid var(--border)}
.rp-foot .s-link{font-size:12.5px}
.shelfhead{display:flex;gap:10px;align-items:center;padding:2px 2px 0}
.shelfhead .sq{width:40px;height:40px;border-radius:11px;display:grid;place-items:center;color:#fff;flex:none}
.shelfhead .t{flex:1;min-width:0}
.shelfhead b{display:block;font:750 15.5px/1.15 var(--display);letter-spacing:-.01em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.shelfhead small{display:block;color:var(--faint);font-size:11.5px;margin-top:2px}
.backlink{display:inline-flex;align-items:center;gap:6px;color:var(--muted);font-size:12.5px;font-weight:700;padding:0 4px;text-decoration:none}
.backlink:hover{color:var(--primary-ink)}
/* ---------- top bar ---------- */
.top{height:var(--top-h);flex:none;display:flex;align-items:center;gap:10px;padding:0 max(16px,env(safe-area-inset-right,0px)) 0 12px;background:var(--topbar);border-bottom:1px solid var(--border);position:relative;z-index:6;-webkit-backdrop-filter:var(--blur);backdrop-filter:var(--blur)}
.top-brand{display:none;align-items:center;gap:8px}
.top-brand .s-mark{width:30px;height:30px}
.sx{flex:1 1 auto;max-width:700px;min-width:0;position:relative;display:flex;align-items:stretch;height:44px;border:1.5px solid var(--border-2);border-radius:13px;background:var(--input);transition:border-color .15s,box-shadow .15s}
.sx:focus-within{border-color:var(--primary);box-shadow:0 0 0 3px var(--primary-soft)}
.sx-scope{display:flex;align-items:center;gap:6px;padding:0 4px 0 6px;flex:none}
.sx-seg{display:inline-flex;align-items:center;gap:5px;height:30px;padding:0 9px 0 10px;border-radius:9px;background:var(--card-2);font-size:13px;font-weight:700;color:var(--muted);white-space:nowrap}
.sx-seg:hover{color:var(--fg)}
.sx-tok{display:inline-flex;align-items:center;gap:6px;height:30px;padding:0 4px 0 4px;border-radius:9px;background:var(--primary-soft);color:var(--primary-ink);font-size:13px;font-weight:700;white-space:nowrap;max-width:200px}
.sx-tok span{overflow:hidden;text-overflow:ellipsis}
.sx-tok button{width:22px;height:22px;border-radius:6px;display:grid;place-items:center}
.sx-tok button:hover{background:color-mix(in srgb,var(--primary) 18%,transparent)}
.sx-tok .mono{--s:22px;border-radius:6px}
.sx input{flex:1;min-width:0;border:0;background:transparent;padding:0 10px;font-size:15px;outline:none;color:var(--fg)}
.sx input::placeholder{color:var(--faint)}
.sx-x{width:30px;display:grid;place-items:center;color:var(--faint);flex:none}
.sx-x:hover{color:var(--fg)}
.sx-go{width:58px;flex:none;border-left:1.5px solid var(--border-2);background:var(--card-2);display:grid;place-items:center;border-radius:0 11px 11px 0;color:var(--fg)}
.sx-go:hover{background:var(--primary-soft);color:var(--primary-ink)}
.top-acts{margin-left:auto;display:flex;align-items:center;gap:4px;flex:none}
.new-btn{display:inline-flex;align-items:center;gap:7px;height:38px;padding:0 13px 0 10px;border-radius:11px;border:1px solid var(--border-2);font-weight:700;font-size:13.5px;margin-right:4px}
.new-btn:hover,.new-btn[aria-expanded="true"]{border-color:var(--primary);color:var(--primary-ink);background:var(--primary-soft)}
.unlock{display:inline-flex;align-items:center;gap:8px;height:38px;padding:0 12px 0 8px;border-radius:999px;font-size:13px;font-weight:700;color:var(--fg);margin-right:2px}
.unlock:hover,.unlock[aria-expanded="true"]{background:var(--pin-bg)}
.s-ring{--p:66;width:24px;height:24px;border-radius:50%;flex:none;background:conic-gradient(var(--pin-btn) calc(var(--p) * 1%),var(--border) 0);display:grid;place-items:center}
.s-ring::after{content:"";width:16px;height:16px;border-radius:50%;background:var(--topbar)}
.unlock .ul-t b{color:var(--pin-ink)}
.unlock .ul-pro{color:var(--pin-ink)}
.top .s-av{width:36px;height:36px;margin-left:4px}
.top .s-av:hover{box-shadow:0 0 0 3px var(--primary-soft)}
.badge-dot{position:absolute;top:8px;right:9px;width:8px;height:8px;border-radius:50%;background:var(--danger);box-shadow:0 0 0 2px var(--topbar)}
/* search results panel (the Documents hub smart-search engine, now global) */
.sx-panel{position:absolute;top:calc(100% + 8px);left:0;right:0;background:var(--card);border:1px solid var(--border);border-radius:16px;box-shadow:var(--sh-3);max-height:min(72vh,600px);overflow:auto;padding:6px;z-index:70}
.og{padding:4px 0}
.og+.og{border-top:1px solid var(--border)}
.og-h{padding:8px 10px 4px;font-size:11px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:var(--faint)}
.opt{display:flex;align-items:center;gap:11px;padding:8px 10px;border-radius:11px;cursor:pointer}
.opt[aria-selected="true"]{background:var(--primary-soft)}
.opt .ot{flex:1;min-width:0;line-height:1.3}
.opt .ot b{display:block;font-size:14px;font-weight:650;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.opt .ot span{display:block;font-size:12.5px;color:var(--muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.opt mark{background:none;color:var(--primary-ink);font-weight:800}
.opt .oi{width:32px;height:32px;border-radius:9px;display:grid;place-items:center;background:var(--card-2);color:var(--muted);flex:none}
.opt .why{font-size:11.5px;font-weight:700;color:var(--muted);background:var(--card-2);border-radius:999px;padding:3px 9px;white-space:nowrap;flex:none}
.opt .why.near{color:var(--primary-ink);background:var(--primary-soft)}
.opt .why.mine{color:var(--pin-ink);background:var(--pin-bg)}
.og-note,.og-empty{margin:4px 10px 8px;font-size:12.5px;color:var(--muted)}
.og-foot .opt .ot b{font-weight:600}
.lg-tile{--c:var(--primary);width:32px;height:32px;border-radius:9px;flex:none;display:inline-grid;place-items:center;font:800 11px/1 var(--display);color:#fff;background:var(--c);letter-spacing:-.01em}
.lg-tile.xs{width:22px;height:22px;border-radius:6px;font-size:8.5px}
.lg-tile.sm{width:32px;height:32px}
.lg-tile.lg{width:52px;height:52px;border-radius:14px;font-size:15px}
.lg-tile.long{font-size:8.5px}
/* ---------- popovers, menus, modals, sheets, toasts ---------- */
#ovl{position:relative;z-index:80}
.pop{position:fixed;z-index:85;background:var(--card);color:var(--fg);border:1px solid var(--border);border-radius:16px;box-shadow:var(--sh-3);min-width:220px;max-width:calc(100vw - 24px);max-height:calc(100vh - 90px);overflow:auto;animation:pop-in .16s ease-out}
@keyframes pop-in{from{opacity:0;transform:translateY(-4px) scale(.98)}to{opacity:1;transform:none}}
.menu{padding:6px;display:flex;flex-direction:column;gap:1px}
.menu-h{padding:8px 10px 6px;font-size:11px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:var(--faint)}
.mi{display:flex;align-items:center;gap:11px;padding:9px 10px;border-radius:10px;font-size:14px;font-weight:600;text-align:left;width:100%}
.mi:hover,.mi:focus-visible{background:var(--card-2)}
.mi .i{color:var(--muted)}
.mi .mt{flex:1;min-width:0;line-height:1.3}
.mi .mt small{display:block;font-size:12px;font-weight:500;color:var(--muted)}
.mi kbd{font:600 11px var(--body);color:var(--faint);border:1px solid var(--border);border-radius:5px;padding:1px 5px}
.menu hr{border:0;border-top:1px solid var(--border);margin:5px 4px}
.pop-h{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:14px 16px 8px}
.pop-h h3{font:700 16px/1.2 var(--display);letter-spacing:-.01em}
.scrim{position:fixed;inset:0;z-index:60;background:var(--scrim);display:grid;place-items:center;padding:max(16px,env(safe-area-inset-top,0px)) max(16px,env(safe-area-inset-right,0px)) max(16px,env(safe-area-inset-bottom,0px)) max(16px,env(safe-area-inset-left,0px));animation:fade-in .18s ease-out}
@keyframes fade-in{from{opacity:0}}
.mcard{width:min(560px,100%);max-height:100%;overflow:auto;background:var(--card);border:1px solid var(--border);border-radius:20px;box-shadow:var(--sh-3);animation:pop-in .2s ease-out}
.mcard-h{display:flex;align-items:flex-start;gap:12px;padding:18px 20px 10px}
.mcard-h h2{font:750 20px/1.2 var(--display);letter-spacing:-.02em}
.mcard-h p{margin-top:4px;color:var(--muted);font-size:13.5px}
.mcard-h .s-ib{margin-left:auto;margin-top:-4px}
.mcard-b{padding:6px 20px 20px;display:flex;flex-direction:column;gap:14px}
.sheet-scrim{position:fixed;inset:0;z-index:60;background:var(--scrim);animation:fade-in .18s ease-out}
.sheet{position:fixed;top:0;right:0;bottom:0;z-index:61;width:min(460px,100vw);background:var(--card);border-left:1px solid var(--border);box-shadow:var(--sh-3);display:flex;flex-direction:column;animation:sheet-in .22s ease-out;padding-top:env(safe-area-inset-top,0px);padding-bottom:env(safe-area-inset-bottom,0px);padding-right:env(safe-area-inset-right,0px)}
@keyframes sheet-in{from{transform:translateX(30px);opacity:0}}
.sheet-h{display:flex;align-items:center;gap:12px;padding:16px 18px;border-bottom:1px solid var(--border)}
.sheet-h h2{font:750 18px/1.2 var(--display);letter-spacing:-.015em;flex:1}
.sheet-b{flex:1;overflow:auto;padding:14px 18px 22px;display:flex;flex-direction:column;gap:10px}
.toasts{position:fixed;left:50%;bottom:max(22px,calc(env(safe-area-inset-bottom,0px) + 14px));transform:translateX(-50%);z-index:95;display:flex;flex-direction:column;align-items:center;gap:8px;pointer-events:none;width:max-content;max-width:calc(100vw - 32px)}
.toast{pointer-events:auto;display:flex;align-items:center;gap:12px;background:var(--toast-bg);color:var(--toast-fg);border-radius:12px;padding:11px 14px;font-size:13.5px;font-weight:600;box-shadow:var(--sh-3);animation:toast-in .22s ease-out;max-width:100%}
.toast button{font-weight:800;text-decoration:underline;text-underline-offset:2px;flex:none}
@keyframes toast-in{from{opacity:0;transform:translateY(8px)}}
/* theme picker */
.tp{width:min(380px,calc(100vw - 24px))}
.tp-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px;padding:4px 14px 12px}
.tp-opt{display:flex;flex-direction:column;gap:8px;padding:8px;border:1.5px solid var(--border);border-radius:13px;text-align:left;position:relative}
.tp-opt:hover{border-color:var(--primary-line)}
.tp-opt[aria-checked="true"]{border-color:var(--primary);box-shadow:0 0 0 3px var(--primary-soft)}
.tp-sw{height:46px;border-radius:9px;display:grid;grid-template-columns:28% 1fr;overflow:hidden;border:1px solid rgba(128,128,128,.25)}
.tp-sw i{display:block}
.tp-sw .m{display:flex;flex-direction:column;gap:5px;padding:7px}
.tp-sw .m b{display:block;height:6px;border-radius:3px;width:70%}
.tp-sw .m s{display:block;height:12px;border-radius:4px;width:46%}
.tp-name{display:flex;align-items:center;justify-content:space-between;gap:6px;font-size:13px;font-weight:700;padding:0 2px}
.tp-name small{display:block;font-size:11.5px;font-weight:500;color:var(--muted)}
.tp-lock{position:absolute;top:12px;right:12px}
.tp-foot{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:10px 16px 14px;border-top:1px solid var(--border);font-size:13px;color:var(--muted)}
.tp-foot button{display:flex;align-items:center;gap:9px;font-weight:650;color:var(--fg)}
/* notifications + pro */
.nt{width:min(380px,calc(100vw - 24px))}
.nt-row{display:flex;gap:11px;padding:10px 14px;border-top:1px solid var(--border);text-align:left;width:100%}
.nt-row:hover{background:var(--card-2)}
.nt-row .ni{width:34px;height:34px;border-radius:10px;display:grid;place-items:center;flex:none}
.nt-row .nx{flex:1;min-width:0;font-size:13.5px;line-height:1.4}
.nt-row .nx small{display:block;color:var(--faint);font-size:12px;margin-top:2px}
.nt-row.unread .nx b::after{content:"";display:inline-block;width:7px;height:7px;border-radius:50%;background:var(--primary);margin-left:7px;vertical-align:2px}
.pro{width:min(400px,calc(100vw - 24px));padding:16px}
.pro-meter{display:flex;align-items:center;gap:14px;padding:12px 14px;border-radius:14px;background:var(--pin-bg);border:1px solid var(--pin-line)}
.pro-meter .s-ring{width:44px;height:44px}
.pro-meter .s-ring::after{width:32px;height:32px;background:var(--pin-bg)}
.pro-meter b{display:block;font:750 16px/1.2 var(--display)}
.pro-meter small{color:var(--muted);font-size:12.5px}
.pro-list{list-style:none;margin:14px 0;padding:0;display:flex;flex-direction:column;gap:9px;font-size:13.5px}
.pro-list li{display:flex;gap:10px;align-items:flex-start}
.pro-list .i{color:var(--primary);margin-top:1px}
.plans{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px;margin-bottom:12px}
.plans button{border:1.5px solid var(--border);border-radius:13px;padding:10px 12px;text-align:left}
.plans button[aria-pressed="true"]{border-color:var(--primary);box-shadow:0 0 0 3px var(--primary-soft)}
.plans b{display:block;font:750 14.5px var(--display)}
.plans span{font-size:12.5px;color:var(--muted)}
.plans strong{font:800 20px var(--display)}
/* ---------- phone drawer (the existing mobile drawer, with Menu and Room/Shelf tabs) ---------- */
.mdrawer-scrim{position:fixed;inset:0;z-index:62;background:var(--scrim);animation:fade-in .18s}
.mdrawer{position:fixed;top:0;bottom:0;left:0;z-index:63;width:min(320px,88vw);background:var(--sidebar);border-right:1px solid var(--border);display:flex;flex-direction:column;box-shadow:var(--sh-3);animation:slide-in .22s ease-out;padding-top:env(safe-area-inset-top,0px);padding-bottom:env(safe-area-inset-bottom,0px);padding-left:env(safe-area-inset-left,0px)}
@keyframes slide-in{from{transform:translateX(-100%)}}
.mdrawer .nav-brand{height:58px}
.md-tabs{display:flex;gap:4px;padding:0 12px 10px;border-bottom:1px solid var(--border)}
.md-tabs button{flex:1;height:34px;border-radius:9px;font-size:13px;font-weight:700;color:var(--muted);border:1px solid var(--border)}
.md-tabs button[aria-selected="true"]{background:var(--primary);color:var(--on-primary);border-color:var(--primary)}
.md-body{flex:1;overflow-y:auto;padding:12px;display:flex;flex-direction:column;gap:18px}
.mdrawer .rp-in{padding:0;overflow:visible;height:auto}
/* ---------- prototype panel ---------- */
.proto{position:fixed;left:max(14px,env(safe-area-inset-left,0px));bottom:max(14px,calc(env(safe-area-inset-bottom,0px) + 8px));z-index:50;display:inline-flex;align-items:center;gap:8px;height:34px;padding:0 13px 0 10px;border-radius:999px;background:var(--toast-bg);color:var(--toast-fg);font-size:12.5px;font-weight:700;box-shadow:var(--sh-2);opacity:.86}
.proto:hover,.proto[aria-expanded="true"]{opacity:1}
.proto .i{width:15px;height:15px}
.pp{width:min(360px,calc(100vw - 24px));padding:14px 16px 16px;display:flex;flex-direction:column;gap:14px}
.pp p{font-size:13px;color:var(--muted)}
.pp-row{display:flex;flex-direction:column;gap:7px}
.pp-seg{display:flex;gap:4px;background:var(--card-2);padding:4px;border-radius:11px}
.pp-seg button{flex:1;height:32px;border-radius:8px;font-size:12.5px;font-weight:700;color:var(--muted)}
.pp-seg button[aria-pressed="true"]{background:var(--card);color:var(--fg);box-shadow:var(--sh-1)}
.pp select{height:36px;border-radius:9px;border:1px solid var(--border-2);background:var(--input);padding:0 8px}
.pp-links{display:flex;flex-wrap:wrap;gap:8px}
/* ---------- responsive shell ---------- */
@media (max-width:1180px){.unlock .ul-t{display:none}.unlock{padding:0 8px}}
@media (max-width:1024px){.new-btn .nb-t{display:none}.new-btn{padding:0 10px}}
@media (max-width:860px){
.nav,.rpanel{display:none}
.top{padding-left:max(10px,env(safe-area-inset-left,0px));gap:6px}
.top-brand{display:flex}
.sx{position:static;flex:1 1 0;height:42px}
.sx-panel{left:10px;right:10px;top:calc(var(--top-h) - 4px)}
.sx-scope .sx-seg span{display:none}
.sx-go{width:46px}
.new-btn{display:none}
}
@media (max-width:560px){
.sx-scope{display:none}
.sx input{padding-left:12px;font-size:14px}
.top-acts .s-ib.theme-btn{display:none}
.unlock{padding:0 4px}
}
/* ---------- shell additions ---------- */
.menu-btn{display:none}
.nav-home{display:inline-grid;place-items:center;color:inherit;border-radius:10px}
.nav-row{color:inherit;text-decoration:none}
.nav-row .ri.raw{background:none;width:auto;height:auto}
.it a.t,.it button.t{color:inherit;text-decoration:none;text-align:left;min-width:0;background:none;padding:0;font:inherit}
.it a.t:hover,.it button.t:hover{color:var(--primary-ink)}
.it a.t:focus-visible,.it button.t:focus-visible{outline:2px solid var(--primary);outline-offset:1px;border-radius:4px}
.tw-b{width:16px;height:16px;flex:none;display:grid;place-items:center;color:var(--faint);border-radius:5px}
.tw-b:hover{background:var(--card-2);color:var(--fg)}
.tw-b .twi{width:13px;height:13px}
.it-empty{color:var(--faint);font-size:12.5px;cursor:default}
.it-empty:hover{background:none}
a.it{text-decoration:none}
.sdot.round{border-radius:50%}
.tp-sw.tp-auto{grid-template-columns:1fr 1fr}
.mi{color:var(--fg);text-decoration:none}
.mi-ic{width:34px;height:34px;border-radius:10px;display:grid;place-items:center;flex:none;background:var(--primary-soft);color:var(--primary-ink)}
.mi-ic .i{color:inherit}
.mi-ic.gold{background:var(--pin-bg);color:var(--pin-ink)}
.mi[aria-checked="true"]{color:var(--primary-ink)}
.new-pop{width:min(320px,calc(100vw - 24px))}
.acct-pop{width:min(290px,calc(100vw - 24px))}
.acct-h{display:flex;gap:11px;align-items:center;padding:10px 10px 12px;border-bottom:1px solid var(--border);margin-bottom:4px}
.acct-h b{display:block;font-size:14px}
.acct-h small{display:block;color:var(--muted);font-size:12px}
.fld{display:flex;flex-direction:column;gap:6px;font-size:13px;font-weight:650;color:var(--muted)}
.fld textarea{min-height:90px;padding:10px 12px;line-height:1.45;resize:vertical}
.ok-msg{display:flex;gap:10px;align-items:flex-start;padding:12px 14px;border-radius:12px;background:var(--primary-soft);color:var(--primary-ink);font-weight:650;font-size:14px}
.drop{display:flex;flex-direction:column;align-items:center;gap:6px;padding:26px 16px;border:1.5px dashed var(--primary-line);border-radius:14px;background:var(--primary-soft);color:var(--primary-ink);cursor:pointer;text-align:center}
.drop small{color:var(--muted);font-weight:500}
.drop:focus-within{outline:2px solid var(--primary);outline-offset:2px}
.drop-ic{width:48px;height:48px;border-radius:14px;background:var(--card);display:grid;place-items:center}
.note-row{display:flex;align-items:center;gap:10px;padding:10px 12px;border:1px solid var(--pin-line);background:var(--pin-note);border-radius:10px;font-size:14px;color:var(--fg)}
.note-row>span:nth-child(2){flex:1;min-width:0}
.nr-ic{color:var(--pin-ink);display:grid}
.muted{color:var(--muted)}
.proto{left:max(14px,calc(var(--main-left,0px) + 14px))}
.mcard-pro .pro{padding:20px}
.pp-links .s-chip{text-decoration:none}
/* category glyph (from the Course Hub) */
.cg{--cg-s:40px;width:var(--cg-s);height:var(--cg-s);flex:none;border-radius:calc(var(--cg-s) * .3);background:var(--cg-bg);color:var(--cg-deep);display:inline-grid;place-items:center;line-height:0}
.cg.plain{background:none}
/* mini paper thumbnail (from the Course Hub's DocThumb) */
.mth{--w:32px;width:var(--w);height:calc(var(--w) * 1.25);flex:none;border-radius:4px;background:#fff;border:1px solid #e1e6e4;box-shadow:0 1px 2px rgba(16,32,29,.08);padding:calc(var(--w) * .14);display:flex;flex-direction:column;gap:calc(var(--w) * .07);overflow:hidden}
.mth i{display:block;height:max(2px,calc(var(--w) * .05));border-radius:9px;background:#e3e8e6}
.mth i:nth-child(odd){width:86%}
.mth .h{height:max(3px,calc(var(--w) * .08));width:60%;background:var(--mth-a,#0a6a60)}
.mth .b{height:calc(var(--w) * .2);background:var(--mth-t,#e2f2ef)}
.mth .q{width:80%;border-left:max(2px,calc(var(--w) * .07)) solid var(--mth-a)}
.mth .g{height:calc(var(--w) * .26);background:repeating-linear-gradient(90deg,var(--mth-t) 0 30%,#fff 30% 34%)}
.mth-guide{--mth-a:#0A6A60;--mth-t:#E2F2EF}
.mth-exam{--mth-a:#B8430F;--mth-t:#FCEBE1}
.mth-notes{--mth-a:#3A64A8;--mth-t:#E7EEF9}
.mth-sheet{--mth-a:#6E45C9;--mth-t:#EFE9FB}
@media (max-width:860px){.menu-btn{display:inline-grid}}
/* document focus mode: the reading surface only */
#app.focus #nav,#app.focus #rpanel,#app.focus #top,#app.focus .amb{display:none!important}
.exit-focus{position:fixed;top:calc(14px + env(safe-area-inset-top,0px));right:calc(16px + env(safe-area-inset-right,0px));z-index:65;box-shadow:var(--sh-2)}
/* ===== 35-pages.css ===== */
/* ---------- shared page pieces ---------- */
.crumbs{display:flex;align-items:center;gap:6px;flex-wrap:wrap;font-size:12.5px;font-weight:600;color:var(--faint);margin-bottom:14px;min-height:20px}
.crumbs a{display:inline-flex;align-items:center;gap:5px;color:var(--muted);text-decoration:none;border-radius:6px}
.crumbs a:hover{color:var(--primary-ink);text-decoration:underline}
.crumbs [aria-current]{color:var(--fg);font-weight:700}
.crumbs .sep{width:12px;height:12px;color:var(--faint)}
.drow-sm{display:flex;align-items:center;gap:12px;padding:9px 10px;border-radius:12px;border:1px solid var(--border);background:var(--card);color:inherit;text-decoration:none;text-align:left;width:100%}
.drow-sm:hover{border-color:var(--primary-line)}
.drow-sm .t{flex:1;min-width:0;line-height:1.3}
.drow-sm b{display:block;font-size:14px;font-weight:700;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.drow-sm small{display:block;font-size:12.5px;color:var(--muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.drow-sm .pct{font-weight:800;font-size:13px;color:var(--primary-ink)}
.free-tag{display:inline-flex;align-items:center;height:18px;padding:0 6px;border-radius:5px;font-size:10px;font-weight:800;letter-spacing:.06em;background:var(--tb-bg);color:var(--tb-ink);border:1px solid var(--tb-line);text-transform:uppercase;flex:none}
.empty-box{display:flex;flex-direction:column;align-items:flex-start;gap:6px;padding:16px;border:1.5px dashed var(--border-2);border-radius:14px;color:var(--muted);font-size:13.5px}
.empty-box b{color:var(--fg);font-size:14.5px}
.eb-acts{display:flex;gap:8px;flex-wrap:wrap;margin-top:6px}
.eb-acts a{text-decoration:none}
.s-btn.danger{background:var(--danger);border-color:var(--danger);color:#fff}
.s-btn.quiet.danger{background:transparent;border-color:transparent;color:var(--danger)}
.s-btn.quiet.danger:hover{background:color-mix(in srgb,var(--danger) 10%,transparent)}
a.s-btn{text-decoration:none}
.mi.danger,.mi.danger .i{color:var(--danger)}
.sheet .crow-ic{width:38px;height:38px;flex:none;border-radius:11px;display:grid;place-items:center;background:var(--primary-soft);color:var(--primary-ink)}
/* ===== 36-extras.css ===== */
/* ---------- Study Room glue (the ported styles live in 45-study.scope.py) ---------- */
.pg-study .sr-head{margin:2px 0 14px}
.pg-study .sr-head h1{font:800 clamp(26px,3vw,34px)/1.1 var(--display);letter-spacing:-.02em;outline:none}
.pg-study .sr-head p{color:var(--muted);margin-top:6px;max-width:68ch}
.sr-tabs{display:flex;gap:4px;border-bottom:1px solid var(--border);margin-bottom:24px;overflow-x:auto;scrollbar-width:none}
.sr-tab{display:inline-flex;align-items:center;gap:8px;height:46px;padding:0 14px;font-weight:700;color:var(--muted);text-decoration:none;white-space:nowrap;position:relative}
.sr-tab:hover{color:var(--fg)}
.sr-tab[aria-current="page"]{color:var(--fg)}
.sr-tab[aria-current="page"]::after{content:"";position:absolute;left:10px;right:10px;bottom:-1px;height:2px;border-radius:2px;background:var(--primary)}
.sr-tab .count{font-size:11.5px;padding:1px 7px;border-radius:999px;background:var(--card-2);color:var(--muted)}
.sr-tab .new{font-size:9.5px;font-weight:800;letter-spacing:.1em;text-transform:uppercase;background:var(--pin-bg);color:var(--pin-ink);padding:3px 6px;border-radius:6px}
.pg-study .sr-body{display:flex;flex-direction:column;gap:28px}
.pg-study .sr-cm{display:flex;flex-direction:column;gap:28px}
.pg-study .sr-cm .cm-top{display:flex;align-items:center;gap:12px;flex-wrap:wrap}
.pg-study .sr-search{display:flex;align-items:center;gap:8px;height:40px;padding:0 12px;border:1px solid var(--border-2);border-radius:12px;background:var(--input);min-width:min(260px,100%);color:var(--muted)}
.pg-study .sr-search input{border:0;background:none;outline:none;flex:1;min-width:0;color:var(--fg)}
.pg-study .sr-cm .cm-top .stat{margin-left:auto}
.pg-study .h1{font:800 clamp(26px,3vw,38px)/1.1 var(--display);letter-spacing:-.02em}
.pg-study .ai-row{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:14px}
.pg-study .ai-card{display:flex;align-items:center;gap:12px;padding:14px;border:1px solid var(--border);border-radius:16px;background:var(--card);text-align:left}
.pg-study .ai-card:hover{border-color:var(--primary-line)}
.pg-study .ai-card .appicon{width:42px;height:42px;border-radius:12px;display:grid;place-items:center;color:#fff;background:var(--ai,var(--card-2));flex:none}
.pg-study .ai-card.new .appicon{color:var(--muted)}
.pg-study .ai-t{flex:1;min-width:0;display:flex;flex-direction:column;gap:3px}
.pg-study .ai-t b{font-size:14.5px}
.pg-study .ai-t>span{font-size:12.5px;color:var(--muted)}
.pg-study .ai-bar{height:5px;border-radius:9px;background:var(--card-2);overflow:hidden;margin-top:4px}
.pg-study .ai-bar i{display:block;height:100%;background:var(--primary)}
.pg-study .ai-pct{font-weight:800;color:var(--primary-ink)}
.pg-study .mk-acts{display:flex;align-items:center;gap:12px;flex-wrap:wrap;margin-top:auto}
.pg-study .st-sent{color:var(--muted);font-size:14px;margin:-6px 0 14px;max-width:90ch}
.pg-study .st-sent .linkish,.pg-study .mk-acts .linkish{color:var(--primary-ink);font-weight:700;text-decoration:underline;text-underline-offset:3px}
.pg-study .st-wait{height:100%;min-height:260px;display:grid;place-content:center;justify-items:center;gap:10px;color:var(--stage-muted,#8ba3a6);font-weight:600}
.pg-study .arena.sr-cm{display:block;padding:12px}
.pg-study .sr-empty{max-width:640px}
.pg-study .rmeta{font-size:13px;color:var(--muted)}
.pg-study .rmeta2{font-size:13.5px;color:var(--fg-2)}
.pop.sr-pop{padding:0;border:0;background:none;box-shadow:none;overflow:visible;min-width:0}
.pg-study .sr-ib .tokpop{position:static;animation:none}
.pg-study .sr-remix .kick{font-size:11px;font-weight:800;letter-spacing:.16em;text-transform:uppercase;color:var(--muted)}
.mcard.sr-mcard{width:min(600px,100%)}
.pg-study .sr-remix form{display:flex;flex-direction:column;gap:14px;padding:18px 20px 20px}
.pg-study .sheet .sr-st .pub-form{display:flex;flex-direction:column;gap:16px}
@media (max-width:860px){.pg-study .sr-cm .cm-top .stat{margin-left:0}.pg-study .studio{grid-template-columns:minmax(0,1fr);height:auto}.pg-study .pane-chat{max-height:520px}.pg-study .pane-preview{min-height:520px}}
/* ---------- Ask Hoot: the Documents hub button + Study buddy suggestions ---------- */
#hoot-root{position:fixed;right:calc(18px + env(safe-area-inset-right,0px));bottom:calc(18px + env(safe-area-inset-bottom,0px));z-index:70;display:flex;flex-direction:column;align-items:flex-end;gap:10px;pointer-events:none}
#hoot-root>*{pointer-events:auto}
#app.focus~#hoot-root{display:none}
.hb{display:flex;align-items:center;gap:0;border-radius:999px;background:var(--hero-card);color:var(--hero-card-fg);box-shadow:var(--sh-3);padding:5px;max-width:min(360px,calc(100vw - 32px));order:2}
.hb-face{width:46px;height:46px;border-radius:50%;background:var(--pin-bg);display:grid;place-items:center;flex:none}
.hb-face svg{width:38px;height:38px}
.hb-main{display:flex;align-items:center;gap:10px;padding:0 12px 0 10px;min-width:0;text-align:left;color:inherit}
.hb-k{display:block;font-size:10.5px;font-weight:800;letter-spacing:.14em;text-transform:uppercase;color:var(--hero-card-label)}
.hb-main>.i{flex:none;opacity:.8}
.hb-q{display:block;font-weight:700;font-size:13.5px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;transition:opacity .2s,transform .2s;max-width:240px}
.hb-q.out{opacity:0;transform:translateY(-6px)}
.hb-main span{min-width:0}
.hp{order:1;width:min(380px,calc(100vw - 32px));height:min(560px,calc(100vh - 120px));display:flex;flex-direction:column;background:var(--card);color:var(--fg);border:1px solid var(--border);border-radius:20px;box-shadow:var(--sh-3);overflow:hidden;animation:pop-in .2s ease-out}
.hp-h{display:flex;align-items:center;gap:10px;padding:12px 12px 12px 14px;border-bottom:1px solid var(--border)}
.hp-av{width:40px;height:40px;border-radius:12px;background:var(--pin-bg);display:grid;place-items:center;flex:none}
.hp-av svg{width:34px;height:34px}
.hoot-f.think{animation:hoot-think 1s ease-in-out infinite;transform-origin:50% 80%}
@keyframes hoot-think{0%,100%{transform:rotate(0)}50%{transform:rotate(-9deg) translateY(-1px)}}
.hp-n{flex:1;min-width:0;display:flex;flex-direction:column}
.hp-n b{font:750 15px/1.2 var(--display)}
.hp-n span{font-size:12px;color:var(--muted)}
.hp-b{flex:1;overflow:auto;padding:14px;display:flex;flex-direction:column;gap:12px}
.hp-hello b{font:800 19px/1.2 var(--display);letter-spacing:-.01em}
.hp-hello p{color:var(--muted);font-size:13.5px;margin:4px 0 12px}
.hp-chips{display:flex;flex-direction:column;gap:6px}
.hp-chips button{min-height:42px;padding:8px 12px;border:1px solid var(--border);border-radius:12px;text-align:left;font-weight:650;font-size:13.5px}
.hp-chips button:hover{border-color:var(--primary);background:var(--card-2)}
.hp-thread{display:flex;flex-direction:column;gap:8px}
.hm{max-width:86%;padding:9px 12px;border-radius:14px;font-size:14px;line-height:1.45}
.hm.me{align-self:flex-end;background:var(--primary);color:var(--on-primary);border-bottom-right-radius:4px}
.hm.bot{align-self:flex-start;background:var(--bubble);border-bottom-left-radius:4px}
.hm.typing{display:flex;gap:4px;padding:12px}
.hm.typing i{width:6px;height:6px;border-radius:50%;background:var(--muted);animation:hoot-dot 1s infinite}
.hm.typing i:nth-child(2){animation-delay:.15s}.hm.typing i:nth-child(3){animation-delay:.3s}
@keyframes hoot-dot{0%,100%{opacity:.3}50%{opacity:1}}
.hp-tools{display:flex;gap:6px;padding:8px 12px;border-top:1px solid var(--border);overflow-x:auto;scrollbar-width:none}
.hp-tools button{display:inline-flex;align-items:center;gap:6px;height:32px;padding:0 10px;border-radius:999px;background:var(--card-2);font-size:12.5px;font-weight:700;white-space:nowrap}
.hp-f{display:flex;gap:6px;padding:10px 12px 12px}
.hp-f input{flex:1;min-width:0;height:40px;border:1.5px solid var(--border-2);border-radius:12px;padding:0 12px;background:var(--input);color:var(--fg)}
.hp-f input:focus{outline:none;border-color:var(--primary)}
@media (max-width:640px){.hb-main{display:none}.hb{padding:4px}}
/* ---------- Welcome card ---------- */
.mcard.wc-card{width:min(820px,100%);padding:0;overflow:hidden}
.wc{display:grid;grid-template-columns:minmax(0,300px) minmax(0,1fr);min-height:420px}
.wc-art{background:var(--hero);color:var(--hero-fg);display:grid;place-items:center;padding:28px}
.wc-hoot{display:flex;flex-direction:column;align-items:center;gap:14px}
.wc-hoot svg{width:150px;height:150px;animation:wc-bob 3s ease-in-out infinite}
@keyframes wc-bob{0%,100%{transform:translateY(0)}50%{transform:translateY(-6px)}}
.wc-plate{text-align:center}.wc-plate b{display:block;font:800 18px var(--display)}.wc-plate span{font-size:13px;color:var(--hero-muted)}
.wc-ic{display:flex;flex-direction:column;align-items:center;gap:12px}
.wc-ic .i{width:72px;height:72px;padding:16px;border-radius:22px;background:rgba(255,255,255,.1);stroke-width:1.6}
.wc-ic b{font:800 20px var(--display)}
.wc-main{display:flex;flex-direction:column;gap:14px;padding:18px 22px 20px}
.wc-top{display:flex;align-items:center;gap:8px}
.wc-steps{display:flex;gap:4px;flex:1;flex-wrap:wrap}
.wc-steps button{font-size:12px;font-weight:700;color:var(--muted);padding:5px 9px;border-radius:999px}
.wc-steps button[aria-current="step"]{background:var(--primary-soft);color:var(--primary-ink)}
.wc-steps button.done{color:var(--fg)}
.wc-c{flex:1;display:flex;flex-direction:column;gap:10px}
.wc-eb{font-size:11px;font-weight:800;letter-spacing:.16em;text-transform:uppercase;color:var(--primary-ink)}
.wc-c h2{font:800 clamp(24px,3vw,30px)/1.15 var(--display);letter-spacing:-.02em}
.wc-c p{color:var(--muted);font-size:15px;line-height:1.55;max-width:56ch}
.wc-chips{display:flex;gap:6px;flex-wrap:wrap}
.wc-chips span{font-size:12.5px;font-weight:650;padding:5px 10px;border-radius:999px;background:var(--card-2)}
.wc-note{display:flex;gap:8px;align-items:center;font-size:13px;color:var(--muted)}
.wc-f{display:flex;gap:8px;align-items:center}
@media (max-width:700px){.wc{grid-template-columns:minmax(0,1fr)}.wc-art{padding:18px}.wc-hoot svg{width:90px;height:90px}.wc-ic .i{width:54px;height:54px;padding:12px}}
/* ---------- Preferences ---------- */
.pg-prefs .pf{max-width:760px;display:flex;flex-direction:column;gap:18px}
.pg-prefs h1{font:800 clamp(26px,3vw,34px)/1.1 var(--display);letter-spacing:-.02em;outline:none}
.pf-sec{border:1px solid var(--border);border-radius:18px;background:var(--card);padding:16px 18px;display:flex;flex-direction:column;gap:12px}
.pf-sec h2{font:750 16px/1.2 var(--display)}
.pf-d{font-size:13.5px;color:var(--muted)}
.pf-row{display:flex;align-items:center;gap:14px;flex-wrap:wrap}
.pf-row+.pf-row{border-top:1px solid var(--border);padding-top:12px}
.pf-t{flex:1;min-width:200px;display:flex;flex-direction:column;gap:2px}
.pf-t b{font-size:14.5px}.pf-t span{font-size:13px;color:var(--muted)}
.pf-seg{display:flex;gap:6px}
.pg-prefs .tp-grid{padding:0}
/* ===== 40-room.pcss ===== */
.pg-room .room{display:flex;flex-direction:column;gap:24px}
.pg-room .phone-only{display:none}
.pg-room .mbtn{display:inline-flex;align-items:center;gap:7px;height:34px;padding:0 12px;border:1px solid var(--primary-line);background:var(--primary-soft);color:var(--primary-ink);border-radius:10px;font-size:13px;font-weight:700}
.pg-room .wall{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1.1fr);gap:18px 28px;padding:22px;border:1px solid var(--wb-line);border-radius:20px;background:var(--wb-bg)}
.pg-room .wall-l{display:flex;flex-direction:column;gap:14px;min-width:0}
.pg-room .wall-r{display:flex;flex-direction:column;gap:10px;min-width:0}
.pg-room .greet{font:800 clamp(24px,2.6vw,30px)/1.12 var(--display);letter-spacing:-.025em;outline:none}
.pg-room .hsub{margin-top:6px;color:var(--muted);font-size:14.5px}
.pg-room .hsub b{color:var(--fg);font-weight:700}
.pg-room .today{display:flex;flex-direction:column;gap:8px;padding:13px 15px 11px;border:1px solid var(--border);border-radius:14px;background:var(--card)}
.pg-room .sub2{display:flex;align-items:center;gap:7px;font-size:11px;font-weight:800;letter-spacing:.12em;text-transform:uppercase;color:var(--muted)}
.pg-room .sub2 em{font-style:normal;letter-spacing:0;font-weight:700;background:var(--card-2);border-radius:99px;padding:0 7px;line-height:18px}
.pg-room .trows{display:flex;flex-direction:column;gap:2px;margin:0 -6px}
.pg-room .trow{display:flex;align-items:flex-start;gap:11px;padding:7px 6px;border-radius:10px}
.pg-room .trow:hover{background:var(--card-2)}
.pg-room .check{width:20px;height:20px;border-radius:50%;border:1.7px solid var(--faint);display:grid;place-items:center;flex:none;color:transparent;background:var(--card);margin-top:1px}
.pg-room .check .i{width:12px;height:12px;stroke-width:3}
.pg-room .check:hover{border-color:var(--primary);color:var(--primary)}
.pg-room .trow.is-done .check{background:var(--primary);border-color:var(--primary);color:var(--on-primary)}
.pg-room .trow-b{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}
.pg-room .trow-t{font-size:14px;font-weight:700;line-height:1.35;text-align:left}
.pg-room .trow-t:hover{color:var(--primary-ink)}
.pg-room .trow.is-done .trow-t{text-decoration:line-through;color:var(--muted);font-weight:500}
.pg-room .trow-s{display:flex;align-items:center;gap:6px;font-size:12.5px;color:var(--muted);min-width:0;flex-wrap:wrap}
.pg-room .trow-s a{color:inherit;text-decoration:none}
.pg-room .trow-s a:hover{color:var(--primary-ink);text-decoration:underline}
.pg-room .trow-s .hi{color:var(--ta-ink);font-weight:800}
.pg-room .trow-s .day{color:var(--pin-ink);font-weight:800}
.pg-room .trow-s .sdot{width:8px;height:8px;border-radius:2.5px}
.pg-room .clear-note{font-size:13px;color:var(--muted);padding:2px 0 4px}
.pg-room .modh{display:flex;align-items:center;justify-content:space-between;gap:8px;min-height:22px}
.pg-room .ey{display:inline-flex;align-items:center;gap:7px;font-size:11px;font-weight:800;letter-spacing:.12em;text-transform:uppercase;color:var(--primary-ink);white-space:nowrap}
.pg-room .ey.amber{color:var(--pin-ink)}
.pg-room .ey.rose{color:var(--ta-ink)}
.pg-room .modlink{white-space:nowrap;font-size:12.5px;font-weight:700;color:var(--muted);display:inline-flex;align-items:center;gap:3px;text-decoration:none}
.pg-room .modlink:hover{color:var(--primary-ink)}
.pg-room .stickies{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px}
.pg-room .sticky{position:relative;min-height:100px;padding:18px 12px 12px;background:var(--pin-note);border:1px solid var(--pin-line);border-radius:7px;font-size:13.5px;line-height:1.4;color:var(--fg);box-shadow:0 1px 2px var(--shadow);transform:rotate(var(--r,0deg));overflow-wrap:anywhere}
.pg-room .sticky::before{content:"";position:absolute;top:7px;left:50%;width:8px;height:8px;margin-left:-4px;border-radius:50%;background:var(--ta-ink)}
.pg-room .sticky .nx{position:absolute;top:3px;right:3px;width:24px;height:24px;border-radius:7px;display:grid;place-items:center;color:var(--pin-ink);opacity:0}
.pg-room .sticky:hover .nx, .pg-room .sticky .nx:focus-visible{opacity:.85}
.pg-room .sticky-more{display:grid;place-items:center;min-height:100px;border:1px dashed var(--pin-line);border-radius:7px;font-size:12.5px;font-weight:700;color:var(--pin-ink);text-align:center;padding:8px}
.pg-room .note-add{display:flex;gap:8px;margin-top:auto}
.pg-room .note-add input{flex:1;min-width:0;height:40px;border:1px solid var(--border-2);border-radius:10px;padding:0 12px;background:var(--input);font-size:14px}
.pg-room .note-add input:focus{outline:none;border-color:var(--primary);box-shadow:0 0 0 3px var(--primary-soft)}
.pg-room .note-add button{width:40px;height:40px;flex:none;border-radius:10px;background:var(--pin-btn);color:var(--on-pin-btn);display:grid;place-items:center}
.pg-room .toolbar{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
.pg-room .drawer2{display:inline-flex;align-items:center;gap:9px;height:40px;padding:0 13px 0 7px;border:1px solid var(--border);border-radius:11px;background:var(--card);font-weight:700;font-size:13.5px;white-space:nowrap}
.pg-room .drawer2:hover{border-color:var(--primary-line);background:var(--card-2)}
.pg-room .drawer2 .ic{width:26px;height:26px;border-radius:8px;background:var(--card-2);display:grid;place-items:center;color:var(--muted)}
.pg-room .drawer2 em{font-style:normal;font-size:12.5px;font-weight:700;color:var(--muted);font-variant-numeric:tabular-nums}
.pg-room .toolbar .spacer{flex:1}
.pg-room .toolbar a.s-btn{text-decoration:none}
.pg-room .lbl{display:flex;align-items:center;gap:8px;font-size:12px;font-weight:800;letter-spacing:.12em;text-transform:uppercase;color:var(--muted)}
.pg-room .lbl .count{font-weight:700;letter-spacing:0}
.pg-room .sechead{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:12px;min-height:28px}
.pg-room .lim{display:flex;align-items:center;gap:8px;font-size:12.5px;color:var(--muted);font-variant-numeric:tabular-nums}
.pg-room .lim .s-bar{width:64px}
.pg-room .lim.warn{color:var(--pin-ink);font-weight:800}
.pg-room .shelves2{display:grid;grid-template-columns:repeat(auto-fill,minmax(210px,1fr));gap:14px}
.pg-room .shelf2{position:relative;display:flex;flex-direction:column;min-width:0;border:1px solid color-mix(in srgb,var(--c) 28%,var(--border));border-radius:16px;background:var(--card);overflow:hidden;text-align:left;text-decoration:none;color:inherit;transition:transform .18s,box-shadow .18s,border-color .18s}
.pg-room .shelf2:hover{transform:translateY(-2px);box-shadow:0 12px 26px -14px var(--shadow);border-color:color-mix(in srgb,var(--c) 60%,var(--border))}
.pg-room .shelf-art{display:block;background:color-mix(in srgb,var(--c) 11%,var(--card));padding-bottom:10px}
.pg-room .pages{display:flex;gap:8px;align-items:flex-end;padding:15px 15px 0}
.pg-room .pg{width:40px;height:50px;background:var(--card);border:1px solid color-mix(in srgb,var(--c) 22%,var(--border));border-radius:4px;padding:7px 6px;display:flex;flex-direction:column;gap:4px;transition:transform .2s}
.pg-room .pg i{display:block;height:2px;border-radius:2px;background:color-mix(in srgb,var(--c) 32%,var(--border))}
.pg-room .pg i:nth-child(1){width:68%;background:color-mix(in srgb,var(--c) 55%,var(--border))}
.pg-room .pg i:nth-child(2){width:90%}
.pg-room .pg i:nth-child(3){width:80%}
.pg-room .pg i:nth-child(4){width:55%}
.pg-room .shelf2:hover .pg:nth-child(2){transform:translateY(-3px)}
.pg-room .shelf-line{display:block;height:2px;margin:0 12px;border-radius:2px;background:color-mix(in srgb,var(--c) 70%,var(--fg))}
.pg-room .shelf-body{display:flex;align-items:center;gap:10px;padding:12px 13px 14px}
.pg-room .shelf-txt{flex:1;min-width:0;display:flex;flex-direction:column;gap:5px}
.pg-room .shelf-name{font:800 15.5px/1.25 var(--display);color:color-mix(in srgb,var(--c) 58%,var(--fg));overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.pg-room .shelf-meta{font-size:12.5px;color:var(--muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.pg-room .chip2{display:inline-flex;align-items:center;gap:5px;height:24px;padding:0 9px;border:1px solid var(--border);border-radius:999px;background:var(--card);color:var(--muted);font-size:11.5px;font-weight:700;white-space:nowrap;align-self:flex-start;max-width:100%;overflow:hidden;text-overflow:ellipsis}
.pg-room .chip2.date{background:var(--pin-bg);border-color:var(--pin-line);color:var(--pin-ink)}
.pg-room .chip2.ex{background:var(--tc-bg);border-color:var(--tc-line);color:var(--tc-ink)}
.pg-room .shelf-go{width:30px;height:30px;border-radius:50%;border:1px solid var(--border);display:grid;place-items:center;color:var(--muted);flex:none;transition:background .18s,border-color .18s,color .18s}
.pg-room .shelf2:hover .shelf-go{background:var(--c);border-color:var(--c);color:#fff}
.pg-room .newtile{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px;min-height:158px;padding:18px;border:1.5px dashed var(--border-2);border-radius:16px;color:var(--muted);text-align:center}
.pg-room .newtile:hover{border-color:var(--primary-line);background:var(--primary-soft)}
.pg-room .newtile .plus{width:38px;height:38px;border-radius:50%;background:var(--primary-soft);color:var(--primary-ink);display:grid;place-items:center}
.pg-room .newtile b{font-size:12px;font-weight:800;letter-spacing:.12em;text-transform:uppercase;color:var(--primary-ink)}
.pg-room .newtile small{font-size:12.5px;color:var(--muted)}
.pg-room .combo{display:grid;grid-template-columns:minmax(0,1fr) 320px;gap:16px 22px;align-items:start}
.pg-room .desk-side{display:flex;flex-direction:column;gap:16px;min-width:0}
.pg-room .mod{display:flex;flex-direction:column;gap:12px;padding:15px 16px;border:1px solid var(--border);border-radius:16px;background:var(--card);min-width:0}
.pg-room .crows{display:flex;flex-direction:column;gap:2px;margin:0 -6px}
.pg-room .crow{display:flex;align-items:center;gap:11px;width:100%;padding:8px 6px;border-radius:12px;text-align:left;color:inherit;text-decoration:none}
.pg-room .crow:hover{background:var(--card-2)}
.pg-room .crow-ic{width:38px;height:38px;flex:none;border-radius:11px;display:grid;place-items:center;background:var(--primary-soft);color:var(--primary-ink)}
.pg-room .crow-ic.doc{background:var(--card-2);color:var(--muted)}
.pg-room .crow-b{flex:1;min-width:0;display:flex;flex-direction:column;gap:3px}
.pg-room .crow-b b{font-size:14px;font-weight:700;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.pg-room .crow-b small{font-size:12.5px;color:var(--muted)}
.pg-room .crow-b .s-bar{max-width:160px;margin-top:2px}
.pg-room .crow>.i{color:var(--muted)}
.pg-room .more-link{align-self:flex-start;margin-top:-4px}
.pg-room .feed-rows{display:flex;flex-direction:column;gap:2px;margin:0 -6px}
.pg-room .frow{display:flex;align-items:center;gap:12px;padding:8px 6px;border-radius:12px;color:inherit;text-decoration:none}
.pg-room .frow:hover{background:var(--card-2)}
.pg-room .frow .t{flex:1;min-width:0;line-height:1.3}
.pg-room .frow b{display:block;font-size:14px;font-weight:700;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.pg-room .frow small{display:block;font-size:12.5px;color:var(--muted)}
.pg-room .fr-ago{width:68px;flex:none;font-size:11.5px;font-weight:700;color:var(--faint)}
.pg-room .feed-foot{font-size:12.5px;color:var(--muted);border-top:1px solid var(--border);padding-top:10px}
.pg-room .ph{display:flex;align-items:flex-start;justify-content:space-between;gap:16px;margin-bottom:16px;flex-wrap:wrap}
.pg-room .ph-l{min-width:0;flex:1 1 320px}
.pg-room .ph h1{font:800 clamp(24px,2.6vw,30px)/1.1 var(--display);letter-spacing:-.025em;display:flex;align-items:center;gap:12px;outline:none}
.pg-room .sq{width:34px;height:34px;border-radius:10px;display:grid;place-items:center;color:#fff;flex:none}
.pg-room .sq .i{width:17px;height:17px}
.pg-room .ph .meta{display:flex;gap:12px;align-items:center;margin-top:8px;font-size:13px;color:var(--muted);flex-wrap:wrap}
.pg-room .chipc{display:inline-flex;align-items:center;gap:5px;border:1px solid var(--pin-line);background:var(--pin-bg);color:var(--pin-ink);border-radius:99px;padding:3px 10px;font-size:12px;font-weight:800}
.pg-room .ph .acts{display:flex;gap:8px;flex-wrap:wrap;justify-content:flex-end;align-items:center}
.pg-room .ex-name{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
.pg-room .title-in{font:800 clamp(22px,2.4vw,28px)/1.15 var(--display);letter-spacing:-.02em;border:1.5px dashed var(--primary-line);background:transparent;border-radius:10px;padding:2px 10px;min-width:0;width:min(420px,100%);color:var(--fg)}
.pg-room .title-in:focus{outline:none;border-style:solid;border-color:var(--primary);background:var(--input)}
.pg-room .ex-help{margin-top:8px;font-size:13.5px;color:var(--tc-ink);background:var(--tc-bg);border:1px solid var(--tc-line);border-radius:10px;padding:8px 12px;max-width:62ch}
.pg-room .views{display:flex;align-items:center;gap:4px;border-bottom:1px solid var(--border);margin-bottom:16px}
.pg-room .vt{display:inline-flex;align-items:center;gap:7px;padding:10px 12px;font-size:13.5px;font-weight:700;color:var(--muted);border-bottom:2px solid transparent;margin-bottom:-1px;white-space:nowrap}
.pg-room .vt[aria-selected="true"]{color:var(--primary-ink);border-bottom-color:var(--primary)}
.pg-room .vt:hover{color:var(--fg)}
.pg-room .vt .n{color:var(--faint);font-weight:700}
.pg-room .views .tools{margin-left:auto;display:flex;gap:4px;padding-bottom:4px}
.pg-room .board{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:14px;align-items:start}
.pg-room .colh{display:flex;align-items:center;gap:8px;padding:0 4px 9px}
.pg-room .colh b{font-size:12px;letter-spacing:.1em;text-transform:uppercase}
.pg-room .colh .n{font-size:12px;color:var(--faint);font-variant-numeric:tabular-nums}
.pg-room .colh .st{width:10px;height:10px;border-radius:50%}
.pg-room .col{display:flex;flex-direction:column;gap:9px;background:color-mix(in srgb,var(--border) 40%,transparent);border-radius:14px;padding:9px;min-height:130px;transition:background .15s,box-shadow .15s}
.pg-room .col.over{background:var(--primary-soft);box-shadow:inset 0 0 0 2px var(--primary-line)}
.pg-room .card{position:relative;background:var(--card);border:1px solid var(--border);border-radius:12px;padding:11px 12px 11px 26px;display:flex;flex-direction:column;gap:8px;text-align:left;cursor:grab}
.pg-room .card:hover{border-color:var(--primary-line)}
.pg-room .card.dragging{opacity:.5}
.pg-room .card .grip{position:absolute;left:7px;top:12px;color:var(--faint)}
.pg-room .card-t{font-size:13.5px;font-weight:700;line-height:1.35;text-align:left}
.pg-room .card-t:hover{color:var(--primary-ink)}
.pg-room .card.done .card-t{text-decoration:line-through;color:var(--muted)}
.pg-room .card .row{display:flex;gap:6px;flex-wrap:wrap;align-items:center}
.pg-room .pill{display:inline-flex;align-items:center;gap:4px;border-radius:99px;padding:2px 8px;font-size:11.5px;font-weight:700;border:1px solid var(--border);color:var(--muted);background:var(--card-2);white-space:nowrap;max-width:100%;overflow:hidden;text-overflow:ellipsis;text-decoration:none}
.pg-room .pill.due{border-color:var(--pin-line);background:var(--pin-bg);color:var(--pin-ink)}
.pg-room .pill.late{border-color:color-mix(in srgb,var(--danger) 40%,transparent);color:var(--danger);background:color-mix(in srgb,var(--danger) 9%,transparent)}
.pg-room .pill.doc{color:var(--primary-ink);border-color:var(--primary-line);background:var(--primary-soft)}
.pg-room a.pill.doc:hover{text-decoration:underline}
.pg-room .pill.hi{color:var(--ta-ink);border-color:var(--ta-line);background:var(--ta-bg)}
.pg-room .addc{border:1px dashed var(--border-2);border-radius:11px;padding:9px;color:var(--muted);font-size:12.5px;font-weight:700;display:flex;align-items:center;gap:6px;justify-content:center}
.pg-room .addc:hover{border-color:var(--primary-line);color:var(--primary-ink);background:var(--card)}
.pg-room .addc.wide{width:100%;padding:12px}
.pg-room .tbl-wrap{overflow-x:auto;border-radius:13px}
.pg-room .tbl{width:100%;min-width:640px;border-collapse:separate;border-spacing:0;border:1px solid var(--border);border-radius:13px;overflow:hidden;background:var(--card);font-size:13.5px}
.pg-room .tbl th{text-align:left;padding:10px 12px;font-size:11px;font-weight:800;letter-spacing:.1em;text-transform:uppercase;color:var(--muted);background:var(--card-2);border-bottom:1px solid var(--border)}
.pg-room .tbl td{padding:10px 12px;border-bottom:1px solid var(--border);vertical-align:middle}
.pg-room .tbl tr:last-child td{border-bottom:0}
.pg-room .tbl td.t{font-weight:700;display:flex;align-items:center;gap:9px}
.pg-room .tt{text-align:left;font-weight:700}
.pg-room .tt:hover{color:var(--primary-ink)}
.pg-room .cb{width:18px;height:18px;border-radius:5px;border:1.6px solid var(--faint);display:inline-grid;place-items:center;flex:none;color:transparent}
.pg-room .cb.on{background:var(--primary);border-color:var(--primary);color:var(--on-primary)}
.pg-room .cb .i{width:11px;height:11px;stroke-width:3}
.pg-room .lgroup{display:flex;flex-direction:column;gap:7px}
.pg-room .lg-h{margin:8px 0 2px;font-size:11.5px;font-weight:800;letter-spacing:.12em;text-transform:uppercase;color:var(--muted);display:flex;gap:8px;align-items:center}
.pg-room .lg-h .n{color:var(--faint);font-weight:700;letter-spacing:0}
.pg-room .lrow{display:flex;align-items:center;gap:11px;background:var(--card);border:1px solid var(--border);border-radius:11px;padding:9px 12px;font-size:13.5px;flex-wrap:wrap}
.pg-room .lrow .cb{border-radius:50%}
.pg-room .lrow .t{flex:1;min-width:140px;font-weight:700;text-align:left;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.pg-room .lrow.done .t{text-decoration:line-through;color:var(--muted);font-weight:500}
.pg-room .dlist{display:flex;flex-direction:column;gap:8px}
.pg-room .docrow{display:flex;align-items:center;gap:13px;background:var(--card);border:1px solid var(--border);border-radius:12px;padding:10px 12px}
.pg-room .docrow .t{flex:1;min-width:0;color:inherit;text-decoration:none}
.pg-room .docrow .t:hover b{color:var(--primary-ink);text-decoration:underline}
.pg-room .docrow b{display:block;font-size:14px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.pg-room .docrow small{color:var(--muted);font-size:12.5px}
.pg-room .ns-card{width:min(900px,100%);display:flex;flex-direction:column;max-height:100%;overflow:hidden}
.pg-room .mh{display:flex;align-items:flex-start;gap:12px;padding:18px 20px 12px}
.pg-room .mh h2{font:800 21px var(--display);letter-spacing:-.02em}
.pg-room .mh p{margin-top:3px;font-size:13.5px;color:var(--muted)}
.pg-room .mh .s-ib{margin-left:auto}
.pg-room .mb{display:grid;grid-template-columns:290px 1fr;flex:1;min-height:0;overflow:auto}
.pg-room .mcol{padding:4px 20px 18px;display:flex;flex-direction:column;gap:16px}
.pg-room .mcol.right{border-left:1px solid var(--border);background:color-mix(in srgb,var(--card-2) 60%,transparent);padding-top:12px}
.pg-room .title-in2{font:800 16px var(--display)!important}
.pg-room .s-input.bad{border-color:var(--danger)}
.pg-room .hint{font-size:12px;color:var(--muted);font-weight:500}
.pg-room .kind{display:flex;gap:6px;flex-wrap:wrap}
.pg-room .kind button{border:1px solid var(--border-2);border-radius:99px;padding:6px 11px;font-size:12.5px;font-weight:700;color:var(--muted)}
.pg-room .kind button[aria-checked="true"]{border-color:var(--primary);background:var(--primary-soft);color:var(--primary-ink)}
.pg-room .sw{display:flex;gap:8px;flex-wrap:wrap}
.pg-room .sw button{width:26px;height:26px;border-radius:50%;border:2px solid transparent;box-shadow:inset 0 0 0 2px var(--card)}
.pg-room .sw button[aria-checked="true"]{border-color:var(--fg)}
.pg-room .tp-head{display:flex;justify-content:space-between}
.pg-room .tpls{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px}
.pg-room .tpl{border:1px solid var(--border);background:var(--card);border-radius:13px;padding:10px;display:flex;flex-direction:column;gap:7px;text-align:left}
.pg-room .tpl:hover{border-color:var(--primary-line)}
.pg-room .tpl[aria-checked="true"]{border-color:var(--primary);box-shadow:0 0 0 3px var(--primary-soft)}
.pg-room .tpl .pv{height:64px;border-radius:8px;background:var(--card-2);border:1px solid var(--border);padding:6px;display:flex;gap:4px;overflow:hidden}
.pg-room .tpl .pv .col{flex:1;display:flex;flex-direction:column;gap:3px;background:none;padding:0;min-height:0;border-radius:0}
.pg-room .tpl .pv .col i{display:block;height:9px;border-radius:3px;background:var(--border-2)}
.pg-room .tpl .pv .col i.a{background:var(--primary);opacity:.55}
.pg-room .tpl .pv .col i.g{background:var(--pin-btn);opacity:.7}
.pg-room .tpl .pv.blank{align-items:center;justify-content:center;color:var(--faint);font-size:11px;font-weight:800;letter-spacing:.1em;text-transform:uppercase}
.pg-room .tpl .pv.list{flex-direction:column}
.pg-room .tpl .pv.list i{display:block;height:8px;border-radius:3px;background:var(--border-2)}
.pg-room .tpl .pv.cal{display:grid;grid-template-columns:repeat(7,1fr);gap:2px;align-content:start}
.pg-room .tpl .pv.cal i{display:block;height:11px;border-radius:2px;background:var(--border-2)}
.pg-room .tpl .pv.cal i.g{background:var(--pin-btn)}
.pg-room .tpl b{font-size:13.5px}
.pg-room .tpl small{color:var(--muted);font-size:12px;line-height:1.35}
.pg-room .tpl .pick{font-size:10.5px;font-weight:800;letter-spacing:.12em;text-transform:uppercase;color:var(--primary-ink)}
.pg-room .preview{border:1px solid var(--border);background:var(--card);border-radius:13px;padding:13px 14px;display:flex;flex-direction:column;gap:8px}
.pg-room .preview ul{margin:0;padding-left:18px;font-size:13.5px;line-height:1.55}
.pg-room .preview .ex{font-size:12.5px;color:var(--faint)}
.pg-room .mf{display:flex;align-items:center;gap:10px;padding:12px 20px;border-top:1px solid var(--border);background:var(--card);flex-wrap:wrap}
.pg-room .mf .lim{margin-right:auto}
.pg-room .ns-note{padding:0 20px 12px;font-size:12px;color:var(--faint);background:var(--card)}
.pg-room .task-form{display:flex;flex-direction:column;gap:14px}
.pg-room .pri-tog{display:flex;align-items:center;gap:10px;font-weight:650;font-size:14px}
.pg-room .task-acts{display:flex;gap:8px;flex-wrap:wrap}
.pg-room .add-row{display:flex;align-items:center;gap:11px;padding:8px 2px;border-bottom:1px solid var(--border)}
.pg-room .add-row .t{flex:1;min-width:0}
.pg-room .add-row b{display:block;font-size:13.5px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.pg-room .add-row small{display:block;font-size:12px;color:var(--muted)}
.pg-room .added{display:inline-flex;align-items:center;gap:5px;color:var(--primary-ink);font-weight:700;font-size:13px}
@media (max-width:1180px){.pg-room .combo{grid-template-columns:minmax(0,1fr) 290px}}
@media (max-width:1024px){.pg-room .wall{grid-template-columns:1fr}
.pg-room .combo{grid-template-columns:1fr}}
@media (max-width:860px){.pg-room .phone-only{display:flex}
.pg-room button.phone-only{display:inline-flex}
.pg-room .views{overflow-x:auto;scrollbar-width:none}
.pg-room .views .tools{display:none}
.pg-room .board{display:flex;overflow-x:auto;scroll-snap-type:x mandatory;gap:10px;padding-bottom:6px;scrollbar-width:none}
.pg-room .board>.bcol{min-width:82%;scroll-snap-align:start}
.pg-room .mb{grid-template-columns:1fr}
.pg-room .mcol.right{border-left:0;border-top:1px solid var(--border)}
.pg-room .tpls{grid-template-columns:1fr 1fr}
.pg-room .ph .acts{justify-content:flex-start}}
@media (max-width:560px){.pg-room .wall{padding:15px}
.pg-room .stickies{grid-template-columns:repeat(2,minmax(0,1fr))}
.pg-room .stickies>:nth-child(3){display:none}
.pg-room .toolbar{flex-wrap:nowrap;overflow-x:auto;scrollbar-width:none;padding-bottom:2px;margin-right:-16px}
.pg-room .toolbar .spacer{display:none}
.pg-room .shelves2{grid-template-columns:1fr 1fr}
.pg-room .pg{width:30px;height:40px}
.pg-room .fr-ago{display:none}
.pg-room .mf .lim{width:100%}}
/* ===== 43-course.pcss ===== */
.pg-course .page-in{max-width:1240px}
.pg-course .cp-hero{position:relative;overflow:hidden;display:flex;flex-wrap:wrap;align-items:center;gap:8px 32px;padding:34px 38px 32px;background:var(--ct-tint);border:1px solid var(--ct-light);border-radius:28px;margin-bottom:30px}
.pg-course .cp-hero-main{flex:999 1 440px;min-width:0;display:flex;flex-direction:column;gap:16px}
.pg-course .cp-hero-art{flex:1 1 280px;min-width:0;display:flex;justify-content:center;align-items:center}
.pg-course .cp-hero-art svg{width:min(340px,100%);height:auto;display:block}
.pg-course .cp-tags{display:flex;flex-wrap:wrap;align-items:center;gap:8px}
.pg-course .cp-pill{display:inline-flex;align-items:center;gap:7px;height:32px;padding:0 13px;border-radius:999px;background:var(--card);font-size:13.5px;font-weight:700;color:var(--fg);text-decoration:none}
.pg-course .cp-pill.brand{color:var(--primary-ink);padding-left:10px}
.pg-course .cp-pill.brand .i{color:var(--primary)}
.pg-course .cp-pill.cat{color:var(--ct-ink);padding-left:4px}
.pg-course .cp-pill.soft{background:color-mix(in srgb,var(--card) 60%,transparent);color:var(--muted);font-weight:600}
.pg-course a.cp-pill.soft:hover{color:var(--fg);background:var(--card)}
.pg-course .cp-pill .mono{--s:18px;border-radius:5px}
.pg-course .cp-titles{display:flex;flex-direction:column;gap:2px}
.pg-course .cp-code{font:700 20px var(--display);color:var(--ct-ink)}
.pg-course .cp-hero h1{font:700 clamp(32px,4.2vw,50px)/1.04 var(--display);letter-spacing:-.025em;text-wrap:balance;outline:none}
.pg-course .cp-blurb{max-width:62ch;font-size:16.5px;line-height:1.6;color:color-mix(in srgb,var(--fg) 82%,transparent)}
.pg-course .cp-glance{list-style:none;margin:0;padding:0;display:flex;flex-wrap:wrap;gap:8px}
.pg-course .cp-glance li{display:inline-flex;align-items:baseline;gap:6px;padding:7px 14px;border-radius:12px;background:var(--card);font-size:14px;color:var(--muted)}
.pg-course .cp-glance strong{font:700 18px var(--display);color:var(--fg)}
.pg-course .cp-topics{display:flex;flex-wrap:wrap;align-items:center;gap:6px}
.pg-course .cp-topics>span:first-child{font-size:13.5px;font-weight:700;color:var(--muted);margin-right:4px}
.pg-course .topic{padding:4px 12px;border-radius:999px;border:1px solid var(--ct-light);background:color-mix(in srgb,var(--card) 60%,transparent);font-size:13.5px;color:var(--ct-ink)}
.pg-course .cp-acts{display:flex;flex-wrap:wrap;gap:10px;padding-top:4px}
.pg-course .cp-btn{height:44px;padding:0 16px;border-radius:12px;border:1px solid var(--border-2);background:var(--card);color:var(--fg);font-size:14.5px;font-weight:700;display:inline-flex;align-items:center;justify-content:center;gap:8px;text-decoration:none;white-space:nowrap}
.pg-course .cp-btn:hover{background:var(--card-2)}
.pg-course .cp-btn.lg{height:48px;padding:0 20px;border-radius:14px;font-size:15px}
.pg-course .cp-btn.icon{width:48px;padding:0}
.pg-course .cp-btn.primary{background:var(--primary);border-color:var(--primary);color:var(--on-primary)}
.pg-course .cp-btn.primary:hover{background:var(--primary-ink);border-color:var(--primary-ink)}
.pg-course .cp-btn.more{align-self:center;margin-top:4px}
.pg-course .cp-btn.gold{background:var(--pin-btn);border-color:var(--pin-btn);color:var(--on-pin-btn);align-self:flex-start}
.pg-course .cp-cols{display:flex;flex-wrap:wrap;gap:28px;align-items:flex-start}
.pg-course .cp-docs{flex:999 1 560px;min-width:0;display:flex;flex-direction:column;gap:16px}
.pg-course .cp-side{flex:1 1 320px;min-width:0;display:flex;flex-direction:column;gap:18px}
.pg-course .cp-docs-h{display:flex;align-items:flex-end;justify-content:space-between;gap:16px;flex-wrap:wrap}
.pg-course .cp-docs-h h2{font:700 30px/1.15 var(--display);letter-spacing:-.015em}
.pg-course .cp-docs-h p{margin-top:4px;font-size:15px;color:var(--muted)}
.pg-course .cp-seg{display:inline-flex;padding:4px;gap:4px;background:var(--card-2);border-radius:14px}
.pg-course .cp-seg button{width:44px;height:40px;border-radius:10px;display:grid;place-items:center;color:var(--muted)}
.pg-course .cp-seg button[aria-pressed="true"]{background:var(--card);color:var(--fg);box-shadow:var(--sh-1)}
.pg-course .cp-tools{display:flex;flex-wrap:wrap;gap:10px}
.pg-course .cp-search{flex:1 1 260px;height:50px;display:flex;align-items:center;gap:10px;padding:0 16px;background:var(--input);border:1.5px solid var(--border-2);border-radius:14px;color:var(--muted)}
.pg-course .cp-search:focus-within{border-color:var(--primary);box-shadow:0 0 0 4px var(--primary-soft)}
.pg-course .cp-search input{flex:1;min-width:0;border:0;outline:none;background:transparent;font-size:15.5px;color:var(--fg)}
.pg-course .cp-sort{flex:0 0 auto;height:50px;display:flex;align-items:center;gap:6px;padding:0 8px 0 16px;background:var(--input);border:1.5px solid var(--border-2);border-radius:14px;font-size:14.5px;color:var(--muted)}
.pg-course .cp-sort select{height:44px;border:0;background:transparent;font-size:15px;font-weight:700;color:var(--fg);cursor:pointer}
.pg-course .cp-chips{display:flex;flex-wrap:wrap;gap:8px}
.pg-course .cp-chip{height:42px;padding:0 16px;border-radius:999px;border:1px solid var(--border-2);background:var(--card);color:var(--fg);font-size:14.5px;font-weight:700;display:inline-flex;align-items:center;gap:8px}
.pg-course .cp-chip span{font-size:13px;font-weight:700;padding:1px 7px;border-radius:99px;background:var(--card-2)}
.pg-course .cp-chip:hover{border-color:var(--primary-line)}
.pg-course .cp-chip[aria-pressed="true"]{background:var(--chip-on);border-color:var(--chip-on);color:var(--on-chip)}
.pg-course .cp-chip[aria-pressed="true"] span{background:color-mix(in srgb,var(--on-chip) 18%,transparent)}
.pg-course .ty-guide{--ty:#0A6A60;--ty-t:#E2F2EF}
.pg-course .ty-exam{--ty:#B8430F;--ty-t:#FCEBE1}
.pg-course .ty-notes{--ty:#3A64A8;--ty-t:#E7EEF9}
.pg-course .ty-worksheet{--ty:#6E45C9;--ty-t:#EFE9FB}
:root.dk .pg-course .ty-guide{--ty:#5fd3c2;--ty-t:#123430}
:root.dk .pg-course .ty-exam{--ty:#f39a6e;--ty-t:#3a2218}
:root.dk .pg-course .ty-notes{--ty:#8fb4f0;--ty-t:#1a2a44}
:root.dk .pg-course .ty-worksheet{--ty:#b9a1f5;--ty-t:#2a2244}
.pg-course .ty-label{display:inline-flex;align-items:center;gap:7px;font-size:13px;font-weight:700;color:var(--ty)}
.pg-course .ty-label i{width:7px;height:7px;border-radius:99px;background:var(--ty)}
.pg-course .ty-label.sm{font-size:12.5px}
.pg-course .dthumb{--w:62px;--h:78px;--u:2px;width:var(--w);height:var(--h);flex:none;background:#fff;border:1px solid #E1E6E4;border-radius:max(4px,calc(var(--w) * .06));box-shadow:0 1px 2px rgba(16,32,29,.06),0 6px 14px rgba(16,32,29,.06);padding:max(6px,calc(var(--w) * .13));display:flex;flex-direction:column;gap:max(2px,calc(var(--u) * 1.15));overflow:hidden}
.pg-course .dthumb.scan{background:#F3EBDA;border-color:#E3D6BC;transform:rotate(-2deg)}
.pg-course .dt-top{display:flex;align-items:center;justify-content:space-between;gap:var(--u);flex:none}
.pg-course .dt-head{display:block;height:calc(var(--u) * 1.7);width:62%;border-radius:99px;background:var(--ty)}
.pg-course .scan .dt-head{background:#6F6250}
.pg-course .dt-mark{display:block;width:calc(var(--u) * 3.2);height:calc(var(--u) * 3.2);border-radius:99px;border:max(1px,calc(var(--u) / 2)) solid #D1495B;flex:none}
.pg-course .dt-box{display:flex;flex-direction:column;gap:var(--u);padding:var(--u);border-radius:var(--u);background:var(--ty-t);flex:none}
.pg-course .dt-box i{display:block;height:var(--u);width:88%;border-radius:99px;background:var(--ty);opacity:.5}
.pg-course .dt-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:1px;background:#E1E6E4;border:1px solid #E1E6E4;border-radius:var(--u);overflow:hidden;flex:none}
.pg-course .dt-grid i{height:calc(var(--u) * 2.6);background:#fff}
.pg-course .dt-grid i.t{background:var(--ty-t)}
.pg-course .dt-q{display:flex;align-items:center;gap:var(--u);flex:none}
.pg-course .dt-q b{width:calc(var(--u) * 2.2);height:calc(var(--u) * 2.2);border-radius:99px;border:max(1px,calc(var(--u) / 2.4)) solid var(--ty);flex:none}
.pg-course .dt-q i{height:var(--u);border-radius:99px;background:#E3E8E6}
.pg-course .dt-l{display:block;height:var(--u);border-radius:99px;background:#E3E8E6;flex:none}
.pg-course .scan .dt-l{background:#BFB096}
:root.dk .pg-course .dthumb:not(.scan){background:#e9eeec}
.pg-course .cp-list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:10px}
.pg-course .cp-row{display:flex;align-items:center;gap:18px;padding:14px;background:var(--card);border:1px solid var(--border);border-radius:18px;transition:border-color .15s,box-shadow .15s,transform .15s}
.pg-course .cp-row:hover{border-color:var(--primary-line);box-shadow:0 8px 22px -12px var(--shadow);transform:translateY(-1px)}
.pg-course .cp-row.seen{border-color:color-mix(in srgb,var(--primary) 30%,var(--border))}
.pg-course .r-b{flex:1 1 auto;min-width:0;display:flex;flex-direction:column;gap:4px}
.pg-course .cp-title{font:600 19px/1.3 var(--display);letter-spacing:-.01em;color:var(--fg);text-decoration:none;text-align:left}
.pg-course .cp-title:hover{color:var(--primary-ink);text-decoration:underline;text-underline-offset:3px}
.pg-course .cp-meta{display:flex;flex-wrap:wrap;align-items:center;gap:4px 16px;font-size:14px;color:var(--muted)}
.pg-course .cp-meta>span{display:inline-flex;align-items:center;gap:5px}
.pg-course .m-free{color:var(--success);font-weight:700}
.pg-course .m-free i{width:7px;height:7px;border-radius:99px;background:currentColor}
.pg-course .m-pro{color:var(--primary-ink);font-weight:700}
.pg-course .m-seen .i{color:var(--primary)}
.pg-course .cp-save{flex:none;width:44px;height:44px;border-radius:12px;display:grid;place-items:center;color:var(--muted)}
.pg-course .cp-save:hover{background:var(--card-2)}
.pg-course .cp-save.on{color:var(--star)}
.pg-course .cp-save.on .i{fill:var(--pin-btn);stroke:var(--star)}
.pg-course .cp-save.wide{width:auto;padding:0 18px;height:48px;border:1px solid var(--border-2);display:inline-flex;gap:8px;font-weight:700}
.pg-course .cp-save.over{position:absolute;top:10px;right:10px;background:color-mix(in srgb,var(--card) 88%,transparent)}
.pg-course .cp-open{flex:none;height:44px;padding:0 16px;border-radius:12px;background:var(--primary-soft);color:var(--primary-ink);font-size:14.5px;font-weight:700;display:inline-flex;align-items:center;gap:6px;text-decoration:none}
.pg-course .cp-open:hover{background:color-mix(in srgb,var(--primary) 22%,var(--primary-soft))}
.pg-course .cp-grid{list-style:none;margin:0;padding:0;display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:14px}
.pg-course .cp-gcard{display:flex;flex-direction:column;background:var(--card);border:1px solid var(--border);border-radius:20px;overflow:hidden}
.pg-course .cp-gcard:hover{border-color:var(--primary-line)}
.pg-course .g-art{position:relative;height:150px;padding-top:22px;background:var(--ty-t);display:flex;justify-content:center;overflow:hidden}
.pg-course .g-b{flex:1 1 auto;padding:14px 16px 16px;display:flex;flex-direction:column;gap:5px}
.pg-course .g-b .cp-title{font-size:17px}
.pg-course .g-b .cp-meta{margin-top:auto;padding-top:6px;font-size:13px;gap:4px 12px}
.pg-course .cp-empty{padding:44px 24px;text-align:center;background:var(--card);border:1.5px dashed var(--border-2);border-radius:22px;display:flex;flex-direction:column;align-items:center;gap:8px;color:var(--muted)}
.pg-course .cp-empty strong{font:700 20px var(--display);color:var(--fg)}
.pg-course .ce-ic{width:52px;height:52px;border-radius:16px;background:var(--primary-soft);color:var(--primary-ink);display:grid;place-items:center}
.pg-course .cp-note{font-size:13px;color:var(--faint);text-align:center}
.pg-course .cp-one{display:flex;flex-direction:column;gap:14px}
.pg-course .one-card{display:flex;flex-wrap:wrap;align-items:center;gap:28px;padding:24px;background:var(--card);border:1px solid var(--border);border-radius:24px}
.pg-course .one-art{width:210px;height:200px;padding-top:22px;background:var(--ty-t);border-radius:18px;overflow:hidden;display:flex;justify-content:center}
.pg-course .one-b{flex:1 1 300px;min-width:0;display:flex;flex-direction:column;gap:10px}
.pg-course .one-b h3{font:700 30px/1.15 var(--display);letter-spacing:-.015em}
.pg-course .one-acts{display:flex;flex-wrap:wrap;gap:10px;padding-top:6px}
.pg-course .cp-start{display:flex;flex-wrap:wrap;align-items:center;gap:16px;padding:18px 20px;border:1.5px dashed var(--border-2);border-radius:20px}
.pg-course .cp-start>div{flex:1 1 260px;min-width:0;display:flex;flex-direction:column}
.pg-course .cp-start b{font-size:15.5px}
.pg-course .cp-start span{font-size:14.5px;color:var(--muted)}
.pg-course .cs-ic{width:46px;height:46px;border-radius:14px;background:var(--pin-bg);color:var(--pin-ink);display:grid;place-items:center;flex:none}
.pg-course .cp-card{background:var(--card);border:1px solid var(--border);border-radius:22px;padding:20px;display:flex;flex-direction:column;gap:12px}
.pg-course .cp-card h3{font:700 19px/1.25 var(--display)}
.pg-course .cp-card .sub{font-size:14px;color:var(--muted);margin-top:-6px}
.pg-course .ex-path{list-style:none;margin:0;padding:0;display:flex;flex-direction:column}
.pg-course .ex-path li{display:flex;gap:14px}
.pg-course .ex-rail{flex:none;display:flex;flex-direction:column;align-items:center}
.pg-course .ex-dot{width:34px;height:34px;border-radius:99px;background:var(--primary-soft);color:var(--primary-ink);font:800 15px var(--display);display:grid;place-items:center}
.pg-course .ex-dot.fin{background:var(--chip-on);color:var(--on-chip)}
.pg-course .ex-line{flex:1 1 auto;width:2px;min-height:12px;background:var(--border)}
.pg-course .ex-b{flex:1;min-width:0;padding:6px 0 14px}
.pg-course .ex-b>b{font-size:15px}
.pg-course .ex-b>b span{font-weight:500;color:var(--muted)}
.pg-course .ex-docs{display:flex;flex-wrap:wrap;gap:6px;margin-top:8px}
.pg-course .ex-chip{max-width:100%;padding:6px 11px;border-radius:10px;border:1px solid var(--border);background:var(--card-2);color:var(--fg);font-size:13px;font-weight:600;line-height:1.3;text-decoration:none}
.pg-course .ex-chip:hover{border-color:var(--primary-line);color:var(--primary-ink)}
.pg-course .prog{display:flex;align-items:center;gap:16px}
.pg-course .prog-ring{position:relative;flex:none;width:86px;height:86px}
.pg-course .prog-ring span{position:absolute;inset:0;display:grid;place-items:center;font:700 17px var(--display)}
.pg-course .prog b{display:block;font-size:15.5px}
.pg-course .prog span{font-size:14px;color:var(--muted);line-height:1.45}
.pg-course .prog-n{display:flex;gap:8px}
.pg-course .prog-n>div{flex:1;padding:10px 12px;border-radius:14px;background:var(--card-2)}
.pg-course .prog-n span{display:block;font-size:13px;color:var(--muted)}
.pg-course .prog-n b{font:700 20px var(--display)}
.pg-course .tool-row{width:calc(100% + 16px);margin:0 -8px;display:flex;align-items:center;gap:12px;padding:9px 8px;border-radius:14px;text-align:left}
.pg-course .tool-row:hover{background:var(--card-2)}
.pg-course .tool-row>span:nth-child(2){flex:1;display:flex;flex-direction:column}
.pg-course .tool-row b{font-size:15px}
.pg-course .tool-row small{font-size:13.5px;color:var(--muted)}
.pg-course .tool-row>.i{color:var(--faint)}
.pg-course .tr-ic{width:42px;height:42px;border-radius:12px;display:grid;place-items:center;flex:none}
.pg-course .tr-ic.t1{background:var(--pin-bg);color:var(--pin-ink)}
.pg-course .tr-ic.t2{background:var(--td-bg);color:var(--td-ink)}
.pg-course .tr-ic.t3{background:var(--tc-bg);color:var(--tc-ink)}
.pg-course .rel{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:2px}
.pg-course .rel-row{width:calc(100% + 16px);margin:0 -8px;display:flex;align-items:center;gap:12px;padding:8px;border-radius:14px;text-decoration:none;color:inherit}
.pg-course .rel-row:hover{background:var(--card-2)}
.pg-course .rel-row>span:nth-child(2){flex:1;min-width:0;display:flex;flex-direction:column;line-height:1.3}
.pg-course .rel-row b{font-size:15px}
.pg-course .rel-row small{font-size:13.5px;color:var(--muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.pg-course .rel-n{flex:none;font-size:12.5px;font-weight:700;color:var(--muted);background:var(--card-2);padding:3px 9px;border-radius:99px}
.pg-course .cp-link{font-size:14.5px;font-weight:700;color:var(--primary-ink);display:inline-flex;align-items:center;gap:6px;text-decoration:none}
.pg-course .cp-link:hover{text-decoration:underline}
.pg-course .cp-up{background:var(--hero);color:var(--hero-fg);border-radius:22px;padding:22px;display:flex;flex-direction:column;gap:10px}
.pg-course .cp-up h3{font:700 19px var(--display);color:var(--hero-fg)}
.pg-course .cp-up p{font-size:14.5px;line-height:1.5;color:var(--hero-muted)}
@media (max-width:760px){.pg-course .cp-hero{padding:24px 20px;border-radius:22px}
.pg-course .cp-hero-art{order:-1;justify-content:flex-start}
.pg-course .cp-hero-art svg{width:min(220px,70%)}
.pg-course .cp-row{gap:12px;padding:12px}
.pg-course .cp-row .dthumb{display:none}
.pg-course .cp-open span{display:none}
.pg-course .cp-open{padding:0 12px}
.pg-course .cp-docs-h h2{font-size:26px}}
/* ===== 41-docs.scope.py (ported prototype CSS, scoped) ===== */
.pg-docs{--surface:var(--card);--surface-2:var(--card-2);--sunk:color-mix(in srgb,var(--card-2) 70%,var(--border));--line:var(--border);--line-strong:var(--border-2);
--faint:var(--muted);--accent:var(--primary);--accent-strong:var(--primary-ink);--on-accent:var(--on-primary);--accent-soft:var(--primary-soft);--accent-line:var(--primary-line);
--star-soft:var(--pin-bg);--ink:var(--hero-card);--ink-2:var(--hero-card-2);--ink-line:var(--hero-line);--ink-fg:var(--hero-card-fg);--ink-muted:color-mix(in srgb,var(--hero-card-fg) 72%,transparent);--ink-accent:var(--hero-card-label);
--mint-bg:var(--tb-bg);--mint-fg:var(--tb-ink);--gold-bg:var(--pin-bg);--gold-fg:var(--pin-ink);--blue-bg:var(--td-bg);--blue-fg:var(--td-ink);--lav-bg:var(--tc-bg);--lav-fg:var(--tc-ink);
--shadow-sm:var(--sh-1);--shadow-lg:var(--sh-3);--font-display:var(--display);--font-body:var(--body)}
.pg-docs .page-head{margin-bottom:6px}
.pg-docs .page-head h1{outline:none}
.pg-docs .dtabs{display:flex;gap:6px;flex-wrap:wrap;margin:16px 0 18px;padding:4px;border-radius:14px;background:var(--card-2);width:fit-content;max-width:100%}
.pg-docs .dtabs a{display:inline-flex;align-items:center;gap:7px;height:36px;padding:0 14px;border-radius:10px;font-size:13.5px;font-weight:700;color:var(--muted);text-decoration:none;white-space:nowrap}
.pg-docs .dtabs a:hover{color:var(--fg)}
.pg-docs .dtabs a[aria-current="page"]{background:var(--card);color:var(--fg);box-shadow:var(--sh-1)}
.pg-docs .logo{box-shadow:inset 0 -3px 0 var(--c2,transparent)}
.pg-docs .tag{text-decoration:none;color:inherit}
.pg-docs a.tag:hover{border-color:var(--primary-line);color:var(--primary-ink)}
.pg-docs a.ccard,.pg-docs a.dcard,.pg-docs a.nrow,.pg-docs a.jchip,.pg-docs a.way,.pg-docs a.rk-row,.pg-docs a.code-row,.pg-docs a.fd,.pg-docs a.dl-row,.pg-docs a.arow,.pg-docs a.sh-link,.pg-docs a.stretch{text-decoration:none;color:inherit}
.pg-docs .ccard .cg{flex:none}
.pg-docs .subj .cg{flex:none}
.pg-docs .rdot{text-decoration:none}
.pg-docs .view{margin-top:0}
@media (max-width:560px){.pg-docs .dtabs{width:100%}.pg-docs .dtabs a{flex:1;justify-content:center;padding:0 8px}}
.pg-docs *{box-sizing: border-box;}
.pg-docs button, .pg-docs input, .pg-docs select{font: inherit; color: inherit;}
.pg-docs button{cursor: pointer; background: none; border: 0; padding: 0; text-align: inherit;}
.pg-docs button:disabled{cursor: default;}
.pg-docs h1, .pg-docs h2, .pg-docs h3{margin: 0; font-family: var(--font-display); letter-spacing: -0.01em; text-wrap: balance;}
.pg-docs p, .pg-docs ol, .pg-docs ul{margin: 0;}
.pg-docs ol, .pg-docs ul{padding: 0; list-style: none;}
.pg-docs .sr{position: absolute !important; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap;}
.pg-docs svg.i{width: 20px; height: 20px; flex: none; fill: none; stroke: currentColor; stroke-width: 1.9; stroke-linecap: round; stroke-linejoin: round;}
.pg-docs svg.i.sm{width: 16px; height: 16px;}
.pg-docs svg.i.lg{width: 24px; height: 24px;}
.pg-docs svg.i.star-f{fill: currentColor; color: var(--star);}
.pg-docs .muted{color: var(--muted);}
.pg-docs .hint{font-size: 14px; color: var(--muted);}
.pg-docs .field-label{font-size: 13px; font-weight: 700; color: var(--faint);}
.pg-docs .link{color: var(--accent-strong); font-weight: 600; text-decoration: underline; text-underline-offset: 3px; text-decoration-thickness: 1px;}
.pg-docs .link:hover{text-decoration-thickness: 2px;}
.pg-docs .icon-btn{width: 42px; height: 42px; border-radius: 50%; display: grid; place-items: center; color: var(--muted); position: relative; flex: none;}
.pg-docs .icon-btn:hover{background: var(--sunk); color: var(--fg);}
.pg-docs .icon-btn.sm{width: 36px; height: 36px;}
.pg-docs .page-head h1{font-size: clamp(24px, 2.8vw, 30px); font-weight: 700; line-height: 1.15;}
.pg-docs .page-head p{margin-top: 6px; font-size: 16px; color: var(--muted);}
.pg-docs .view{display: flex; flex-direction: column; gap: 48px;}
.pg-docs .card{background: var(--surface); border: 1px solid var(--line); border-radius: 20px;}
.pg-docs .btn{display: inline-flex; align-items: center; justify-content: center; gap: 8px; min-height: 44px; padding: 0 18px; border-radius: 12px; font-size: 15px; font-weight: 600; white-space: nowrap; transition: background-color .15s, border-color .15s, color .15s;}
.pg-docs .btn-primary{background: var(--accent); color: var(--on-accent);}
.pg-docs .btn-primary:hover{background: var(--accent-strong);}
.pg-docs .btn-ghost{background: var(--surface); border: 1px solid var(--line-strong); color: var(--fg);}
.pg-docs .btn-ghost:hover{border-color: var(--accent); color: var(--accent-strong);}
.pg-docs .btn-quiet{color: var(--accent-strong); padding: 0 12px;}
.pg-docs .btn-quiet:hover{background: var(--accent-soft);}
.pg-docs .btn-star{background: var(--star-soft); color: var(--gold-fg);}
.pg-docs .btn.sm{min-height: 36px; padding: 0 12px; font-size: 13px; border-radius: 10px;}
.pg-docs .btn.lg{min-height: 52px; padding: 0 22px; font-size: 16px;}
.pg-docs .seg{display: inline-flex; gap: 3px; padding: 4px; background: var(--sunk); border-radius: 12px; flex-wrap: wrap;}
.pg-docs .seg button{min-height: 38px; padding: 0 14px; border-radius: 9px; font-size: 14px; font-weight: 600; color: var(--muted);}
.pg-docs .seg button:hover{color: var(--fg);}
.pg-docs .seg button[aria-pressed="true"]{background: var(--surface); color: var(--fg); box-shadow: var(--shadow-sm);}
.pg-docs .chips{display: flex; gap: 6px; flex-wrap: wrap;}
.pg-docs .chip{display: inline-flex; align-items: center; gap: 6px; min-height: 36px; padding: 0 14px; border-radius: 999px; border: 1px solid var(--line-strong); background: var(--surface); font-size: 14px; font-weight: 600; color: var(--muted);}
.pg-docs .chip:hover{border-color: var(--accent); color: var(--fg);}
.pg-docs .chip[aria-pressed="true"]{background: var(--fg); color: var(--bg); border-color: var(--fg);}
.pg-docs .logo{--c: #0B7F70; flex: none; width: 48px; height: 48px; border-radius: 12px; display: grid; place-items: center; font: 700 13px/1 var(--font-body); letter-spacing: -0.01em; text-align: center; white-space: nowrap; background: color-mix(in srgb, var(--c) 13%, var(--surface)); color: color-mix(in srgb, var(--c) 45%, var(--fg)); border: 1px solid color-mix(in srgb, var(--c) 24%, var(--surface));}
.pg-docs .logo.long{font-size: 10px;}
.pg-docs .logo.xs{width: 26px; height: 26px; border-radius: 7px; font-size: 9px;}
.pg-docs .logo.sm{width: 38px; height: 38px; border-radius: 10px; font-size: 11px;}
.pg-docs .logo.sm.long{font-size: 8.5px;}
.pg-docs .logo.lg{width: 62px; height: 62px; border-radius: 16px; font-size: 16px;}
.pg-docs .logo.lg.long{font-size: 12px;}
.pg-docs .tone-mint{background: var(--mint-bg); color: var(--mint-fg);}
.pg-docs .tone-gold{background: var(--gold-bg); color: var(--gold-fg);}
.pg-docs .tone-blue{background: var(--blue-bg); color: var(--blue-fg);}
.pg-docs .tone-lav{background: var(--lav-bg); color: var(--lav-fg);}
.pg-docs .cat-ic{width: 34px; height: 34px; border-radius: 10px; display: grid; place-items: center; flex: none;}
.pg-docs .sec{display: flex; flex-direction: column; gap: 18px; scroll-margin-top: 90px;}
.pg-docs .sh{display: flex; align-items: flex-end; justify-content: space-between; gap: 12px 20px; flex-wrap: wrap;}
.pg-docs .sh > div:not(.seg){display: flex; flex-direction: column; gap: 4px; min-width: 0;}
.pg-docs .sh h2{font-size: 24px; font-weight: 700; line-height: 1.2;}
.pg-docs .sh p{font-size: 15px; color: var(--muted);}
.pg-docs .sh-link{display: inline-flex; align-items: center; gap: 6px; min-height: 40px; font-size: 15px; font-weight: 600; color: var(--accent-strong);}
.pg-docs .sh-link:hover{text-decoration: underline; text-underline-offset: 3px;}
.pg-docs .grid2, .pg-docs .grid3, .pg-docs .grid4{display: grid; gap: 16px;}
.pg-docs .grid2{grid-template-columns: repeat(2, minmax(0, 1fr));}
.pg-docs .grid3{grid-template-columns: repeat(3, minmax(0, 1fr));}
.pg-docs .grid4{grid-template-columns: repeat(4, minmax(0, 1fr));}
.pg-docs .center-row{display: flex; justify-content: center; gap: 12px; flex-wrap: wrap;}
.pg-docs .scard{position: relative; display: flex; flex-direction: column; gap: 16px; padding: 20px; background: var(--surface); border: 1px solid var(--line); border-radius: 18px; transition: border-color .15s, box-shadow .15s;}
.pg-docs .scard:hover{border-color: var(--accent-line); box-shadow: var(--shadow-sm);}
.pg-docs .scard.is-mine{border-color: color-mix(in srgb, var(--star) 55%, var(--line));}
.pg-docs .scard-top{display: flex; gap: 14px; align-items: flex-start;}
.pg-docs .scard-id{flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 3px;}
.pg-docs .scard h3{font-size: 18px; line-height: 1.25;}
.pg-docs .stretch{text-align: left; font: inherit; color: var(--fg);}
.pg-docs .stretch::after{content: ""; position: absolute; inset: 0; border-radius: 18px;}
.pg-docs .stretch:focus-visible{outline: none;}
.pg-docs .scard:has(.stretch:focus-visible){outline: 2px solid var(--accent); outline-offset: 2px;}
.pg-docs .meta{font-size: 13px; color: var(--faint);}
.pg-docs .pin{position: relative; z-index: 1; width: 40px; height: 40px; flex: none; border-radius: 12px; display: grid; place-items: center; color: var(--faint); border: 1px solid var(--line); background: var(--surface);}
.pg-docs .pin:hover{color: var(--gold-fg); border-color: color-mix(in srgb, var(--star) 50%, var(--line));}
.pg-docs .pin[aria-pressed="true"]{color: var(--star); background: var(--star-soft); border-color: transparent;}
.pg-docs .pin[aria-pressed="true"] svg{fill: currentColor;}
.pg-docs .counts{display: flex; gap: 18px; font-size: 14px; color: var(--muted);}
.pg-docs .counts b{font-size: 20px; color: var(--fg); font-variant-numeric: tabular-nums;}
.pg-docs .tags{display: flex; gap: 6px; flex-wrap: wrap; align-items: center;}
.pg-docs .tag{font-size: 12px; font-weight: 600; padding: 4px 10px; border: 1px solid var(--line-strong); border-radius: 999px; color: var(--muted);}
.pg-docs .tags .more{font-size: 12px; color: var(--faint);}
.pg-docs .mine-badge{display: inline-flex; align-items: center; gap: 5px; width: fit-content; font-size: 12px; font-weight: 700; color: var(--gold-fg); background: var(--star-soft); padding: 3px 9px; border-radius: 999px;}
.pg-docs .ccard{display: flex; flex-direction: column; gap: 8px; padding: 18px; background: var(--surface); border: 1px solid var(--line); border-radius: 18px; text-align: left; width: 100%; transition: border-color .15s, box-shadow .15s;}
.pg-docs .ccard:hover{border-color: var(--accent-line); box-shadow: var(--shadow-sm);}
.pg-docs .cc-top{display: flex; align-items: center; justify-content: space-between; gap: 10px;}
.pg-docs .code{font-size: 13px; font-weight: 700; color: var(--accent-strong); font-variant-numeric: tabular-nums;}
.pg-docs .ccard .ttl{font: 700 18px/1.25 var(--font-display);}
.pg-docs .cc-school{display: flex; align-items: center; gap: 8px; font-size: 13px; color: var(--muted); min-width: 0;}
.pg-docs .cc-school span:last-child{overflow: hidden; text-overflow: ellipsis; white-space: nowrap;}
.pg-docs .cc-n{margin-top: auto; padding-top: 4px; font-size: 14px; font-weight: 600; font-variant-numeric: tabular-nums;}
.pg-docs .dcard{display: flex; gap: 14px; padding: 16px; background: var(--surface); border: 1px solid var(--line); border-radius: 18px; text-align: left; width: 100%; transition: border-color .15s;}
.pg-docs .dcard:hover{border-color: var(--accent-line);}
.pg-docs .thumb{position: relative; flex: none; width: 50px; height: 64px; border-radius: 8px; padding: 18px 8px 0; display: flex; flex-direction: column; gap: 5px; border: 1px solid var(--line); background: var(--surface); overflow: hidden;}
.pg-docs .thumb::before{content: ""; position: absolute; left: 0; right: 0; top: 0; height: 11px; background: currentColor;}
.pg-docs .thumb i{display: block; height: 3px; border-radius: 2px; background: var(--line-strong);}
.pg-docs .thumb i:nth-child(2){width: 82%;}
.pg-docs .thumb i:nth-child(3){width: 92%;}
.pg-docs .thumb i:nth-child(4){width: 60%;}
.pg-docs .thumb.sm{width: 40px; height: 50px; padding-top: 14px; gap: 4px;}
.pg-docs .thumb.sm::before{height: 8px;}
.pg-docs .thumb.xs{width: 32px; height: 40px; padding: 11px 5px 0; gap: 3px;}
.pg-docs .thumb.xs::before{height: 6px;}
.pg-docs .thumb.xs i{height: 2px;}
.pg-docs .t-mint{color: var(--mint-fg);}
.pg-docs .t-gold{color: var(--gold-fg);}
.pg-docs .t-blue{color: var(--blue-fg);}
.pg-docs .t-lav{color: var(--lav-fg);}
.pg-docs .dt{flex: 1; display: flex; flex-direction: column; gap: 4px; min-width: 0;}
.pg-docs .dt b{font-size: 15px; font-weight: 600; line-height: 1.35;}
.pg-docs .dt .meta{font-size: 13px; color: var(--muted);}
.pg-docs .rate{display: inline-flex; align-items: center; gap: 5px; font-size: 13px; font-weight: 600; color: var(--accent-strong);}
.pg-docs .omni{position: relative; display: flex; align-items: center; gap: 10px; background: var(--surface); border: 1.5px solid var(--line-strong); border-radius: 999px; padding: 0 6px 0 16px; min-height: 46px; color: var(--muted);}
.pg-docs .omni:focus-within{border-color: var(--accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 18%, transparent);}
.pg-docs .omni input{flex: 1; min-width: 0; border: 0; outline: none; background: transparent; color: var(--fg); font-size: 15px; padding: 10px 0;}
.pg-docs .omni input::placeholder{color: var(--faint);}
.pg-docs .omni-hero{border-radius: 16px; border-width: 2px; min-height: 64px; padding: 6px 6px 6px 20px;}
.pg-docs .omni-hero input{font-size: 18px;}
.pg-docs .omni-hero > svg.i{width: 22px; height: 22px;}
.pg-docs .omni-go{min-height: 50px; padding: 0 24px; border-radius: 11px;}
.pg-docs .omni-clear{width: 38px; height: 38px; flex: none; border-radius: 50%; display: grid; place-items: center; color: var(--faint);}
.pg-docs .omni-clear:hover{background: var(--sunk); color: var(--fg);}
.pg-docs .omni-sm{border-radius: 12px; min-height: 46px; padding-right: 12px;}
.pg-docs .omni-panel{position: absolute; left: -2px; right: -2px; top: calc(100% + 8px); z-index: 45; background: var(--surface); color: var(--fg); border: 1px solid var(--line); border-radius: 16px; box-shadow: var(--shadow-lg); padding: 8px; max-height: min(66vh, 540px); overflow-y: auto; text-align: left;}
.pg-docs .og + .og{margin-top: 6px; padding-top: 6px; border-top: 1px solid var(--line);}
.pg-docs .og-h{font-size: 12px; font-weight: 700; color: var(--faint); padding: 6px 10px 4px;}
.pg-docs .opt{display: flex; align-items: center; gap: 12px; min-height: 52px; padding: 7px 10px; border-radius: 10px; cursor: pointer;}
.pg-docs .opt[aria-selected="true"]{background: var(--accent-soft);}
.pg-docs .ot{flex: 1; min-width: 0; display: flex; flex-direction: column;}
.pg-docs .ot b{font-size: 15px; font-weight: 600; line-height: 1.3;}
.pg-docs .ot > span{font-size: 13px; color: var(--faint); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;}
.pg-docs .oi{width: 38px; height: 38px; flex: none; border-radius: 10px; display: grid; place-items: center; background: var(--sunk); color: var(--muted);}
.pg-docs .why{flex: none; max-width: 46%; overflow: hidden; text-overflow: ellipsis; font-size: 12px; font-weight: 600; padding: 3px 9px; border-radius: 999px; background: var(--sunk); color: var(--muted); white-space: nowrap;}
.pg-docs .why.near{background: var(--accent-soft); color: var(--accent-strong);}
.pg-docs .why.mine{background: var(--star-soft); color: var(--gold-fg);}
.pg-docs .opt[aria-selected="true"] .why{background: var(--surface);}
.pg-docs mark{background: var(--star-soft); color: inherit; border-radius: 3px; padding: 0 1px;}
.pg-docs .og-empty, .pg-docs .og-note{padding: 12px 10px; color: var(--muted); font-size: 14px;}
.pg-docs .og-note{padding-top: 2px; font-size: 13px;}
.pg-docs .og-foot .oi{background: transparent; color: var(--accent-strong);}
.pg-docs .og-foot .ot b{font-size: 14px; color: var(--accent-strong);}
.pg-docs .a-hero{display: flex; flex-wrap: wrap; gap: 24px; padding: clamp(20px, 3vw, 32px);}
.pg-docs .a-main{flex: 3 1 460px; min-width: 0; display: flex; flex-direction: column; gap: 16px;}
.pg-docs .a-title h2{font-size: clamp(26px, 3vw, 34px); font-weight: 700; line-height: 1.1;}
.pg-docs .a-title p{margin-top: 6px; font-size: 16px; color: var(--muted);}
.pg-docs .jump{display: flex; align-items: center; gap: 8px; flex-wrap: wrap;}
.pg-docs .jump-l{font-size: 13px; font-weight: 600; color: var(--faint); margin-right: 2px;}
.pg-docs .jchip{display: inline-flex; align-items: center; gap: 8px; min-height: 40px; padding: 4px 12px 4px 6px; border-radius: 999px; border: 1px solid var(--line); background: var(--surface-2); font-size: 14px; font-weight: 600;}
.pg-docs .jchip:hover{border-color: var(--accent);}
.pg-docs .jchip.is-mine{border-color: color-mix(in srgb, var(--star) 50%, var(--line)); background: var(--star-soft);}
.pg-docs .try{display: flex; align-items: center; gap: 8px; flex-wrap: wrap;}
.pg-docs .try-l{font-size: 13px; font-weight: 600; color: var(--faint);}
.pg-docs .try-chip{display: inline-flex; align-items: center; gap: 6px; min-height: 36px; padding: 0 12px; border-radius: 999px; background: var(--sunk); font-size: 13px; font-weight: 600; color: var(--fg);}
.pg-docs .try-chip:hover{background: var(--accent-soft); color: var(--accent-strong);}
.pg-docs .try-chip small{font-size: 12px; font-weight: 500; color: var(--faint);}
.pg-docs .a-side{flex: 2 1 300px; min-width: 0; display: flex; flex-direction: column; gap: 10px; padding: 20px; border-radius: 16px; background: var(--surface-2); border: 1px solid var(--line);}
.pg-docs .a-side-h{display: flex; align-items: center; justify-content: space-between; gap: 8px; flex-wrap: wrap;}
.pg-docs .a-side-h h3{font-size: 18px;}
.pg-docs .loc-sel{position: relative; display: inline-flex; align-items: center; gap: 6px; color: var(--accent-strong);}
.pg-docs .loc-sel select{appearance: none; -webkit-appearance: none; border: 0; background: transparent; font-size: 14px; font-weight: 600; color: var(--accent-strong); padding: 8px 20px 8px 0; cursor: pointer; min-height: 40px;}
.pg-docs .loc-sel .chev-d{position: absolute; right: 0; pointer-events: none;}
.pg-docs .nrow{display: flex; align-items: center; gap: 12px; min-height: 56px; padding: 8px 10px; border-radius: 12px; width: 100%; text-align: left;}
.pg-docs .nrow:hover{background: var(--sunk);}
.pg-docs .nt{flex: 1; min-width: 0; display: flex; flex-direction: column;}
.pg-docs .nt b{font-size: 14px; font-weight: 600; line-height: 1.3;}
.pg-docs .nt span{font-size: 12.5px; color: var(--faint);}
.pg-docs .nd{font-size: 13px; font-weight: 600; color: var(--muted); font-variant-numeric: tabular-nums; white-space: nowrap;}
.pg-docs .near-rows{display: flex; flex-direction: column; margin: 0 -10px;}
.pg-docs .stats-line{margin-top: auto; padding-top: 12px; border-top: 1px solid var(--line); font-size: 13px; color: var(--muted);}
.pg-docs .stats-line b{color: var(--fg);}
.pg-docs .subjects{display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 10px;}
.pg-docs .subj{display: flex; flex-direction: column; justify-content: space-between; gap: 14px; min-height: 104px; padding: 14px; border-radius: 16px; text-align: left; font-size: 14px; font-weight: 700; line-height: 1.25;}
.pg-docs .subj .cat-ic{background: var(--surface);}
.pg-docs .subj span:last-child{color: var(--fg);}
.pg-docs .subj:hover{box-shadow: inset 0 0 0 1.5px currentColor;}
.pg-docs .subj[aria-pressed="true"]{box-shadow: inset 0 0 0 2px var(--fg);}
.pg-docs .filter-on{display: inline-flex; align-items: center; gap: 8px; width: fit-content; min-height: 36px; padding: 0 8px 0 14px; border-radius: 999px; background: var(--fg); color: var(--bg); font-size: 13px; font-weight: 600;}
.pg-docs .filter-on svg{opacity: .8;}
.pg-docs .empty{padding: 24px; border: 1.5px dashed var(--line-strong); border-radius: 16px; color: var(--muted); font-size: 15px; display: flex; flex-direction: column; gap: 10px; align-items: flex-start;}
.pg-docs .empty b{color: var(--fg);}
.pg-docs .a-ways{margin-top: auto; padding-top: 16px; border-top: 1px solid var(--line); display: flex; flex-wrap: wrap; align-items: center; gap: 8px 10px;}
.pg-docs .a-ways .try-l{margin-right: 2px;}
.pg-docs .way{display: inline-flex; align-items: center; gap: 8px; min-height: 40px; padding: 0 14px; border-radius: 12px; border: 1px solid var(--line); background: var(--surface-2); font-size: 14px; font-weight: 600; color: var(--fg);}
.pg-docs .way svg{color: var(--accent-strong);}
.pg-docs .way:hover{border-color: var(--accent);}
.pg-docs .quick{display: flex; flex-direction: column; gap: 8px; padding-top: 16px; border-top: 1px solid var(--line);}
.pg-docs .quick .chips .chip{padding-left: 6px; color: var(--fg);}
.pg-docs .b-preview{display: flex; align-items: center; justify-content: flex-end; gap: 10px; flex-wrap: wrap; margin-bottom: -32px; font-size: 14px; color: var(--muted);}
.pg-docs .b-band{display: flex; flex-wrap: wrap; gap: 20px; align-items: stretch; scroll-margin-top: 90px;}
.pg-docs .b-left{flex: 3 1 480px; min-width: 0; display: flex;}
.pg-docs .b-left > *{flex: 1; min-width: 0;}
.pg-docs .b-right{flex: 2 1 300px; min-width: 0; display: flex;}
.pg-docs .b-right > *{flex: 1; min-width: 0;}
.pg-docs .hoot-card{padding: clamp(20px, 3vw, 32px); display: flex; flex-direction: column; gap: 22px;}
.pg-docs .hc-top{display: flex; align-items: center; justify-content: space-between; gap: 12px; font-size: 13px; color: var(--faint); font-weight: 600;}
.pg-docs .dots{display: inline-flex; gap: 5px; align-items: center;}
.pg-docs .dots i{width: 22px; height: 6px; border-radius: 3px; background: var(--line-strong);}
.pg-docs .dots i.on{background: var(--accent);}
.pg-docs .say{display: flex; gap: 16px; align-items: flex-start;}
.pg-docs .hoot-av{flex: none; width: 76px; height: 76px; border-radius: 24px; background: var(--star-soft); display: grid; place-items: center;}
.pg-docs .hoot-av svg{width: 62px; height: 62px;}
.pg-docs .bubble{flex: 1; min-width: 0; padding: 14px 18px; border-radius: 4px 18px 18px 18px; background: var(--surface-2); border: 1px solid var(--line); font: 600 clamp(19px, 2.2vw, 22px)/1.3 var(--font-display);}
.pg-docs .bubble small{display: block; margin-top: 6px; font: 400 15px/1.45 var(--font-body); color: var(--muted);}
.pg-docs .choice-row{display: flex; gap: 10px; flex-wrap: wrap;}
.pg-docs .choice-grid{display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px;}
.pg-docs .choice{display: flex; align-items: center; gap: 14px; min-height: 76px; padding: 14px 16px; border-radius: 16px; border: 1.5px solid var(--line); background: var(--surface); text-align: left;}
.pg-docs .choice:hover, .pg-docs .pick:hover{border-color: var(--accent); background: var(--surface-2);}
.pg-docs .ct{display: flex; flex-direction: column; min-width: 0;}
.pg-docs .ct b{font-size: 16px;}
.pg-docs .ct span{font-size: 13px; color: var(--muted);}
.pg-docs .pick-list{display: flex; flex-direction: column; gap: 8px;}
.pg-docs .pick{display: flex; align-items: center; gap: 14px; min-height: 70px; padding: 10px 14px; border-radius: 16px; border: 1.5px solid var(--line); background: var(--surface); width: 100%; text-align: left;}
.pg-docs .pt{flex: 1; min-width: 0; display: flex; flex-direction: column;}
.pg-docs .pt b{font-size: 16px; font-weight: 600; line-height: 1.3;}
.pg-docs .pt span{font-size: 13px; color: var(--muted);}
.pg-docs .pd{font-size: 14px; font-weight: 700; color: var(--accent-strong); white-space: nowrap; font-variant-numeric: tabular-nums;}
.pg-docs .loc-box{display: flex; flex-direction: column; gap: 10px; padding: 16px; border-radius: 16px; background: var(--surface-2); border: 1px solid var(--line);}
.pg-docs .hc-foot{display: flex; gap: 8px 20px; flex-wrap: wrap; align-items: center; margin-top: auto; padding-top: 4px; font-size: 14px;}
.pg-docs .pass{overflow: hidden; display: flex; flex-direction: column;}
.pg-docs .pass-band{height: 8px; background: var(--c); flex: none;}
.pg-docs .pass-head{display: flex; gap: 16px; align-items: center; flex-wrap: wrap; padding: 22px clamp(20px, 3vw, 28px) 0;}
.pg-docs .pass-id{flex: 1 1 240px; min-width: 0;}
.pg-docs .pass-k{display: inline-flex; align-items: center; gap: 6px; font-size: 13px; font-weight: 700; color: var(--gold-fg);}
.pg-docs .pass-id h2{font-size: clamp(22px, 2.6vw, 28px); line-height: 1.15; margin: 2px 0 4px;}
.pg-docs .pass-acts{flex-basis: 100%; display: flex; gap: 8px; flex-wrap: wrap;}
.pg-docs .pass-body{display: flex; flex-direction: column; gap: 16px; padding: 20px clamp(20px, 3vw, 28px) 24px;}
.pg-docs .switch-box{margin: 16px clamp(20px, 3vw, 28px) 0; padding: 12px; border-radius: 16px; background: var(--surface-2); border: 1px solid var(--line); display: flex; flex-direction: column; gap: 4px; align-items: stretch;}
.pg-docs .switch-box .btn{align-self: flex-start; margin-top: 6px;}
.pg-docs .mc-wrap{display: flex; flex-direction: column; gap: 8px;}
.pg-docs .mycourses{display: flex; gap: 8px; flex-wrap: wrap;}
.pg-docs .mc{display: inline-flex; flex-direction: column; align-items: flex-start; justify-content: center; gap: 1px; min-height: 58px; padding: 8px 14px; border-radius: 12px; border: 1px solid var(--line); background: var(--surface-2); text-align: left;}
.pg-docs .mc b{font-size: 14px;}
.pg-docs .mc span{font-size: 12px; color: var(--faint);}
.pg-docs .mc:hover{border-color: var(--accent);}
.pg-docs .mc-add{flex-direction: row; align-items: center; gap: 6px; border-style: dashed; color: var(--accent-strong); font-weight: 600; font-size: 14px; background: transparent;}
.pg-docs .pass-new{display: flex; align-items: center; gap: 8px; font-size: 14px; color: var(--muted);}
.pg-docs .pass-new .pulse{width: 8px; height: 8px; border-radius: 50%; background: var(--accent);}
.pg-docs .saved-flash{animation: dh-flash 1.4s ease-out 1;}
@keyframes dh-flash{ from { box-shadow: 0 0 0 5px color-mix(in srgb, var(--star) 50%, transparent); } to { box-shadow: 0 0 0 0 transparent; } }
.pg-docs .ink-card{background: var(--ink); color: var(--ink-fg); border-radius: 20px; padding: clamp(20px, 3vw, 28px); display: flex; flex-direction: column; gap: 16px;}
.pg-docs .ink-card h2{font-size: 22px;}
.pg-docs .ink-card p{color: var(--ink-muted); font-size: 14px;}
.pg-docs .types{display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px;}
.pg-docs .types div{display: flex; flex-direction: column; gap: 6px; padding: 14px; border-radius: 14px; background: var(--ink-2);}
.pg-docs .types svg{color: var(--ink-accent);}
.pg-docs .types b{font-size: 15px;}
.pg-docs .types span{font-size: 13px; color: var(--ink-muted); line-height: 1.4;}
.pg-docs .recent-card{padding: clamp(20px, 3vw, 28px); display: flex; flex-direction: column; gap: 8px;}
.pg-docs .recent-card h2{font-size: 22px; margin-bottom: 6px;}
.pg-docs .rrow{display: flex; align-items: center; gap: 14px; padding: 10px; border-radius: 12px; width: 100%; text-align: left;}
.pg-docs .rrow:hover{background: var(--sunk);}
.pg-docs .rrow .dt b{font-size: 14px;}
.pg-docs .rrow .go{font-size: 13px; font-weight: 700; color: var(--accent-strong);}
.pg-docs .recent-card > .link{margin-top: auto; padding-top: 8px; width: fit-content;}
.pg-docs .atlas{overflow: hidden; scroll-margin-top: 90px;}
.pg-docs .atlas-top{display: flex; align-items: flex-end; justify-content: space-between; gap: 16px; flex-wrap: wrap; padding: clamp(20px, 3vw, 28px) clamp(20px, 3vw, 28px) 18px;}
.pg-docs .atlas-top h2{font-size: clamp(24px, 2.6vw, 30px);}
.pg-docs .atlas-top p{color: var(--muted); margin-top: 4px;}
.pg-docs .atlas-tools{display: flex; gap: 10px; flex-wrap: wrap; align-items: center;}
.pg-docs .acrumbs{display: flex; align-items: center; gap: 2px; flex-wrap: wrap; min-height: 48px; padding: 6px clamp(14px, 2.6vw, 22px); border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); background: var(--surface-2); font-size: 14px;}
.pg-docs .acrumbs button{min-height: 36px; padding: 0 8px; border-radius: 8px; color: var(--accent-strong); font-weight: 600;}
.pg-docs .acrumbs button:hover{background: var(--accent-soft);}
.pg-docs .acrumbs .here{padding: 0 8px; font-weight: 700;}
.pg-docs .acrumbs svg{color: var(--faint);}
.pg-docs .acols{display: grid; grid-template-columns: minmax(0, .9fr) minmax(0, .9fr) minmax(0, .9fr) minmax(0, 1.5fr); min-height: 440px;}
.pg-docs .acol-list > *, .pg-docs .d-list > li{flex-shrink: 0;}
.pg-docs .acol{display: flex; flex-direction: column; min-width: 0; border-right: 1px solid var(--line);}
.pg-docs .acol:last-child{border-right: 0;}
.pg-docs .acol-h{display: flex; align-items: center; gap: 6px; min-height: 48px; padding: 6px 16px; font-size: 13px; font-weight: 700; color: var(--faint); border-bottom: 1px solid var(--line);}
.pg-docs .acol-back{display: none; margin-left: -8px;}
.pg-docs .acol-list{display: flex; flex-direction: column; gap: 2px; padding: 8px; overflow-y: auto; max-height: 470px;}
.pg-docs .arow{display: flex; align-items: center; gap: 10px; min-height: 50px; padding: 8px 10px; border-radius: 10px; width: 100%; text-align: left; font-size: 15px; font-weight: 600;}
.pg-docs .arow:hover{background: var(--sunk);}
.pg-docs .arow[aria-current="true"]{background: var(--accent-soft); color: var(--accent-strong);}
.pg-docs .an{flex: 1; min-width: 0; display: flex; flex-direction: column;}
.pg-docs .an small{font-size: 12px; font-weight: 500; color: var(--faint);}
.pg-docs .arow[aria-current="true"] .an small{color: var(--accent-strong);}
.pg-docs .arow .chev{color: var(--faint);}
.pg-docs .arow-more{color: var(--muted); font-weight: 500; font-size: 14px;}
.pg-docs .arow-s .an b{font-size: 14px; line-height: 1.3;}
.pg-docs .cc{flex: none; display: grid; place-items: center; width: 32px; height: 24px; border-radius: 6px; background: var(--sunk); font-size: 11px; font-weight: 700; color: var(--muted);}
.pg-docs .acol-empty{display: grid; place-items: center; padding: 24px; text-align: center; color: var(--faint); font-size: 14px; background: var(--surface-2);}
.pg-docs .az-tools{padding: 16px clamp(16px, 3vw, 28px) 0;}
.pg-docs .az-rail{display: flex; flex-wrap: wrap; gap: 4px; padding: 12px clamp(16px, 3vw, 28px); border-bottom: 1px solid var(--line);}
.pg-docs .az-rail button{width: 40px; height: 40px; border-radius: 10px; font-weight: 700; font-size: 14px; color: var(--accent-strong);}
.pg-docs .az-rail button:hover:not(:disabled){background: var(--accent-soft);}
.pg-docs .az-rail button:disabled{color: var(--line-strong);}
.pg-docs .az-list{position: relative; max-height: 470px; overflow-y: auto; padding: 0 clamp(8px, 2vw, 18px) 12px;}
.pg-docs .az-l{position: sticky; top: 0; z-index: 1; background: var(--surface); padding: 12px 10px 6px; font-size: 17px; color: var(--accent-strong); border-bottom: 1px solid var(--line); margin-bottom: 4px;}
.pg-docs .az-rows{display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 2px 12px;}
.pg-docs .atlas-foot{padding: 12px clamp(20px, 3vw, 28px); border-top: 1px solid var(--line); font-size: 13px; color: var(--faint);}
.pg-docs .kbd{display: inline-block; min-width: 22px; padding: 0 5px; border: 1px solid var(--line-strong); border-bottom-width: 2px; border-radius: 5px; font-size: 12px; font-weight: 700; text-align: center; color: var(--muted);}
.pg-docs .c-lower{display: flex; flex-wrap: wrap; gap: 20px;}
.pg-docs .c-recent{flex: 3 1 420px; min-width: 0; padding: clamp(20px, 3vw, 28px); display: flex; flex-direction: column; gap: 12px;}
.pg-docs .c-recent h2, .pg-docs .ranked h2{font-size: 22px;}
.pg-docs .c-req{flex: 2 1 300px; min-width: 0;}
.pg-docs .rec-grid{display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 4px 12px; margin: 0 -10px;}
.pg-docs .field{display: flex; flex-direction: column; gap: 6px;}
.pg-docs .field label{font-size: 13px; font-weight: 600;}
.pg-docs .ink-card input{min-height: 44px; border: 1px solid var(--ink-line); border-radius: 10px; padding: 0 12px; background: var(--ink-2); color: var(--ink-fg);}
.pg-docs .ink-card input::placeholder{color: var(--ink-muted);}
.pg-docs .ink-card .btn-primary{background: var(--ink-accent); color: #0F2134;}
.pg-docs .ink-card .btn-primary:hover{background: #A6E8D8;}
.pg-docs .ok-msg{display: flex; gap: 10px; align-items: flex-start; font-size: 14px; font-weight: 600; color: var(--ink-accent);}
.pg-docs .ranked{padding: clamp(20px, 3vw, 28px); display: flex; flex-direction: column; gap: 10px;}
.pg-docs .ranked li + li{border-top: 1px solid var(--line);}
.pg-docs .rk-row{display: flex; align-items: center; gap: 14px; padding: 12px 4px; width: 100%; text-align: left;}
.pg-docs .rk-row:hover .rk-t b{color: var(--accent-strong);}
.pg-docs .rk-n{flex: none; width: 26px; font: 700 20px/1 var(--font-display); color: var(--faint); font-variant-numeric: tabular-nums;}
.pg-docs .rk-t{flex: 1; min-width: 0; display: flex; flex-direction: column;}
.pg-docs .rk-t b{font-size: 15px;}
.pg-docs .rk-t span{font-size: 13px; color: var(--muted);}
.pg-docs .rk-d{font-size: 14px; font-weight: 600; white-space: nowrap; font-variant-numeric: tabular-nums;}
.pg-docs .d-band{display: flex; flex-wrap: wrap; gap: 20px; align-items: stretch;}
.pg-docs .radar-card{flex: 3 1 460px; min-width: 0; padding: clamp(18px, 3vw, 28px); display: flex; flex-direction: column; gap: 14px; overflow: hidden; scroll-margin-top: 90px;}
.pg-docs .radar-h{display: flex; justify-content: space-between; align-items: flex-end; gap: 12px; flex-wrap: wrap;}
.pg-docs .radar-h h2{font-size: clamp(24px, 2.6vw, 30px);}
.pg-docs .loc-line{display: flex; align-items: center; gap: 4px 12px; flex-wrap: wrap; margin-top: 4px; font-size: 14px; color: var(--muted);}
.pg-docs .radar-wrap{display: flex; justify-content: center; padding-top: 8px;}
.pg-docs .radar{position: relative; width: 100%; max-width: 560px; aspect-ratio: 1; container-type: inline-size;}
.pg-docs .r-disc{position: absolute; inset: 4%; border-radius: 50%; background: color-mix(in srgb, var(--accent-soft) 60%, var(--surface)); border: 1px solid var(--accent-line);}
.pg-docs .r-ring{position: absolute; left: 50%; top: 50%; border-radius: 50%; border: 1px dashed var(--accent-line); transform: translate(-50%, -50%); pointer-events: none;}
.pg-docs .r-ring b{position: absolute; top: 0; left: 50%; transform: translate(-50%, -50%); padding: 1px 8px; border-radius: 999px; background: var(--surface); border: 1px solid var(--accent-line); font-size: 11px; font-weight: 600; color: var(--muted); white-space: nowrap;}
.pg-docs .r-axis{position: absolute; left: 50%; top: 4%; bottom: 4%; width: 1px; background: color-mix(in srgb, var(--accent-line) 60%, transparent); pointer-events: none;}
.pg-docs .r-axis.h{left: 4%; right: 4%; top: 50%; bottom: auto; width: auto; height: 1px;}
.pg-docs .r-sweep{position: absolute; inset: 4%; border-radius: 50%; background: conic-gradient(from 0deg, transparent 0deg, color-mix(in srgb, var(--accent) 22%, transparent) 44deg, transparent 45deg); opacity: 0; pointer-events: none;}
.pg-docs .radar.sweep .r-sweep{animation: dh-sweep 1.7s cubic-bezier(.3, .6, .4, 1) 1;}
@keyframes dh-sweep{ 0% { transform: rotate(0deg); opacity: 1; } 80% { opacity: .7; } 100% { transform: rotate(360deg); opacity: 0; } }
.pg-docs .r-you{position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); display: grid; place-items: center; pointer-events: none;}
.pg-docs .r-you span{width: 16px; height: 16px; border-radius: 50%; background: var(--accent); border: 3px solid var(--surface); animation: dh-you 2.6s ease-out infinite;}
.pg-docs .r-you b{position: absolute; top: 18px; font-size: 11px; font-weight: 700; color: var(--accent-strong);}
@keyframes dh-you{ 0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--accent) 45%, transparent); } 100% { box-shadow: 0 0 0 18px transparent; } }
.pg-docs .rdot{position: absolute; transform: translate(-50%, -50%); height: calc(var(--k) * 1cqw); min-width: calc(var(--k) * 1cqw); padding: 0 1cqw; border-radius: 999px; display: grid; place-items: center; font-size: clamp(8.5px, 2cqw, 12px); font-weight: 700; white-space: nowrap; background: color-mix(in srgb, var(--c) 16%, var(--surface)); color: color-mix(in srgb, var(--c) 45%, var(--fg)); border: 1.5px solid color-mix(in srgb, var(--c) 40%, var(--surface)); box-shadow: var(--shadow-sm); transition: transform .15s; z-index: 1;}
.pg-docs .rdot:hover, .pg-docs .rdot.is-hot, .pg-docs .rdot:focus-visible{transform: translate(-50%, -50%) scale(1.14); z-index: 3; border-color: var(--accent);}
.pg-docs .rdot.is-mine{border-color: var(--star); box-shadow: 0 0 0 3px color-mix(in srgb, var(--star) 35%, transparent);}
.pg-docs .rdot::after{content: attr(data-tip); position: absolute; bottom: calc(100% + 8px); left: 50%; transform: translateX(-50%); padding: 5px 10px; border-radius: 8px; background: var(--fg); color: var(--bg); font-size: 12px; font-weight: 600; white-space: nowrap; pointer-events: none; opacity: 0; transition: opacity .12s;}
.pg-docs .rdot:hover::after, .pg-docs .rdot.is-hot::after, .pg-docs .rdot:focus-visible::after{opacity: 1;}
.pg-docs .r-msg{position: absolute; left: 50%; top: 50%; transform: translate(-50%, 28px); width: 64%; text-align: center; font-size: 14px; color: var(--muted);}
.pg-docs .radar-note{font-size: 13px; color: var(--faint); text-align: center;}
.pg-docs .d-list-card{flex: 2 1 320px; min-width: 0; padding: clamp(18px, 3vw, 24px); display: flex; flex-direction: column; gap: 12px;}
.pg-docs .d-list-card h3{font-size: 18px;}
.pg-docs .d-list{display: flex; flex-direction: column; gap: 2px; margin: 0 -8px; padding: 0 8px;}
@media (min-width: 1024px){.pg-docs .d-list{overflow-y: auto; max-height: 540px;}}
.pg-docs .dl-row{display: flex; align-items: center; gap: 12px; min-height: 62px; padding: 8px 10px; border-radius: 12px; width: 100%; text-align: left;}
.pg-docs .dl-row:hover, .pg-docs .dl-row.is-hot{background: var(--sunk);}
.pg-docs .dl-d{flex: none; width: 46px; font: 700 17px/1 var(--font-display); color: var(--accent-strong); font-variant-numeric: tabular-nums; text-align: right;}
.pg-docs .dl-d small{display: block; font: 600 11px/1.5 var(--font-body); color: var(--faint);}
.pg-docs .d-code{padding: clamp(20px, 3vw, 28px); display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.2fr); gap: 16px 28px; align-items: center;}
.pg-docs .d-code h2{font-size: 22px;}
.pg-docs .d-code > div > p{color: var(--muted); margin-top: 4px;}
.pg-docs .d-code-res{grid-column: 1 / -1; display: flex; flex-direction: column; gap: 10px;}
.pg-docs .res-h{font-size: 13px; font-weight: 700; color: var(--faint);}
.pg-docs .code-grid{display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px;}
.pg-docs .code-row{display: flex; align-items: center; gap: 12px; padding: 10px 12px; border: 1px solid var(--line); border-radius: 14px; width: 100%; text-align: left; background: var(--surface-2);}
.pg-docs .code-row:hover{border-color: var(--accent);}
.pg-docs .d-lower{display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.25fr); gap: 32px; align-items: start;}
.pg-docs .feed li + li{border-top: 1px solid var(--line);}
.pg-docs .fd{display: flex; gap: 12px; align-items: flex-start; padding: 12px 4px; width: 100%; text-align: left;}
.pg-docs .fd:hover .dt b{color: var(--accent-strong);}
.pg-docs .fd-time{flex: none; width: 82px; font-size: 12px; font-weight: 600; color: var(--faint); padding-top: 3px;}
.pg-docs .fd .dt b{font-size: 14px;}
@media (max-width: 1180px){.pg-docs .grid3, .pg-docs .grid4{grid-template-columns: repeat(2, minmax(0, 1fr));}}
@media (min-width: 761px) and (max-width: 1180px){.pg-docs .acols{grid-template-columns: minmax(0, 1fr) minmax(0, 1.4fr);}
.pg-docs .acol{display: none;}
.pg-docs .acol.prev, .pg-docs .acol.cur, .pg-docs .acol.next{display: flex;}
.pg-docs .acol-back{display: grid;}
.pg-docs .acol.prev + .acol.cur .acol-back{display: none;}}
@media (max-width: 1023px){.pg-docs .d-lower{grid-template-columns: minmax(0, 1fr);}}
@media (max-width: 900px){.pg-docs .d-code{grid-template-columns: minmax(0, 1fr);}
.pg-docs .code-grid{grid-template-columns: minmax(0, 1fr);}}
@media (max-width: 760px){.pg-docs .acols{grid-template-columns: minmax(0, 1fr); min-height: 0;}
.pg-docs .acol{display: none; border-right: 0;}
.pg-docs .acol.cur{display: flex;}
.pg-docs .acol-empty{display: none !important;}
.pg-docs .acol-back{display: grid;}
.pg-docs .az-rows, .pg-docs .rec-grid{grid-template-columns: minmax(0, 1fr);}
.pg-docs .subjects{grid-template-columns: repeat(2, minmax(0, 1fr));}
.pg-docs .b-preview{justify-content: flex-start; margin-bottom: -28px;}}
@media (max-width: 640px){.pg-docs .grid2, .pg-docs .grid3, .pg-docs .grid4{grid-template-columns: minmax(0, 1fr);}
.pg-docs .choice-grid{grid-template-columns: minmax(0, 1fr);}
.pg-docs .omni-hero{padding-left: 14px; min-height: 58px;}
.pg-docs .omni-hero input{font-size: 16px;}
.pg-docs .omni-go{padding: 0 16px; min-height: 46px;}
.pg-docs .view{gap: 40px;}
.pg-docs .why{display: none;}
.pg-docs .fd-time{width: 64px;}}
@media (prefers-reduced-motion: reduce){.pg-docs *, .pg-docs *::before, .pg-docs *::after{animation: none !important; transition: none !important; scroll-behavior: auto !important;}}
/* ===== 42-campus.scope.py (ported prototype CSS, scoped) ===== */
.pg-campus{--surface:var(--card);--surface-2:var(--card-2);--surface-3:color-mix(in srgb,var(--card-2) 65%,var(--border));--line:var(--border);--line-soft:color-mix(in srgb,var(--border) 55%,transparent);
--ink:var(--fg);--ink-2:var(--muted);--ink-3:var(--faint);--brand:var(--primary);--brand-ink:var(--primary-ink);--brand-tint:var(--primary-soft);--brand-mint:var(--primary-soft);--on-brand:var(--on-primary);
--paper:#ffffff;--paper-line:#dde4e3;--paper-ink:#a3b3b1;--shadow-1:var(--sh-1);--shadow-2:var(--sh-2);--f-display:var(--display);--f-body:var(--body);
--sc-ink:var(--sc-ink-l);--sc-tint:var(--sc-tint-l);--sc-wash:var(--sc-wash-l);--r-sm:9px;--r:13px;--r-lg:18px}
:root.dk .pg-campus{--sc-ink:var(--sc-ink-d);--sc-tint:var(--sc-tint-d);--sc-wash:var(--sc-wash-d);--paper:#1b2629;--paper-line:#2c3f43;--paper-ink:#55696c}
.pg-campus .page-in{display:flex;flex-direction:column;gap:18px}
.pg-campus .page-in>.crumbs{margin-bottom:-4px}
.pg-campus .sec-h{margin:8px 0 -6px}
.pg-campus .a-grid{margin-top:4px}
.pg-campus .ch-mine .panel-h{margin-bottom:12px}
.pg-campus a.go,.pg-campus a.link{text-decoration:none;color:inherit}
.pg-campus a.link{color:var(--primary-ink);font-weight:650}
.pg-campus .hero-a h1{outline:none}
.pg-campus .star .i{width:17px;height:17px}
.pg-campus .star.on .i{fill:currentColor}
.pg-campus *{box-sizing:border-box}
.pg-campus .bare{background:none;border:0;padding:0;margin:0;text-align:inherit;color:inherit}
.pg-campus .muted{color:var(--ink-3)}
.pg-campus h1, .pg-campus h2, .pg-campus h3{text-wrap:balance}
.pg-campus .icon-btn{width:40px;height:40px;border-radius:12px;border:0;background:transparent;color:var(--ink-2);display:grid;place-items:center;position:relative}
.pg-campus .icon-btn:hover{background:var(--surface-2);color:var(--ink)}
.pg-campus .icon-btn .dot{position:absolute;top:9px;right:10px;width:7px;height:7px;border-radius:50%;background:var(--brand);box-shadow:0 0 0 2px var(--surface)}
.pg-campus .mono{--s:56px;width:var(--s);height:var(--s);border-radius:calc(var(--s)*.26);background:var(--m-deep,var(--sc-deep));color:var(--m-on,var(--sc-on-deep));display:inline-grid;place-items:center;padding-bottom:calc(var(--s)*.07);font-family:var(--f-display);font-weight:800;font-size:calc(var(--s)*.36);letter-spacing:-.02em;line-height:1;position:relative;overflow:hidden;flex:none}
.pg-campus .mono::after{content:"";position:absolute;left:0;right:0;bottom:0;height:calc(var(--s)*.11);background:var(--m-bright,var(--sc-bright))}
.pg-campus .mono[data-len="3"]{font-size:calc(var(--s)*.3)}
.pg-campus .mono[data-len="4"]{font-size:calc(var(--s)*.24)}
.pg-campus .eyebrow{font-size:11px;font-weight:700;letter-spacing:.15em;text-transform:uppercase;color:var(--sc-ink);display:flex;align-items:center;gap:8px;flex-wrap:wrap}
.pg-campus .badge{display:inline-flex;align-items:center;gap:4px;height:20px;padding:0 8px;border-radius:999px;background:var(--brand-tint);color:var(--brand-ink);font-size:10.5px;letter-spacing:.08em}
.pg-campus .card, .pg-campus .panel{background:var(--surface);border:1px solid var(--line);border-radius:var(--r-lg)}
.pg-campus .panel{padding:18px}
.pg-campus .btn{display:inline-flex;align-items:center;justify-content:center;gap:8px;height:40px;padding:0 16px;border-radius:12px;border:1px solid transparent;font-weight:700;font-size:14px;white-space:nowrap;text-decoration:none}
.pg-campus .btn.sm{height:34px;padding:0 12px;font-size:13px;border-radius:10px}
.pg-campus .btn.wide{width:100%}
.pg-campus .btn.primary{background:var(--brand);color:var(--on-brand)}
.pg-campus .btn.primary:hover{filter:brightness(1.06)}
.pg-campus .btn.primary.is-on{background:var(--brand-tint);color:var(--brand-ink);border-color:color-mix(in srgb,var(--brand) 35%,transparent)}
.pg-campus .btn.soft{background:var(--surface-2);color:var(--ink);border-color:var(--line)}
.pg-campus .btn.soft:hover{background:var(--surface-3)}
.pg-campus .btn.ghost{background:transparent;color:var(--ink-2);border-color:var(--line)}
.pg-campus .btn.ghost:hover{background:var(--surface-2);color:var(--ink)}
.pg-campus .btn:disabled{opacity:.45;cursor:default}
.pg-campus .link{display:inline-flex;align-items:center;gap:4px;border:0;background:none;padding:0;color:var(--brand-ink);font-weight:700;font-size:13px;white-space:nowrap}
.pg-campus .link:hover{text-decoration:underline}
.pg-campus .chips{display:flex;flex-wrap:wrap;gap:8px}
.pg-campus .chip{display:inline-flex;align-items:center;gap:7px;min-height:30px;padding:4px 12px;border-radius:999px;font-size:13px;font-weight:600;border:1px solid var(--line);color:var(--ink-2);background:var(--surface);text-decoration:none}
.pg-campus .chip.sc{background:var(--sc-tint);border-color:transparent;color:var(--sc-ink)}
.pg-campus .chip.sc .sub{font-weight:500;opacity:.85}
.pg-campus a.chip:hover{border-color:var(--ink-3);color:var(--ink)}
.pg-campus .facts{display:flex;flex-wrap:wrap;gap:6px 18px;list-style:none;margin:0;padding:0;color:var(--ink-2);font-size:14px}
.pg-campus .facts li{display:inline-flex;align-items:center;gap:6px}
.pg-campus .facts svg{color:var(--ink-3)}
.pg-campus .desc{max-width:66ch;color:var(--ink-2);margin:0;font-size:14.5px;line-height:1.55}
.pg-campus .more{position:relative}
.pg-campus .menu{position:absolute;right:0;top:calc(100% + 6px);min-width:200px;background:var(--surface);border:1px solid var(--line);border-radius:12px;box-shadow:var(--shadow-2);padding:6px;z-index:30;display:flex;flex-direction:column}
.pg-campus .menu button{border:0;background:none;text-align:left;padding:9px 10px;border-radius:8px;font-size:14px;color:var(--ink);display:flex;align-items:center;gap:10px}
.pg-campus .menu button svg{color:var(--ink-3)}
.pg-campus .menu button:hover{background:var(--surface-2)}
.pg-campus .panel-h{display:flex;align-items:baseline;justify-content:space-between;gap:12px;margin-bottom:14px;flex-wrap:wrap}
.pg-campus .panel-h h2, .pg-campus .sec-h h2{font-family:var(--f-display);font-size:20px;font-weight:700;margin:0;letter-spacing:-.01em;display:flex;align-items:baseline;gap:8px}
.pg-campus .count{font-family:var(--f-body);font-size:13px;font-weight:700;color:var(--ink-3);font-variant-numeric:tabular-nums}
.pg-campus .toolbar{display:grid;grid-template-columns:minmax(0,1.5fr) minmax(0,1fr) minmax(0,.85fr) auto;gap:10px;align-items:stretch}
.pg-campus .field{display:flex;flex-direction:column;justify-content:center;gap:1px;background:var(--surface-2);border:1px solid var(--line);border-radius:12px;padding:7px 12px 6px;position:relative;min-width:0;transition:border-color .15s,box-shadow .15s}
.pg-campus .field:focus-within{border-color:var(--brand);box-shadow:0 0 0 3px var(--brand-tint)}
.pg-campus .field label{font-size:11.5px;font-weight:700;color:var(--ink-2);letter-spacing:.01em}
.pg-campus .fi{display:flex;align-items:center;gap:8px;min-width:0}
.pg-campus .fi svg{color:var(--ink-3)}
.pg-campus .field input, .pg-campus .field select{border:0;background:transparent;font-size:14.5px;color:var(--ink);padding:0;height:24px;outline:none;min-width:0;width:100%;-webkit-appearance:none;appearance:none}
.pg-campus .field input::placeholder{color:var(--ink-3)}
.pg-campus .field input[type=search]::-webkit-search-cancel-button{display:none}
.pg-campus .field select{cursor:pointer;padding-right:18px;background-image:linear-gradient(45deg,transparent 50%,var(--ink-3) 50%),linear-gradient(135deg,var(--ink-3) 50%,transparent 50%);background-position:calc(100% - 9px) 11px,calc(100% - 4px) 11px;background-size:5px 5px;background-repeat:no-repeat}
.pg-campus .field select option{background:var(--surface);color:var(--ink)}
.pg-campus .combo-x{width:22px;height:22px;border-radius:6px;border:0;background:var(--surface-3);color:var(--ink-2);display:grid;place-items:center;flex:none}
.pg-campus .combo-chev{color:var(--ink-3);display:grid;place-items:center;flex:none;transition:transform .15s}
.pg-campus .combo.open .combo-chev{transform:rotate(180deg)}
.pg-campus .combo-list{position:absolute;left:-1px;right:-1px;top:calc(100% + 6px);max-height:300px;overflow-y:auto;margin:0;padding:6px;list-style:none;background:var(--surface);border:1px solid var(--line);border-radius:12px;box-shadow:var(--shadow-2);z-index:35;min-width:220px}
.pg-campus .combo-list li{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:8px 10px;border-radius:8px;cursor:pointer;font-size:14px}
.pg-campus .combo-list li b{font-weight:700;letter-spacing:.04em;color:var(--ink)}
.pg-campus .combo-list li span{font-size:12.5px;color:var(--ink-3);font-variant-numeric:tabular-nums}
.pg-campus .combo-list li[aria-selected="true"]{background:var(--surface-2)}
.pg-campus .combo-list li.is-current b{color:var(--brand-ink)}
.pg-campus .combo-list li.none{color:var(--ink-3);cursor:default}
.pg-campus .seg2{display:inline-flex;align-items:center;gap:2px;border:1px solid var(--line);border-radius:12px;padding:3px;background:var(--surface-2)}
.pg-campus .seg2 button{width:38px;height:100%;min-height:38px;border-radius:9px;border:0;background:transparent;color:var(--ink-3);display:grid;place-items:center}
.pg-campus .seg2 button[aria-pressed="true"]{background:var(--surface);color:var(--brand-ink);box-shadow:var(--shadow-1)}
.pg-campus .fsum{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:8px;margin:14px 2px 4px;font-size:13px;color:var(--ink-3)}
.pg-campus .fsum b{color:var(--ink)}
.pg-campus .pinned{display:flex;flex-wrap:wrap;align-items:center;gap:8px;margin-top:14px;padding:10px 12px;border-radius:12px;background:var(--surface-2)}
.pg-campus .pinned-l{display:inline-flex;align-items:center;gap:6px;font-size:12px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--ink-2);margin-right:4px}
.pg-campus .pinned-l svg{color:var(--star)}
.pg-campus .pchip{display:inline-flex;align-items:center;border-radius:999px;background:var(--surface);border:1px solid var(--line);font-size:12.5px;font-weight:700;letter-spacing:.04em}
.pg-campus .pchip button{border:0;background:none;height:28px;padding:0 6px 0 11px;color:var(--ink)}
.pg-campus .pchip button+button{padding:0 8px 0 2px;color:var(--ink-3)}
.pg-campus .unsorted{display:flex;align-items:center;gap:14px;padding:12px 14px;border:1px dashed var(--line);border-radius:14px;background:var(--surface-2);margin:10px 0 6px}
.pg-campus .unsorted .ic{width:38px;height:38px;border-radius:11px;background:var(--surface);border:1px solid var(--line);display:grid;place-items:center;color:var(--ink-2);flex:none}
.pg-campus .unsorted .tx{flex:1;min-width:0;font-size:14px;color:var(--ink-2);line-height:1.4}
.pg-campus .unsorted .tx b{color:var(--ink)}
.pg-campus .unsorted .tx span{display:block;font-size:12.5px;color:var(--ink-3)}
.pg-campus .clist{list-style:none;margin:4px 0 0;padding:0}
.pg-campus .crow{position:relative;display:grid;grid-template-columns:minmax(0,1fr) auto auto auto;align-items:center;gap:14px;padding:11px 10px;border-top:1px solid var(--line-soft);border-radius:10px}
.pg-campus .crow:first-child{border-top-color:transparent}
.pg-campus .crow:hover{background:var(--surface-2)}
.pg-campus .crow:hover+.crow{border-top-color:transparent}
.pg-campus .go{border:0;background:none;padding:0;text-align:left;display:flex;flex-direction:column;gap:1px;min-width:0;color:inherit}
.pg-campus .go::after{content:"";position:absolute;inset:0;border-radius:inherit}
.pg-campus .go:focus-visible{outline:none}
.pg-campus .go:focus-visible::after{outline:2px solid var(--brand);outline-offset:-2px}
.pg-campus .ccode{font-size:11.5px;font-weight:700;letter-spacing:.09em;color:var(--brand-ink);text-transform:uppercase;font-variant-numeric:tabular-nums}
.pg-campus .v-b .ccode, .pg-campus .v-c .ccode{color:var(--sc-ink)}
.pg-campus .ctitle{font-family:var(--f-display);font-weight:600;font-size:15.5px;line-height:1.3;color:var(--ink)}
.pg-campus .cdocs{display:inline-flex;align-items:center;gap:6px;font-size:11.5px;font-weight:700;letter-spacing:.07em;text-transform:uppercase;color:var(--ink-2);font-variant-numeric:tabular-nums;white-space:nowrap}
.pg-campus .cdocs svg{color:var(--ink-3)}
.pg-campus .star{position:relative;z-index:1;width:32px;height:32px;border-radius:9px;border:0;background:transparent;color:var(--ink-3);display:grid;place-items:center;opacity:0;transition:opacity .12s}
.pg-campus .star:hover{background:var(--surface-3);color:var(--ink)}
.pg-campus .star.on{opacity:1;color:var(--star)}
.pg-campus .crow:hover .star, .pg-campus .ccard:hover .star, .pg-campus .star:focus-visible{opacity:1}
@media (hover:none){.pg-campus .star{opacity:1}}
.pg-campus .arr{color:var(--ink-3);display:grid;place-items:center;transition:transform .15s,color .15s}
.pg-campus .crow:hover .arr{transform:translateX(3px);color:var(--brand-ink)}
.pg-campus .cgrid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px;list-style:none;margin:8px 0 0;padding:0}
.pg-campus .ccard{position:relative;background:var(--surface);border:1px solid var(--line);border-radius:14px;padding:14px 14px 10px;display:flex;flex-direction:column;gap:12px;transition:border-color .15s,box-shadow .15s,transform .15s}
.pg-campus .ccard:hover{border-color:color-mix(in srgb,var(--sc-ink) 40%,var(--line));box-shadow:var(--shadow-2);transform:translateY(-1px)}
.pg-campus .ccard footer{margin-top:auto;display:flex;justify-content:space-between;align-items:center}
.pg-campus .pager{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-top:14px;padding-top:14px;border-top:1px solid var(--line-soft)}
.pg-campus .pages{display:flex;gap:4px;align-items:center}
.pg-campus .pg{min-width:34px;height:34px;border-radius:9px;border:0;background:transparent;color:var(--ink-2);font-weight:600;font-size:13.5px;font-variant-numeric:tabular-nums}
.pg-campus .pg:hover{background:var(--surface-2)}
.pg-campus .pg.on{background:var(--brand-tint);color:var(--brand-ink);font-weight:800}
.pg-campus .pg-gap{color:var(--ink-3);padding:0 4px}
.pg-campus .empty{text-align:center;padding:36px 16px 28px;display:flex;flex-direction:column;align-items:center;gap:8px}
.pg-campus .empty-ic{width:48px;height:48px;border-radius:14px;background:var(--surface-2);display:grid;place-items:center;color:var(--ink-3)}
.pg-campus .empty h3{font-family:var(--f-display);margin:6px 0 0;font-size:18px}
.pg-campus .empty p{margin:0;color:var(--ink-2);max-width:46ch;font-size:14px}
.pg-campus .empty .row{display:flex;gap:8px;flex-wrap:wrap;justify-content:center;margin-top:8px}
.pg-campus .thumb{--w:40px;width:var(--w);aspect-ratio:3/4;max-width:100%;background:var(--paper);border:1px solid var(--line);border-radius:5px;padding:calc(var(--w)*.12);display:flex;flex-direction:column;gap:calc(var(--w)*.055);overflow:hidden;flex:none}
.pg-campus .thumb i{display:block;height:max(2px,calc(var(--w)*.032));background:var(--paper-line);border-radius:2px;flex:none}
.pg-campus .thumb i.h{height:max(3px,calc(var(--w)*.07));background:var(--paper-ink);width:68%;margin-bottom:calc(var(--w)*.03)}
.pg-campus .thumb i.h2{height:max(2px,calc(var(--w)*.05));background:var(--paper-ink);width:45%;margin-top:calc(var(--w)*.03)}
.pg-campus .thumb i.hl{background:color-mix(in srgb,var(--sc-bright) 45%,var(--paper))}
.pg-campus .thumb .cols{display:grid;grid-template-columns:1fr 1fr;gap:calc(var(--w)*.06)}
.pg-campus .thumb .cols>span{display:flex;flex-direction:column;gap:calc(var(--w)*.045)}
.pg-campus .thumb .tbl{display:grid;grid-template-columns:repeat(3,1fr);gap:1px;background:var(--paper-line);border:1px solid var(--paper-line);flex:none}
.pg-campus .thumb .tbl b{height:max(4px,calc(var(--w)*.075));background:var(--paper)}
.pg-campus .thumb .q{display:flex;align-items:center;gap:calc(var(--w)*.05)}
.pg-campus .thumb .q em{width:max(3px,calc(var(--w)*.06));height:max(3px,calc(var(--w)*.06));border-radius:50%;background:var(--paper-ink);flex:none}
.pg-campus .thumb .q i{flex:1}
.pg-campus .thumb .box{height:max(5px,calc(var(--w)*.12));border:1px dashed var(--paper-line);border-radius:3px;flex:none}
.pg-campus .dmeta{font-size:12.5px;color:var(--ink-3);display:flex;flex-wrap:wrap;gap:2px 8px;align-items:center}
.pg-campus .dmeta .code{font-weight:700;letter-spacing:.06em;color:var(--ink-2)}
.pg-campus .dot-sep::before{content:"·";margin-right:8px}
.pg-campus .type-pill{display:inline-flex;align-items:center;height:20px;padding:0 8px;border-radius:999px;background:var(--surface-2);border:1px solid var(--line-soft);font-size:11px;font-weight:700;color:var(--ink-2);letter-spacing:.02em}
.pg-campus .drow{position:relative;display:grid;grid-template-columns:auto minmax(0,1fr);gap:12px;align-items:center;padding:10px 8px;border-radius:12px}
.pg-campus .drow:hover{background:var(--surface-2)}
.pg-campus .drow .go{gap:3px}
.pg-campus .dtitle{font-weight:600;font-size:14px;line-height:1.35;color:var(--ink)}
.pg-campus .dstat{display:inline-flex;align-items:center;gap:5px;font-variant-numeric:tabular-nums}
.pg-campus .dmeta.sub{color:var(--ink-3);font-size:12px}
.pg-campus .nrow{position:relative;display:grid;grid-template-columns:auto minmax(0,1fr) auto;gap:12px;align-items:center;padding:9px 8px;border-radius:12px}
.pg-campus .nrow:hover{background:var(--surface-2)}
.pg-campus .nrow .mono{--s:32px}
.pg-campus .nname{font-weight:600;font-size:14px;line-height:1.3}
.pg-campus .nmeta{font-size:12.5px;color:var(--ink-3)}
.pg-campus .ndist{font-size:12.5px;font-weight:700;color:var(--ink-2);font-variant-numeric:tabular-nums;white-space:nowrap}
.pg-campus .hero-a{overflow:visible;position:relative}
.pg-campus .hero-a .rule{height:5px;border-radius:var(--r-lg) var(--r-lg) 0 0;background:linear-gradient(90deg,var(--sc-bright) 0 64%,var(--sc-deep) 64% 100%)}
.pg-campus .hero-a-body{display:grid;grid-template-columns:auto minmax(0,1fr) auto;gap:20px;padding:24px 26px 22px}
.pg-campus .hero-a-body .mono{--s:64px}
.pg-campus .hero-a h1{font-family:var(--f-display);font-size:clamp(26px,3vw,34px);font-weight:700;letter-spacing:-.025em;line-height:1.08;margin:6px 0 10px}
.pg-campus .hero-a .desc{margin-top:12px}
.pg-campus .hero-a .chips{margin-top:16px}
.pg-campus .hero-actions{display:flex;align-items:flex-start;gap:6px}
.pg-campus .statbar{display:grid;grid-template-columns:repeat(var(--n,4),minmax(0,1fr));border-top:1px solid var(--line);margin:0}
.pg-campus .statbar>div{padding:14px 26px;border-left:1px solid var(--line-soft)}
.pg-campus .statbar>div:first-child{border-left:0}
.pg-campus .statbar dt{font-size:10.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-3);font-weight:700}
.pg-campus .statbar dd{margin:2px 0 0;font-family:var(--f-display);font-size:24px;font-weight:700;letter-spacing:-.01em;font-variant-numeric:tabular-nums}
.pg-campus .a-grid{display:grid;grid-template-columns:minmax(0,1fr) 340px;gap:20px;margin-top:20px;align-items:start}
.pg-campus .rail{display:flex;flex-direction:column;gap:16px;min-width:0}
.pg-campus .rcard{padding:16px 14px 10px}
.pg-campus .rcard-h{display:flex;align-items:baseline;justify-content:space-between;gap:8px;padding:0 8px 8px}
.pg-campus .rcard-h h3{font-family:var(--f-display);font-size:16px;font-weight:700;margin:0;letter-spacing:-.005em}
.pg-campus .rcard-h p{margin:2px 0 0;font-size:12.5px;color:var(--ink-3)}
.pg-campus .upload{padding:18px;display:flex;flex-direction:column;gap:8px;background:var(--surface-2)}
.pg-campus .upload .ic{width:38px;height:38px;border-radius:11px;background:var(--surface);border:1px solid var(--line);display:grid;place-items:center;color:var(--brand-ink)}
.pg-campus .upload h3{font-family:var(--f-display);font-size:16px;margin:4px 0 0}
.pg-campus .upload p{margin:0 0 6px;color:var(--ink-2);font-size:13.5px}
.pg-campus .hero-b{border-radius:var(--r-lg);overflow:hidden;color:var(--sc-on-deep);position:relative;isolation:isolate;background:radial-gradient(120% 140% at 100% 0%,color-mix(in srgb,var(--sc-bright) 30%,transparent) 0,transparent 55%),var(--sc-deep)}
.pg-campus .hero-b .water{position:absolute;right:-.04em;bottom:-.24em;font-family:var(--f-display);font-weight:800;font-size:clamp(84px,14vw,196px);line-height:1;letter-spacing:-.045em;text-transform:uppercase;color:transparent;-webkit-text-stroke:1.5px color-mix(in srgb,var(--sc-on-deep) 17%,transparent);white-space:nowrap;pointer-events:none;z-index:-1;user-select:none}
.pg-campus .hero-b-inner{position:relative;padding:30px 32px 26px;display:grid;grid-template-columns:minmax(0,1fr) auto;gap:20px 28px;align-items:start}
.pg-campus .hero-b-id{display:flex;gap:18px;align-items:center;min-width:0}
.pg-campus .hero-b-id .mono{--s:72px;--m-deep:var(--sc-on-deep);--m-on:var(--sc-deep)}
.pg-campus .hero-b .eyebrow{color:var(--sc-bright-on-deep)}
.pg-campus .hero-b h1{font-family:var(--f-display);font-size:clamp(28px,3.6vw,42px);font-weight:700;letter-spacing:-.03em;line-height:1.04;margin:6px 0 6px}
.pg-campus .hero-b .meta{margin:0;font-size:14px;color:color-mix(in srgb,var(--sc-on-deep) 78%,transparent);display:flex;flex-wrap:wrap;gap:2px 14px}
.pg-campus .hero-b .desc{color:color-mix(in srgb,var(--sc-on-deep) 84%,transparent);grid-column:1/-1;max-width:70ch}
.pg-campus .hero-b-actions{display:flex;gap:8px;flex-wrap:wrap;align-items:center}
.pg-campus .btn.on-deep{background:var(--sc-on-deep);color:var(--sc-deep)}
.pg-campus .btn.on-deep.is-on{background:color-mix(in srgb,var(--sc-on-deep) 16%,transparent);color:var(--sc-on-deep);border-color:color-mix(in srgb,var(--sc-on-deep) 40%,transparent)}
.pg-campus .btn.ghost-deep{background:transparent;color:var(--sc-on-deep);border-color:color-mix(in srgb,var(--sc-on-deep) 34%,transparent)}
.pg-campus .btn.ghost-deep:hover{background:color-mix(in srgb,var(--sc-on-deep) 10%,transparent)}
.pg-campus .hero-b .icon-btn{color:var(--sc-on-deep);border:1px solid color-mix(in srgb,var(--sc-on-deep) 34%,transparent);width:40px;height:40px}
.pg-campus .hero-b .icon-btn:hover{background:color-mix(in srgb,var(--sc-on-deep) 10%,transparent)}
.pg-campus .hero-b-stats{display:grid;grid-template-columns:repeat(var(--n,4),minmax(0,1fr));margin:0;border-top:1px solid color-mix(in srgb,var(--sc-on-deep) 16%,transparent);background:var(--sc-deep)}
.pg-campus .hero-b-stats>div{padding:16px 32px;border-left:1px solid color-mix(in srgb,var(--sc-on-deep) 12%,transparent)}
.pg-campus .hero-b-stats>div:first-child{border-left:0}
.pg-campus .hero-b-stats dd{margin:0;font-family:var(--f-display);font-size:30px;font-weight:700;letter-spacing:-.02em;line-height:1.1;font-variant-numeric:tabular-nums}
.pg-campus .hero-b-stats dt{font-size:10.5px;font-weight:700;letter-spacing:.14em;text-transform:uppercase;color:color-mix(in srgb,var(--sc-on-deep) 70%,transparent);margin-top:3px}
.pg-campus .hero-b .rule{height:6px;background:var(--sc-bright)}
.pg-campus .sec-h{display:flex;align-items:flex-end;justify-content:space-between;gap:12px;margin:30px 2px 12px}
.pg-campus .sec-h p{margin:3px 0 0;color:var(--ink-3);font-size:13.5px}
.pg-campus .depts{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px}
.pg-campus .dept{display:flex;flex-direction:column;gap:6px;text-align:left;background:var(--surface);border:1px solid var(--line);border-radius:14px;padding:14px 14px 13px;color:var(--ink);transition:border-color .15s,background .15s}
.pg-campus .dept:hover{border-color:color-mix(in srgb,var(--sc-ink) 45%,var(--line))}
.pg-campus .dept b{font-family:var(--f-display);font-size:22px;font-weight:700;letter-spacing:.02em;line-height:1.1}
.pg-campus .dept span{font-size:12.5px;color:var(--ink-3);font-variant-numeric:tabular-nums}
.pg-campus .dept .bar{height:5px;border-radius:5px;background:var(--surface-3);overflow:hidden;margin-top:4px}
.pg-campus .dept .bar i{display:block;height:100%;border-radius:inherit;background:var(--sc-bright)}
.pg-campus .dept[aria-pressed="true"]{border-color:var(--sc-ink);box-shadow:inset 0 0 0 1px var(--sc-ink);background:var(--sc-tint)}
.pg-campus .dept.all{background:var(--surface-2);border-style:dashed;justify-content:center}
.pg-campus .dept.all b{font-size:16px;font-family:var(--f-body);letter-spacing:0;display:flex;align-items:center;gap:6px}
.pg-campus .car-nav{display:flex;gap:6px}
.pg-campus .car-nav .icon-btn{border:1px solid var(--line);width:36px;height:36px}
.pg-campus .carousel{display:grid;grid-auto-flow:column;grid-auto-columns:232px;gap:14px;overflow-x:auto;scroll-snap-type:x mandatory;padding:2px 2px 12px;scrollbar-width:thin;overscroll-behavior-x:contain}
.pg-campus .dcard{position:relative;scroll-snap-align:start;background:var(--surface);border:1px solid var(--line);border-radius:16px;overflow:hidden;display:flex;flex-direction:column;transition:box-shadow .15s,transform .15s}
.pg-campus .dcard:hover{box-shadow:var(--shadow-2);transform:translateY(-2px)}
.pg-campus .dcard .tw{background:var(--surface-2);padding:18px 30px 0;height:150px;overflow:hidden;display:flex;justify-content:center;border-bottom:1px solid var(--line-soft)}
.pg-campus .dcard .tw .thumb{border-radius:6px 6px 0 0;box-shadow:var(--shadow-2)}
.pg-campus .dcard .bd{padding:12px 14px 14px;display:flex;flex-direction:column;gap:7px;flex:1}
.pg-campus .dcard .dfoot{margin-top:auto;display:flex;justify-content:space-between;align-items:center;font-size:12px;color:var(--ink-3);font-variant-numeric:tabular-nums}
.pg-campus .near-row{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:10px}
.pg-campus .ncard{position:relative;background:var(--surface);border:1px solid var(--line);border-radius:14px;padding:14px;display:flex;flex-direction:column;gap:10px}
.pg-campus .ncard:hover{border-color:var(--ink-3)}
.pg-campus .ncard .mono{--s:36px}
.pg-campus .c-wrap{display:grid;grid-template-columns:330px minmax(0,1fr);gap:24px;align-items:start}
.pg-campus .c-id{display:flex;flex-direction:column;gap:16px;min-width:0}
@media (min-height:900px) and (min-width:1001px){.pg-campus .c-id{position:sticky;top:calc(var(--tbh) + 20px + env(safe-area-inset-top,0px))}}
.pg-campus .c-card{position:relative;border:1px solid var(--line);border-radius:var(--r-lg);padding:22px;background:linear-gradient(180deg,var(--sc-wash) 0,var(--surface) 70%)}
.pg-campus .c-top{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:14px}
.pg-campus .c-top .mono{--s:60px}
.pg-campus .c-card h1{font-family:var(--f-display);font-size:27px;font-weight:700;letter-spacing:-.025em;line-height:1.1;margin:6px 0 6px}
.pg-campus .c-counts{margin:0 0 12px;font-size:13.5px;color:var(--ink-2);font-weight:600;font-variant-numeric:tabular-nums}
.pg-campus .c-card .desc{font-size:14px;margin-bottom:16px}
.pg-campus .c-facts{display:grid;grid-template-columns:auto minmax(0,1fr);gap:9px 16px;margin:18px 0 0;padding-top:16px;border-top:1px solid var(--line);font-size:13.5px}
.pg-campus .c-facts dt{color:var(--ink-3);font-weight:600}
.pg-campus .c-facts dd{margin:0;color:var(--ink);font-weight:500;min-width:0;overflow-wrap:anywhere}
.pg-campus .c-facts dd small{display:block;color:var(--ink-3);font-size:12.5px}
.pg-campus .c-facts a{color:var(--brand-ink);font-weight:700;text-decoration:none;display:inline-flex;align-items:center;gap:4px}
.pg-campus .c-facts a:hover{text-decoration:underline}
.pg-campus .radar-card{padding:16px 14px 10px}
.pg-campus .radar{display:block;width:100%;height:auto;max-width:100%;margin:2px 0 4px}
.pg-campus .radar .ring{fill:none;stroke:var(--line);stroke-width:1}
.pg-campus .radar .ring.dash{stroke-dasharray:3 4}
.pg-campus .radar .axis{fill:none;stroke:var(--line-soft);stroke-width:1}
.pg-campus .radar .rlab{fill:var(--ink-3);font-size:10px;font-family:var(--f-body);font-weight:600}
.pg-campus .radar .north{fill:var(--ink-3);font-size:10px;font-weight:700;letter-spacing:.1em;font-family:var(--f-body)}
.pg-campus .radar .dlab{fill:var(--ink-2);font-size:11px;font-weight:700;font-family:var(--f-body)}
.pg-campus .radar .home-ring{fill:none;stroke:var(--sc-bright);stroke-width:2}
.pg-campus .radar .home{fill:var(--sc-deep)}
.pg-campus .radar .pt{stroke:var(--pt-stroke,var(--surface));stroke-width:2}
.pg-campus .radar g.rd{cursor:pointer}
.pg-campus .radar g.rd:hover .dlab, .pg-campus .radar g.rd:focus-visible .dlab{fill:var(--ink)}
.pg-campus .radar g.rd:focus-visible{outline:none}
.pg-campus .radar g.rd:focus-visible .pt{stroke:var(--brand);stroke-width:3}
.pg-campus .c-main{display:flex;flex-direction:column;gap:16px;min-width:0}
.pg-campus .c-hello h2{font-family:var(--f-display);font-size:28px;font-weight:700;letter-spacing:-.025em;margin:2px 0 4px}
.pg-campus .c-hello p{margin:0;color:var(--ink-2);max-width:62ch}
.pg-campus .shelf-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(190px,1fr));gap:10px;margin-top:4px}
.pg-campus .shelf-empty{display:flex;flex-direction:column;gap:12px;padding:6px 2px 4px}
.pg-campus .shelf-empty p{margin:0;color:var(--ink-2);font-size:14px}
.pg-campus .suggest{display:flex;flex-wrap:wrap;gap:8px;align-items:center}
.pg-campus .suggest .lab{font-size:12.5px;color:var(--ink-3);font-weight:600;margin-right:2px}
.pg-campus .sug{display:inline-flex;align-items:center;gap:6px;height:32px;padding:0 12px 0 9px;border-radius:999px;border:1px dashed color-mix(in srgb,var(--sc-ink) 45%,var(--line));background:var(--surface);color:var(--ink);font-size:13px;font-weight:700;letter-spacing:.03em}
.pg-campus .sug svg{color:var(--sc-ink)}
.pg-campus .sug:hover{background:var(--sc-tint);border-style:solid}
.pg-campus .tabs-h{display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap;margin-bottom:6px}
.pg-campus .tabs{display:inline-flex;gap:2px;padding:3px;background:var(--surface-2);border:1px solid var(--line);border-radius:12px}
.pg-campus .tabs button{height:34px;padding:0 14px;border-radius:9px;border:0;background:transparent;font-weight:700;font-size:13.5px;color:var(--ink-3)}
.pg-campus .tabs button[aria-selected="true"]{background:var(--surface);color:var(--ink);box-shadow:var(--shadow-1)}
.pg-campus .dlist{list-style:none;margin:0;padding:0;display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:2px 12px}
.pg-campus .pchips{display:flex;gap:8px;overflow-x:auto;padding:2px 2px 10px;margin:0 -2px 6px;scrollbar-width:thin}
.pg-campus .pchips button{flex:none;display:inline-flex;align-items:center;gap:7px;height:34px;padding:0 13px;border-radius:999px;border:1px solid var(--line);background:var(--surface);font-weight:700;font-size:13px;letter-spacing:.04em;color:var(--ink)}
.pg-campus .pchips button span{font-weight:600;color:var(--ink-3);letter-spacing:0;font-variant-numeric:tabular-nums}
.pg-campus .pchips button:hover{border-color:var(--ink-3)}
.pg-campus .pchips button[aria-pressed="true"]{background:var(--ink);border-color:var(--ink);color:var(--bg)}
.pg-campus .pchips button[aria-pressed="true"] span{color:color-mix(in srgb,var(--bg) 70%,transparent)}
.pg-campus .groups{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin-top:8px}
.pg-campus .grp{border:1px solid var(--line-soft);border-radius:14px;padding:12px 8px 6px;background:var(--surface);min-width:0}
.pg-campus .grp-h{display:flex;align-items:baseline;gap:10px;padding:0 8px 6px}
.pg-campus .grp-code{font-family:var(--f-display);font-size:18px;font-weight:700;letter-spacing:.03em;color:var(--sc-ink)}
.pg-campus .grp-meta{font-size:12.5px;color:var(--ink-3);flex:1;font-variant-numeric:tabular-nums}
.pg-campus .grp .crow{padding:9px 8px;grid-template-columns:minmax(0,1fr) auto auto}
.pg-campus .grp .crow .arr{display:none}
.pg-campus .more-groups{margin-top:12px}
@media (max-width:1180px){.pg-campus .cgrid{grid-template-columns:repeat(3,minmax(0,1fr))}
.pg-campus .a-grid{grid-template-columns:minmax(0,1fr)}
.pg-campus .rail{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));align-items:start}
.pg-campus .rail .upload{grid-column:1/-1}
.pg-campus .near-row{grid-template-columns:repeat(3,minmax(0,1fr))}
.pg-campus .near-row .ncard:nth-child(n+4){display:none}}
@media (max-width:1000px){.pg-campus .c-wrap{grid-template-columns:minmax(0,1fr)}
.pg-campus .c-id{display:grid;grid-template-columns:minmax(0,1.2fr) minmax(0,1fr);align-items:start}
.pg-campus .groups{grid-template-columns:minmax(0,1fr)}
.pg-campus .toolbar{grid-template-columns:minmax(0,1fr) minmax(0,1fr)}
.pg-campus .toolbar .f-search{grid-column:1/-1}}
@media (max-width:860px){.pg-campus .hero-a-body{grid-template-columns:auto minmax(0,1fr)}
.pg-campus .hero-actions{grid-column:1/-1}
.pg-campus .statbar, .pg-campus .hero-b-stats{grid-template-columns:repeat(2,minmax(0,1fr))}
.pg-campus .statbar>div:nth-child(3), .pg-campus .hero-b-stats>div:nth-child(3){border-left:0}
.pg-campus .statbar>div:nth-child(n+3), .pg-campus .hero-b-stats>div:nth-child(n+3){border-top:1px solid var(--line-soft)}
.pg-campus .hero-b-stats>div:nth-child(n+3){border-top-color:color-mix(in srgb,var(--sc-on-deep) 12%,transparent)}
.pg-campus .hero-b-inner{grid-template-columns:minmax(0,1fr)}
.pg-campus .depts{grid-template-columns:repeat(2,minmax(0,1fr))}
.pg-campus .dlist{grid-template-columns:minmax(0,1fr)}
.pg-campus .rail{grid-template-columns:minmax(0,1fr)}
.pg-campus .c-id{grid-template-columns:minmax(0,1fr)}}
@media (max-width:620px){.pg-campus .cgrid{grid-template-columns:repeat(2,minmax(0,1fr))}
.pg-campus .near-row{grid-template-columns:minmax(0,1fr)}
.pg-campus .near-row .ncard:nth-child(n+4){display:flex}
.pg-campus .ncard{flex-direction:row;align-items:center}
.pg-campus .toolbar{grid-template-columns:minmax(0,1fr) auto}
.pg-campus .toolbar .combo{grid-column:1/-1;order:2}
.pg-campus .toolbar .f-sort{order:3}
.pg-campus .toolbar .seg2{order:4}
.pg-campus .hero-a-body{padding:20px 18px;gap:14px}
.pg-campus .hero-a-body .mono{--s:52px}
.pg-campus .statbar>div{padding:12px 18px}
.pg-campus .hero-b-inner{padding:24px 20px 20px}
.pg-campus .hero-b-id{flex-direction:column;align-items:flex-start;gap:14px}
.pg-campus .hero-b-id .mono{--s:60px}
.pg-campus .hero-b-stats>div{padding:14px 20px}
.pg-campus .hero-b-stats dd{font-size:24px}
.pg-campus .crow{grid-template-columns:minmax(0,1fr) auto auto;gap:8px}
.pg-campus .crow .arr{display:none}
.pg-campus .panel{padding:14px}
.pg-campus .unsorted{flex-wrap:wrap}
.pg-campus .unsorted .btn{width:100%}
.pg-campus .pager .btn span.t{display:none}
.pg-campus .c-hello h2{font-size:24px}}
@media (prefers-reduced-motion:reduce){.pg-campus *, .pg-campus *::before, .pg-campus *::after{transition:none!important;animation:none!important;scroll-behavior:auto!important}}
/* ===== 44-doc.scope.py (ported prototype CSS, scoped) ===== */
.pg-doc{--surface:var(--card);--surface-2:var(--card-2);--surface-3:color-mix(in srgb,var(--card-2) 55%,var(--border));--ink:var(--fg);--ink-2:var(--muted);--ink-3:var(--faint);
--line:var(--border);--line-2:var(--border-2);--teal:var(--primary);--teal-hi:var(--primary-ink);--teal-soft:var(--primary-soft);--teal-faint:color-mix(in srgb,var(--primary-soft) 55%,var(--card));--on-teal:var(--on-primary);
--gold:var(--pin-btn);--gold-ink:var(--pin-ink);--gold-soft:var(--pin-bg);--coral:var(--danger);--coral-soft:var(--ta-bg);
--desk:color-mix(in srgb,var(--bg) 52%,var(--border));--desk-dot:color-mix(in srgb,var(--fg) 8%,transparent);
--paper:#ffffff;--paper-ink:#1b1b1b;--paper-rule:#2a2a2a;--paper-green:#2f8a3c;--paper-blue:#2440b0;--paper-red:#c4291f;--paper-grid:rgba(66,108,170,.2);
--chrome:var(--toast-bg);--on-chrome:var(--toast-fg);--sh-paper:0 1px 2px rgba(18,33,32,.1),0 14px 30px -16px rgba(18,33,32,.28);
--f-display:var(--display);--f-body:var(--body);--f-paper:"Tinos","Times New Roman",Times,serif;--f-hand:"Caveat","Segoe Print","Bradley Hand",cursive;--f-arial:Arial,"Liberation Sans",Helvetica,sans-serif}
:root.dk .pg-doc{--desk:color-mix(in srgb,var(--bg) 78%,#000);--paper:#e8ebe7;--sh-paper:0 1px 2px rgba(0,0,0,.5),0 14px 30px -16px rgba(0,0,0,.75)}
.pg-doc .page-in{max-width:1320px}
.pg-doc .page-in>.crumbs{max-width:1260px;margin-inline:auto}
.pg-doc .doc-title{outline:none}
.pg-doc a.hcard,.pg-doc a.crow,.pg-doc a.course-mini{text-decoration:none;color:inherit}
.pg-doc .dp-more{display:flex;align-items:center;justify-content:center;gap:12px;flex-wrap:wrap;font-size:13px;color:var(--ink-3)}
.pg-doc .paper.locked{aspect-ratio:8.5/6}
.pg-doc .sk-p{display:flex;flex-direction:column;gap:.62em;padding:.15em 0 .1em}
.pg-doc .sk{display:block;height:.5em;border-radius:2px;background:rgba(20,24,28,.15)}
.pg-doc li>.sk{margin:.42em 0}
.pg-doc .t-red .sk{display:inline-block;vertical-align:middle;background:rgba(196,41,31,.3);margin:0}
.pg-doc .dp-defs li{white-space:nowrap}
.pg-doc .dp-defs .t-red{display:inline-block;width:52%}
.pg-doc .pg-formula .sk:not(:first-child){margin-top:.62em}
.pg-doc .gate-b .btn-ghost{background:color-mix(in srgb,var(--card) 70%,transparent)}
.pg-doc .crow-glyph,.pg-doc .cm-glyph{display:grid;place-items:center;background:none;width:auto;height:auto;border-radius:0;overflow:visible}
.pg-doc .crow{grid-template-columns:40px 84px minmax(0,1fr) auto 18px}
.pg-doc .crow>.i,.pg-doc .course-mini>.i{width:16px;height:16px;color:var(--ink-3)}
.pg-doc .cm-glyph{flex:none}
.pg-doc .rail-actions .btn[aria-pressed="true"] .i{fill:currentColor;fill-opacity:.25}
.pg-doc .about .link{padding:0;font-size:13.5px}
.pg-doc .progress3{grid-template-columns:repeat(var(--n,3),1fr)}
@media (max-width:640px){.pg-doc .crow{grid-template-columns:40px minmax(0,1fr) 16px}}
.pop.dp-ai-pop{width:320px;padding:8px}
.pg-doc button{font:inherit;color:inherit;background:none;border:0;padding:0;cursor:pointer;text-align:inherit}
.pg-doc h1, .pg-doc h2, .pg-doc h3, .pg-doc h4, .pg-doc p, .pg-doc ol, .pg-doc ul{margin:0}
.pg-doc ul, .pg-doc ol{padding:0;list-style:none}
.pg-doc .tnum{font-variant-numeric:tabular-nums}
.pg-doc .muted{color:var(--ink-3)}
.pg-doc .trunc{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0}
.pg-doc .code-chip{font-size:11px;font-weight:700;letter-spacing:.02em;padding:2px 6px;border-radius:6px;background:var(--surface-2);color:var(--ink-2);white-space:nowrap;font-variant-numeric:tabular-nums}
.pg-doc .wrap{width:100%;max-width:1140px;margin-inline:auto;display:flex;flex-direction:column;gap:26px}
.pg-doc .doc-title{font-family:var(--f-display);font-weight:700;font-size:clamp(26px,3.1vw,36px);line-height:1.12;letter-spacing:-.022em;text-wrap:balance}
.pg-doc .meta{display:flex;flex-wrap:wrap;align-items:center;gap:8px 12px;color:var(--ink-2);font-size:14px}
.pg-doc .meta .sep{width:3px;height:3px;border-radius:50%;background:var(--ink-3);opacity:.6}
.pg-doc .tag{display:inline-flex;align-items:center;gap:5px;height:24px;padding:0 9px;border-radius:999px;font-size:11px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;white-space:nowrap}
.pg-doc .tag .i{width:12px;height:12px;stroke-width:2.3}
.pg-doc .tag-pro{background:var(--gold-soft);color:var(--gold-ink)}
.pg-doc .tag-free{background:var(--teal-soft);color:var(--teal-hi)}
.pg-doc .tag-type{background:var(--surface);color:var(--ink-2);box-shadow:inset 0 0 0 1px var(--line);text-transform:none;letter-spacing:0;font-size:12px;font-weight:600}
.pg-doc .btn{display:inline-flex;align-items:center;justify-content:center;gap:8px;height:40px;padding:0 16px;border-radius:11px;font-weight:600;font-size:14px;white-space:nowrap;transition:background .15s,border-color .15s,color .15s,transform .15s,box-shadow .15s}
.pg-doc .btn .i{width:17px;height:17px}
.pg-doc .btn-ghost{background:var(--surface);border:1px solid var(--line-2);color:var(--ink)}
.pg-doc .btn-ghost:hover{border-color:var(--teal);color:var(--teal-hi)}
.pg-doc .btn-ghost[aria-pressed="true"]{background:var(--teal-soft);border-color:transparent;color:var(--teal-hi)}
.pg-doc .btn-ghost[aria-pressed="true"] .i{fill:currentColor;fill-opacity:.25}
.pg-doc .btn-primary{background:var(--teal);color:var(--on-teal);box-shadow:0 1px 0 rgba(255,255,255,.18) inset,0 8px 18px -10px var(--teal)}
.pg-doc .btn-primary:hover{background:var(--teal-hi)}
.pg-doc .btn-primary:active{transform:translateY(1px)}
.pg-doc .btn-icon{width:40px;padding:0}
.pg-doc .btn-sm{height:34px;padding:0 12px;font-size:13px;border-radius:10px}
.pg-doc .btn-lg{height:48px;padding:0 22px;font-size:15px;border-radius:13px}
.pg-doc .btn-block{width:100%}
.pg-doc .link{color:var(--teal-hi);font-weight:600;font-size:14px;text-decoration:underline;text-underline-offset:3px;text-decoration-color:color-mix(in srgb,var(--teal) 40%,transparent)}
.pg-doc .link:hover{text-decoration-color:currentColor}
.pg-doc .trust{display:flex;gap:6px 16px;flex-wrap:wrap;font-size:12.5px;color:var(--ink-3)}
.pg-doc .trust span{display:inline-flex;align-items:center;gap:5px}
.pg-doc .trust .i{width:14px;height:14px}
.pg-doc .block{display:flex;flex-direction:column;gap:16px;margin-top:14px}
.pg-doc .block-head{display:flex;align-items:flex-end;justify-content:space-between;gap:12px 20px;flex-wrap:wrap}
.pg-doc .block-head h2{font-family:var(--f-display);font-size:23px;font-weight:700;letter-spacing:-.015em;line-height:1.2}
.pg-doc .block-head p{color:var(--ink-3);font-size:14px;margin-top:3px}
.pg-doc .topics{display:flex;flex-wrap:wrap;gap:6px}
.pg-doc .topic{font-size:12.5px;font-weight:500;padding:4px 10px;border-radius:999px;background:var(--surface-2);color:var(--ink-2)}
.pg-doc .ring{--p:90;width:20px;height:20px;border-radius:50%;flex:none;background:conic-gradient(var(--teal) calc(var(--p) * 1%),var(--surface-3) 0);-webkit-mask:radial-gradient(farthest-side,transparent 58%,#000 61%);mask:radial-gradient(farthest-side,transparent 58%,#000 61%);transition:background .3s}
.pg-doc .on .i{fill:currentColor;fill-opacity:.2}
@keyframes dp-pop{0%{transform:scale(1)}40%{transform:scale(1.25) rotate(-8deg)}100%{transform:scale(1)}}
.pg-doc .pop .i{animation:dp-pop .4s ease}
.pg-doc .viewer{border:1px solid var(--line);border-radius:18px;background:var(--surface);overflow:clip;box-shadow:var(--sh-1)}
.pg-doc .vbar{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:10px 12px 10px 16px;border-bottom:1px solid var(--line);flex-wrap:wrap}
.pg-doc .vbar-l, .pg-doc .vbar-r{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
.pg-doc .pageno{font-size:13.5px;color:var(--ink-2);font-variant-numeric:tabular-nums}
.pg-doc .pageno b{color:var(--ink)}
.pg-doc .vsep{width:1px;height:20px;background:var(--line);margin:0 4px}
.pg-doc .zoom{display:inline-flex;align-items:center;border:1px solid var(--line);border-radius:10px;height:34px}
.pg-doc .zoom button{width:32px;height:100%;display:grid;place-items:center;color:var(--ink-2);border-radius:9px}
.pg-doc .zoom button:hover{color:var(--ink);background:var(--surface-2)}
.pg-doc .zoom .i{width:15px;height:15px}
.pg-doc .zoom span{min-width:46px;text-align:center;font-size:13px;font-variant-numeric:tabular-nums;color:var(--ink-2)}
.pg-doc .chip-btn{display:inline-flex;align-items:center;gap:7px;height:34px;padding:0 12px;border-radius:10px;font-size:13.5px;font-weight:600;color:var(--ink-2);border:1px solid var(--line)}
.pg-doc .chip-btn .i{width:16px;height:16px}
.pg-doc .chip-btn:hover{color:var(--ink);background:var(--surface-2)}
.pg-doc .chip-btn.ai{background:var(--teal);border-color:transparent;color:var(--on-teal)}
.pg-doc .chip-btn.ai:hover{background:var(--teal-hi)}
.pg-doc .desk{--pw:min(680px,100%);background-color:var(--desk);background-image:radial-gradient(var(--desk-dot) 1px,transparent 1.3px);background-size:18px 18px;padding:32px 24px 44px;overflow-x:auto}
.pg-doc .pages{display:flex;flex-direction:column;align-items:center;align-items:safe center;gap:28px}
.pg-doc .paper{position:relative;display:block;width:var(--pw,100%);flex:none;aspect-ratio:8.5/11;background:var(--paper);color:var(--paper-ink);box-shadow:var(--sh-paper);border-radius:3px;container-type:inline-size;overflow:hidden;text-align:left}
.pg-doc .pg{font-family:var(--f-paper);font-size:2.06cqw;line-height:1.42;padding:3.4em 3.7em 2.6em;height:100%;display:flex;flex-direction:column;gap:.85em}
.pg-doc .paper.locked .pg{filter:blur(.3em);opacity:.82;user-select:none}
.pg-doc .paper.locked{-webkit-mask-image:linear-gradient(#000 35%,transparent 97%);mask-image:linear-gradient(#000 35%,transparent 97%)}
.pg-doc .pg-head{display:flex;justify-content:space-between;gap:1em;font-size:.82em;color:rgba(0,0,0,.6);border-bottom:1px solid rgba(0,0,0,.25);padding-bottom:.5em}
.pg-doc .pg-title{font-size:1.38em;font-weight:700;text-align:center;line-height:1.2}
.pg-doc .pg-sub{font-size:1.12em;font-weight:700}
.pg-doc .pg-formula{border:1px solid rgba(0,0,0,.3);padding:.5em .8em;background:rgba(0,0,0,.03)}
.pg-doc .pg-cap{font-weight:700}
.pg-doc .pg-blank{display:inline-block;width:12em;border-bottom:1px solid var(--paper-rule);height:1em;vertical-align:-.15em}
.pg-doc .pg-blank.s{width:7em}
.pg-doc .pg-blank.xs{width:2.6em}
.pg-doc .pg-hand{font-family:var(--f-hand);color:var(--paper-blue);font-size:1.38em;line-height:1.1;font-weight:500}
.pg-doc .pg-hand.block{display:block;padding-left:1.1em}
.pg-doc .pg-lines{display:flex;flex-direction:column;gap:1.6em;padding-top:.5em}
.pg-doc .pg-lines i{display:block;border-bottom:1px solid rgba(0,0,0,.32)}
.pg-doc .pg-foot{margin-top:auto;text-align:center;font-size:.8em;color:rgba(0,0,0,.5)}
.pg-doc .pg-table{border-collapse:collapse;width:100%;font-size:.9em;line-height:1.3}
.pg-doc .pg-table th, .pg-doc .pg-table td{border:1px solid var(--paper-rule);padding:.3em .45em;text-align:left;font-variant-numeric:tabular-nums}
.pg-doc .pg-table.t-narrow{width:62%}
.pg-doc .pg-table .g{color:var(--paper-green);font-weight:700}
.pg-doc .pg ol{list-style:decimal;padding-left:1.5em;display:flex;flex-direction:column;gap:.25em}
.pg-doc .pg ul{list-style:disc;padding-left:1.4em;display:flex;flex-direction:column;gap:.15em}
.pg-doc .pg ul ul{list-style:circle;margin-top:.1em}
.pg-doc .pg ol.t-alpha{list-style:upper-alpha;margin-top:.15em}
.pg-doc .pg.f-arial{font-family:var(--f-arial);font-size:1.86cqw;line-height:1.36}
.pg-doc .pg.grid{background-image:linear-gradient(var(--paper-grid) 1px,transparent 1px),linear-gradient(90deg,var(--paper-grid) 1px,transparent 1px);background-size:1.3em 1.3em;padding-top:2.4em}
.pg-doc .t-red{color:var(--paper-red)}
.pg-doc .t-blue{color:var(--paper-blue);font-style:italic;font-weight:700}
.pg-doc .t-hand{font-family:var(--f-hand);font-size:2em;line-height:1.28;color:#2A2D3A;display:flex;flex-direction:column;gap:.15em;font-weight:500}
.pg-doc .t-redpen{color:var(--paper-red);font-size:.9em;margin-left:.5em}
.pg-doc .t-nh{font-weight:700;display:flex;justify-content:space-between;border-bottom:1px solid rgba(0,0,0,.3);padding-bottom:.25em}
.pg-doc .t-date{font-weight:400;color:rgba(0,0,0,.55)}
.pg-doc .t-rt{font-size:1.3em;font-weight:700;text-align:center;line-height:1.2}
.pg-doc .t-ra{text-align:center;font-style:italic;color:rgba(0,0,0,.6)}
.pg-doc .t-fig{display:flex;align-items:flex-end;justify-content:center;gap:3em;height:8em;margin:0 4em;border-left:1px solid var(--paper-rule);border-bottom:1px solid var(--paper-rule)}
.pg-doc .t-fig span{width:3.2em;background:var(--paper-blue);opacity:.55}
.pg-doc .t-fig span+span{opacity:.3}
.pg-doc .t-cap{font-size:.86em;font-style:italic;text-align:center}
.pg-doc .t-sketch{width:13em;height:7em;stroke:#2A2D3A;fill:none;stroke-width:1.4}
.pg-doc .dcard-course{font-size:12.5px;color:var(--ink-3);font-weight:500}
.pg-doc a.ccard:hover{box-shadow:var(--sh-2);border-color:var(--line-2);transform:translateY(-2px)}
.pg-doc a.ccard:hover .ccard-link .i{transform:translateX(3px)}
.pg-doc .crow-list{display:flex;flex-direction:column;border:1px solid var(--line);border-radius:16px;background:var(--surface);overflow:hidden}
.pg-doc .crow{display:grid;grid-template-columns:46px 84px minmax(0,1fr) auto 18px;align-items:center;gap:14px;padding:14px 18px;border-top:1px solid var(--line);transition:background .15s}
.pg-doc .crow:first-child{border-top:0}
.pg-doc .crow:hover{background:var(--surface-2)}
.pg-doc .crow-glyph{position:relative;width:46px;height:46px;border-radius:12px;overflow:hidden;color:var(--c);background:color-mix(in srgb,var(--c) 10%,var(--surface))}
.pg-doc .crow-code{font-family:var(--f-display);font-weight:700;font-size:15px;white-space:nowrap}
.pg-doc .crow-main{display:flex;flex-direction:column;min-width:0}
.pg-doc .crow-name{font-weight:600}
.pg-doc .crow-sub{font-size:12.5px;color:var(--ink-3)}
.pg-doc .crow-count{font-size:13px;color:var(--ink-2);white-space:nowrap;font-variant-numeric:tabular-nums}
.pg-doc .crow>.i{width:16px;height:16px;color:var(--ink-3)}
.pg-doc .tabs{display:flex;gap:4px;border-bottom:1px solid var(--line);overflow-x:auto;scrollbar-width:none}
.pg-doc .tabs::-webkit-scrollbar{display:none}
.pg-doc .tab{position:relative;height:46px;padding:0 14px;font-weight:600;color:var(--ink-3);white-space:nowrap;display:inline-flex;align-items:center;gap:8px;transition:color .15s}
.pg-doc .tab .i{width:16px;height:16px}
.pg-doc .tab:hover{color:var(--ink-2)}
.pg-doc .tab[aria-selected="true"]{color:var(--ink)}
.pg-doc .tab[aria-selected="true"]::after{content:"";position:absolute;left:10px;right:10px;bottom:-1px;height:2px;border-radius:2px;background:var(--teal)}
.pg-doc .count{font-size:12px;padding:1px 7px;border-radius:999px;background:var(--surface-2);color:var(--ink-3);font-variant-numeric:tabular-nums}
.pg-doc .gate{position:relative;z-index:2;background:var(--surface);border:1px solid var(--line);border-radius:20px;box-shadow:var(--sh-3)}
.pg-doc .gate h3{font-family:var(--f-display);font-size:23px;font-weight:700;letter-spacing:-.015em;line-height:1.2;text-wrap:balance}
.pg-doc .gate .lead{color:var(--ink-2);font-size:14.5px}
@keyframes dp-ring{0%{box-shadow:var(--sh-3),0 0 0 0 color-mix(in srgb,var(--teal) 50%,transparent)}100%{box-shadow:var(--sh-3),0 0 0 20px transparent}}
.pg-doc .gate.pulse{animation:dp-ring 1.2s ease-out 2}
.pg-doc .wrap-b{max-width:1260px}
.pg-doc .doc-head-b{display:flex;flex-direction:column;gap:12px;max-width:820px}
.pg-doc .b-grid{display:grid;grid-template-columns:minmax(0,1fr) 340px;gap:24px;align-items:start}
.pg-doc .b-rail{display:flex;flex-direction:column;gap:14px}
.pg-doc .rcard{border:1px solid var(--line);border-radius:16px;background:var(--surface);padding:18px}
.pg-doc .rail-actions{display:grid;grid-template-columns:1fr 1fr auto;gap:8px}
.pg-doc .rail-actions .btn{padding:0 10px}
.pg-doc .rc-label{display:inline-flex;align-items:center;gap:6px;font-size:11.5px;font-weight:700;letter-spacing:.09em;text-transform:uppercase;color:var(--ink-3)}
.pg-doc .rc-label .i{width:14px;height:14px;color:var(--teal)}
.pg-doc .rc-big{display:flex;align-items:baseline;gap:8px;margin-top:6px;font-size:14px;color:var(--ink-2)}
.pg-doc .rc-big b{font-family:var(--f-display);font-size:36px;font-weight:700;color:var(--ink);letter-spacing:-.02em;line-height:1;font-variant-numeric:tabular-nums}
.pg-doc .rc-bar{height:6px;border-radius:999px;background:var(--surface-3);margin-top:12px;overflow:hidden}
.pg-doc .rc-bar i{display:block;height:100%;background:var(--teal);border-radius:inherit;transition:width .4s ease}
.pg-doc .rc-sub{margin-top:8px;font-size:12.5px;color:var(--ink-3);font-variant-numeric:tabular-nums}
.pg-doc .rc-ask{margin-top:14px;padding-top:14px;border-top:1px solid var(--line);display:flex;align-items:center;justify-content:space-between;gap:10px;font-size:14px;font-weight:500;flex-wrap:wrap}
.pg-doc .vote-row{display:flex;gap:6px}
.pg-doc .vote{display:inline-flex;align-items:center;gap:6px;height:36px;padding:0 12px;border-radius:10px;border:1px solid var(--line-2);font-weight:600;font-size:13.5px;color:var(--ink-2);font-variant-numeric:tabular-nums;transition:background .15s,border-color .15s,color .15s}
.pg-doc .vote .i{width:16px;height:16px}
.pg-doc .vote:hover{border-color:var(--ink-3);color:var(--ink)}
.pg-doc .vote.up.on{background:var(--teal-soft);border-color:var(--teal);color:var(--teal-hi)}
.pg-doc .vote.down.on{background:var(--coral-soft);border-color:var(--coral);color:var(--coral)}
.pg-doc .lock-band{display:flex;align-items:center;gap:14px;width:var(--pw);flex:none;padding:14px 14px 14px 16px;border-radius:14px;background:color-mix(in srgb,var(--surface) 86%,transparent);backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);border:1px solid var(--line);box-shadow:var(--sh-2);margin-bottom:-12px;position:relative;z-index:2}
.pg-doc .lb-ic{width:40px;height:40px;border-radius:11px;background:var(--gold-soft);color:var(--gold-ink);display:grid;place-items:center;flex:none}
.pg-doc .lock-band p{flex:1;min-width:0;font-size:13.5px;color:var(--ink-2)}
.pg-doc .lock-band p b{display:block;color:var(--ink);font-size:14.5px}
.pg-doc .gate-b{padding:20px;border-radius:16px;box-shadow:var(--sh-2);border-color:color-mix(in srgb,var(--gold) 40%,var(--line));background:linear-gradient(180deg,var(--gold-soft),var(--surface) 42%);display:flex;flex-direction:column;gap:12px}
.pg-doc .gate-b h3{font-size:21px}
.pg-doc .gate-b-top{display:flex;justify-content:space-between;align-items:center}
.pg-doc .price{font-size:13px;color:var(--ink-2)}
.pg-doc .price b{font-family:var(--f-display);font-size:21px;color:var(--ink)}
.pg-doc .progress3{display:grid;grid-template-columns:repeat(3,1fr);gap:4px}
.pg-doc .progress3 i{height:5px;border-radius:999px;background:var(--surface-3)}
.pg-doc .progress3 i.on{background:var(--teal)}
.pg-doc .prog-label{display:flex;justify-content:space-between;font-size:12.5px;color:var(--ink-3);font-weight:600;margin-bottom:6px}
.pg-doc .locked-list{display:flex;flex-direction:column;gap:8px}
.pg-doc .locked-list li{display:flex;align-items:center;gap:10px;padding:9px 12px 9px 9px;border-radius:12px;background:var(--surface);border:1px solid var(--line);font-size:13.5px;line-height:1.3}
.pg-doc .pg-no{width:24px;height:30px;border-radius:4px;border:1px solid var(--line-2);display:grid;place-items:center;font-size:11.5px;font-weight:700;color:var(--ink-2);flex:none;background:var(--surface-2);font-variant-numeric:tabular-nums}
.pg-doc .locked-list .i{margin-left:auto;width:15px;height:15px;color:var(--gold-ink)}
.pg-doc .small{font-size:12.5px;color:var(--ink-3)}
.pg-doc .about p{font-size:14px;color:var(--ink-2);margin:8px 0 12px}
.pg-doc .course-mini{display:flex;align-items:center;gap:14px;transition:border-color .15s,box-shadow .15s}
.pg-doc .course-mini:hover{border-color:var(--line-2);box-shadow:var(--sh-2)}
.pg-doc .cm-art{position:relative;width:52px;height:52px;border-radius:13px;overflow:hidden;flex:none;color:var(--c);background:color-mix(in srgb,var(--c) 10%,var(--surface))}
.pg-doc .cm-body{display:flex;flex-direction:column;gap:2px;min-width:0;flex:1}
.pg-doc .cm-body b{font-size:15px}
.pg-doc .cm-body .small{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.pg-doc .course-mini>.i{color:var(--ink-3)}
.pg-doc .list-b{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px}
.pg-doc .hcard{display:flex;gap:14px;align-items:stretch;padding:12px;border:1px solid var(--line);border-radius:14px;background:var(--surface);transition:border-color .15s,box-shadow .15s}
.pg-doc .hcard:hover{border-color:var(--line-2);box-shadow:var(--sh-2)}
.pg-doc .hthumb{position:relative;width:86px;flex:none;aspect-ratio:3/4;overflow:hidden;border-radius:9px;background:var(--desk);padding:7px 7px 0}
.pg-doc .hthumb .paper{width:100%;box-shadow:var(--sh-1)}
.pg-doc .hcard-body{min-width:0;display:flex;flex-direction:column;gap:3px;flex:1;padding-top:2px}
.pg-doc .hcard-title{font-weight:650;font-size:15.5px;line-height:1.3;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
.pg-doc .hcard-meta{margin-top:auto;display:flex;gap:6px 12px;align-items:center;font-size:12.5px;color:var(--ink-3);flex-wrap:wrap}
.pg-doc .lock-mini{display:inline-flex;align-items:center;gap:4px;color:var(--gold-ink);font-weight:600}
.pg-doc .lock-mini .i{width:13px;height:13px}
.pg-doc .free-mini{color:var(--teal-hi);font-weight:600}
.pg-doc .pm-head{display:flex;align-items:center;gap:8px;padding:8px 10px 10px;font-size:12px;font-weight:700;letter-spacing:.07em;text-transform:uppercase;color:var(--ink-3)}
.pg-doc .pm-head .i{width:15px;height:15px;color:var(--teal)}
.pg-doc .pm-item{display:flex;gap:12px;align-items:center;width:100%;padding:10px;border-radius:11px}
.pg-doc .pm-item:hover{background:var(--surface-2)}
.pg-doc .pm-ic{width:36px;height:36px;border-radius:10px;display:grid;place-items:center;background:var(--teal-soft);color:var(--teal-hi);flex:none}
.pg-doc .pm-ic .i{width:17px;height:17px}
.pg-doc .pm-txt{display:flex;flex-direction:column;min-width:0;flex:1}
.pg-doc .pm-txt b{font-size:14px}
.pg-doc .pm-txt small{font-size:12.5px;color:var(--ink-3)}
.pg-doc .pm-foot{margin:6px 4px 2px;padding:10px 8px 4px;border-top:1px solid var(--line);font-size:12.5px;color:var(--ink-3)}
@media (max-width:1100px){.pg-doc .b-grid{grid-template-columns:minmax(0,1fr)}
.pg-doc .b-rail{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));align-items:start}
.pg-doc .rail-actions{grid-column:1/-1}}
@media (max-width:900px){.pg-doc .list-b{grid-template-columns:minmax(0,1fr)}}
@media (max-width:640px){.pg-doc .gate h3{font-size:20px}
.pg-doc .desk{padding:18px 12px 28px}
.pg-doc .crow{grid-template-columns:42px minmax(0,1fr) 16px;padding:12px 14px}
.pg-doc .crow-code, .pg-doc .crow-count{display:none}
.pg-doc .crow-glyph{width:42px;height:42px}
.pg-doc .rail-actions{grid-template-columns:1fr 1fr auto}}
/* ===== 45-study.scope.py (ported prototype CSS, scoped) ===== */
.pg-study{--fg-2:color-mix(in srgb,var(--fg) 80%,var(--muted));--primary-hov:color-mix(in srgb,var(--primary) 84%,#000);
--tok:var(--primary-ink);--tok-bg:color-mix(in srgb,var(--primary) 9%,transparent);--tok-line:var(--primary);--ink-soft:color-mix(in srgb,var(--fg) 82%,var(--bg));
--bp-page:color-mix(in srgb,var(--primary) 9%,transparent);--studio-h:clamp(560px,calc(100vh - 236px),880px);--gutter:clamp(16px,3vw,36px);
--stage:#0a161d;--stage-2:#0d2c31;--stage-3:#12232b;--stage-line:#1e363d;--stage-fg:#e9f2f1;--stage-muted:#8ba3a6;--stage-accent:#2fd0b8;--stage-gold:#f2c35b}
:root.dk .pg-study{--stage:#071116;--stage-2:#0a2528;--stage-3:#0f1e24;--stage-line:#1b3238}
/* Idea Builder */
.pg-study .sr-ib button, .pg-study .sr-ib input, .pg-study .sr-ib textarea, .pg-study .sr-ib select{font:inherit;color:inherit}
.pg-study .sr-ib button{cursor:pointer;background:none;border:0;padding:0}
.pg-study .sr-ib a{color:inherit;text-decoration:none}
.pg-study .sr-ib h1, .pg-study .sr-ib h2, .pg-study .sr-ib h3, .pg-study .sr-ib h4{font-family:var(--display);margin:0;text-wrap:balance;letter-spacing:-.015em}
.pg-study .sr-ib p{margin:0}
.pg-study .sr-ib .eyebrow{font-size:11px;font-weight:800;letter-spacing:.2em;text-transform:uppercase;color:var(--primary-ink);display:inline-flex;align-items:center;gap:8px}
.pg-study .sr-ib .tnum{font-variant-numeric:tabular-nums}
.pg-study .sr-ib .icon-btn{width:38px;height:38px;border-radius:10px;display:grid;place-items:center;color:var(--fg-2);border:1px solid transparent;transition:background .15s,border-color .15s}
.pg-study .sr-ib .icon-btn:hover{background:var(--card-2)}
.pg-study .sr-ib .icon-btn.boxed{border-color:var(--border)}
.pg-study .sr-ib .btn{display:inline-flex;align-items:center;justify-content:center;gap:9px;height:44px;padding:0 22px;border-radius:999px;font-weight:700;font-size:15px;white-space:nowrap;transition:background .15s,transform .15s,box-shadow .15s,border-color .15s}
.pg-study .sr-ib .btn:active{transform:translateY(1px)}
.pg-study .sr-ib .btn-primary{background:var(--primary);color:var(--on-primary);box-shadow:0 6px 20px color-mix(in srgb,var(--primary) 32%,transparent)}
.pg-study .sr-ib .btn-primary:hover{background:var(--primary-hov)}
.pg-study .sr-ib .btn-outline{border:1px solid var(--border-2);background:var(--card);color:var(--fg)}
.pg-study .sr-ib .btn-outline:hover{border-color:var(--primary);color:var(--primary-ink)}
.pg-study .sr-ib .btn-ghost{color:var(--fg-2)}
.pg-study .sr-ib .btn-ghost:hover{background:var(--card-2)}
.pg-study .sr-ib .btn-on-stage{border:1px solid rgba(255,255,255,.22);color:#fff;background:rgba(255,255,255,.02)}
.pg-study .sr-ib .btn-on-stage:hover{background:rgba(255,255,255,.08)}
.pg-study .sr-ib .btn-sm{height:34px;padding:0 14px;font-size:13.5px}
.pg-study .sr-ib .btn-lg{height:52px;padding:0 28px;font-size:16px}
.pg-study .sr-ib .chip{display:inline-flex;align-items:center;gap:7px;height:38px;padding:0 17px;border-radius:999px;border:1px solid var(--border-2);background:var(--card);font-weight:600;font-size:14.5px;color:var(--fg-2);white-space:nowrap;transition:border-color .15s,background .15s,color .15s}
.pg-study .sr-ib .chip:hover{border-color:var(--primary)}
.pg-study .sr-ib .chip.on{background:var(--primary-soft);border-color:var(--primary);color:var(--primary-ink)}
.pg-study .sr-ib .field-search{display:flex;align-items:center;gap:10px;height:44px;width:min(320px,100%);border:1px solid var(--border-2);border-radius:999px;padding:0 16px;background:var(--card);color:var(--muted)}
.pg-study .sr-ib .field-search input{border:0;outline:0;background:transparent;flex:1;min-width:0;font-size:15px;color:var(--fg)}
.pg-study .sr-ib .row{margin-top:48px}
.pg-study .sr-ib .row-head{display:flex;align-items:flex-end;gap:16px;margin-bottom:18px}
.pg-study .sr-ib .row-head h2{font-size:22px;font-weight:700;display:flex;align-items:center;gap:10px}
.pg-study .sr-ib .row-head h2 .i{color:var(--primary)}
.pg-study .sr-ib .row-head p{color:var(--muted);font-size:15px;margin-top:4px}
.pg-study .sr-ib .row-arrows{margin-left:auto;display:flex;gap:8px}
.pg-study .sr-ib .row-arrows button{width:38px;height:38px;border-radius:50%;border:1px solid var(--border-2);display:grid;place-items:center;color:var(--fg-2);background:var(--card)}
.pg-study .sr-ib .row-arrows button:hover{border-color:var(--primary);color:var(--primary-ink)}
.pg-study .sr-ib .rail{display:grid;grid-auto-flow:column;grid-auto-columns:calc((100% - 3*24px)/4);gap:24px;overflow-x:auto;scroll-snap-type:x mandatory;scrollbar-width:none;padding-bottom:4px}
.pg-study .sr-ib .rail::-webkit-scrollbar{display:none}
.pg-study .sr-ib .rail > *{scroll-snap-align:start;min-width:0}
@media (max-width:1180px){.pg-study .sr-ib .rail{grid-auto-columns:calc((100% - 2*20px)/3);gap:20px}}
@media (max-width:820px){.pg-study .sr-ib .rail{grid-auto-columns:calc((100% - 16px)/2);gap:16px}}
@media (max-width:520px){.pg-study .sr-ib .rail{grid-auto-columns:78%}}
.pg-study .sr-ib .tile{display:flex;flex-direction:column;gap:14px;border-radius:14px;outline-offset:4px}
.pg-study .sr-ib .tile .thumb{transition:transform .2s ease, box-shadow .2s ease}
.pg-study .sr-ib .tile:hover .thumb{transform:translateY(-3px);box-shadow:var(--sh-3)}
.pg-study .sr-ib .tile-meta{display:flex;align-items:center;gap:12px;min-width:0}
.pg-study .sr-ib .tile-text{display:flex;flex-direction:column;min-width:0}
.pg-study .sr-ib .tile-name{font-family:var(--display);font-weight:700;font-size:16px;letter-spacing:-.01em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.pg-study .sr-ib .tile-sub{font-size:13px;color:var(--muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.pg-study .sr-ib .appicon{width:44px;height:44px;border-radius:12px;display:grid;place-items:center;flex:none;color:#fff;background:var(--ai,linear-gradient(145deg,#1d3a4a,#0e1c26));box-shadow:inset 0 1px 0 rgba(255,255,255,.22),0 3px 8px rgba(10,22,29,.2)}
.pg-study .sr-ib .appicon .i{width:22px;height:22px}
.pg-study .sr-ib .thumb{position:relative;aspect-ratio:16/9;max-width:100%;border-radius:12px;overflow:hidden;container-type:inline-size;color:var(--stage-fg);border:1px solid var(--stage-line);
background:radial-gradient(110% 80% at 0% 0%, var(--stage-2) 0%, transparent 60%),var(--stage)}
.pg-study .sr-ib .thumb.violet{background:radial-gradient(90% 80% at 0% 0%, #2a2256 0%, transparent 62%),#12142a;border-color:#272a4a}
.pg-study .sr-ib .thumb.warm{background:radial-gradient(90% 90% at 0% 0%, #3a2410 0%, transparent 60%),#18110c;border-color:#3a2a1c}
.pg-study .sr-ib .thumb.ember{background:radial-gradient(90% 90% at 50% 0%, #3b1512 0%, transparent 60%),#140c0b;border-color:#3a1c18}
.pg-study .sr-ib .t-in{position:absolute;inset:6cqi 6.5cqi 0;display:flex;flex-direction:column;gap:2.2cqi}
.pg-study .sr-ib .t-eye{font:700 2.5cqi/1 var(--mono);letter-spacing:.2em;text-transform:uppercase;color:var(--stage-accent)}
.pg-study .sr-ib .thumb.violet .t-eye{color:#a593ff}
.pg-study .sr-ib .t-q{font:700 4.4cqi/1.22 var(--body);color:#fff}
.pg-study .sr-ib .t-box{border:1px solid var(--stage-line);background:rgba(255,255,255,.025);border-radius:2cqi;padding:2.6cqi 3cqi;display:flex;flex-direction:column;gap:1.6cqi}
.pg-study .sr-ib .thumb.violet .t-box{border-color:#343766;background:rgba(124,104,255,.06)}
.pg-study .sr-ib .t-line{display:flex;align-items:center;gap:2cqi;font-size:3.3cqi;line-height:1.2;color:#d6e4e3}
.pg-study .sr-ib .t-line b{font-weight:700;color:#fff}
.pg-study .sr-ib .t-dot{width:3.4cqi;height:3.4cqi;border-radius:50%;border:.5cqi solid var(--stage-accent);flex:none;display:grid;place-items:center}
.pg-study .sr-ib .t-dot.done{background:transparent}
.pg-study .sr-ib .t-dot.done::after{content:"";width:1.6cqi;height:.9cqi;border-left:.5cqi solid var(--stage-accent);border-bottom:.5cqi solid var(--stage-accent);transform:rotate(-45deg) translate(.1cqi,-.2cqi)}
.pg-study .sr-ib .t-dot.open{border-color:var(--stage-gold)}
.pg-study .sr-ib .t-num{width:4.6cqi;height:4.6cqi;border-radius:1.2cqi;background:#7c62f0;color:#fff;display:grid;place-items:center;font-size:2.8cqi;font-weight:700;flex:none}
.pg-study .sr-ib .t-num.dim{background:#252a45;color:#9aa0c8}
.pg-study .sr-ib .t-opt{display:flex;align-items:center;gap:2cqi;border:1px solid var(--stage-line);border-radius:1.6cqi;padding:1.5cqi 2.2cqi;font-size:3cqi}
.pg-study .sr-ib .t-opt > span{width:3.6cqi;height:3.6cqi;border-radius:.9cqi;background:#16303a;display:grid;place-items:center;font-size:2.1cqi;font-weight:700;color:var(--stage-accent)}
.pg-study .sr-ib .t-opt.bad{border-color:#b54444;background:rgba(181,68,68,.12)}
.pg-study .sr-ib .t-opt.good{border-color:var(--stage-accent);background:rgba(47,208,184,.1)}
.pg-study .sr-ib .t-opt i{margin-left:auto;font-style:normal;font-weight:700;font-size:2.6cqi}
.pg-study .sr-ib .t-opt.bad i{color:#ff7a7a}
.pg-study .sr-ib .t-opt.good i{color:var(--stage-accent)}
.pg-study .sr-ib .t-tiles{display:flex;gap:1.6cqi;justify-content:center}
.pg-study .sr-ib .t-tile{width:6.2cqi;height:6.2cqi;border:1px solid var(--stage-accent);border-radius:1.2cqi;display:grid;place-items:center;font:700 3.2cqi/1 var(--body);color:#fff;background:rgba(47,208,184,.08)}
.pg-study .sr-ib .t-tile.ghost{border-color:var(--stage-line);color:#5d7479;background:none}
.pg-study .sr-ib .t-tile.hit{background:#2b8f62;border-color:#3fbf85}
.pg-study .sr-ib .t-tile.alt{background:#2c5aa0;border-color:#4d7fcb}
.pg-study .sr-ib .t-pill{display:inline-flex;align-items:center;justify-content:center;gap:1cqi;align-self:center;padding:1.2cqi 3cqi;border-radius:1.4cqi;background:var(--stage-accent);color:#052a24;font:700 2.2cqi/1 var(--body);letter-spacing:.12em;text-transform:uppercase}
.pg-study .sr-ib .t-card{border-radius:2cqi;border:1px solid var(--stage-line);display:grid;place-items:center;text-align:center;padding:2cqi}
.pg-study .sr-ib .t-grid{display:grid;gap:.8cqi}
.pg-study .sr-ib .t-cell{aspect-ratio:1;border-radius:.6cqi;display:grid;place-items:center;font:700 2.7cqi/1 var(--body);color:#cfe3e1;background:#0f242b}
.pg-study .sr-ib .t-cell.x{background:#06090b}
.pg-study .sr-ib .t-cell.hl{background:#2e7f74;color:#fff}
.pg-study .sr-ib .t-cell.gold{outline:.4cqi solid var(--stage-gold);color:var(--stage-gold)}
.pg-study .sr-ib .t-mono{font:400 2.8cqi/1.55 var(--mono);color:#7be0cf}
.pg-study .sr-ib .t-serif{font-family:"EB Garamond","Cormorant Garamond",Georgia,serif;font-style:italic}
.pg-study .sr-ib .t-bubble{align-self:flex-end;max-width:72%;background:var(--stage-accent);color:#04241f;font-weight:700;font-size:3.4cqi;line-height:1.2;padding:2.2cqi 3cqi;border-radius:2cqi}
.pg-study .sr-ib .t-bar{height:.9cqi;border-radius:1cqi;background:#1c3138;overflow:hidden}
.pg-study .sr-ib .t-bar > i{display:block;height:100%;background:var(--stage-accent);width:var(--w,40%)}
.pg-study .sr-ib .hoot{display:inline-block;flex:none;width:72px;height:78px;overflow:visible}
.pg-study .sr-ib .hoot-bob{animation:ib-hootbob 4.2s ease-in-out infinite;transform-origin:50% 100%}
@keyframes ib-hootbob{0%,100%{transform:translateY(0) rotate(0)}50%{transform:translateY(-3px) rotate(-1.5deg)}}
.pg-study .sr-ib .ib-band{display:grid;grid-template-columns:minmax(0,1.62fr) minmax(300px,1fr);gap:20px;align-items:stretch}
.pg-study .sr-ib .ib-card{position:relative;background:var(--card);border:1px solid var(--border);border-radius:var(--r-lg);box-shadow:var(--sh-2)}
.pg-study .sr-ib .ib-sent{padding:40px 40px 26px;display:flex;flex-direction:column;gap:22px;margin-top:34px}
.pg-study .sr-ib .ib-sent .eyebrow .new{font-size:9.5px;letter-spacing:.12em;background:var(--pin-bg);color:var(--pin-ink);padding:3px 7px;border-radius:6px}
.pg-study .sr-ib .sentence{font-family:var(--display);font-weight:600;font-size:clamp(27px,3.15vw,46px);line-height:1.42;letter-spacing:-.022em;color:var(--fg);text-wrap:pretty;margin:0}
.pg-study .sr-ib .tok{display:inline;font:inherit;letter-spacing:inherit;color:var(--tok);background:var(--tok-bg);border-bottom:3px solid var(--tok-line);border-radius:10px 10px 4px 4px;padding:0 .2em;cursor:pointer;transition:background .15s,transform .15s,box-shadow .15s;-webkit-box-decoration-break:clone;box-decoration-break:clone}
.pg-study .sr-ib .tok:hover{background:color-mix(in srgb,var(--primary) 16%,transparent)}
.pg-study .sr-ib .tok[aria-expanded="true"]{background:color-mix(in srgb,var(--primary) 20%,transparent);box-shadow:0 0 0 3px color-mix(in srgb,var(--primary) 18%,transparent)}
.pg-study .sr-ib .tok.topic{--tok:var(--pin-ink);--tok-bg:var(--pin-bg);--tok-line:var(--pin-btn)}
.pg-study .sr-ib .tok.topic:hover, .pg-study .sr-ib .tok.topic[aria-expanded="true"]{background:color-mix(in srgb,var(--pin-btn) 22%,transparent)}
.pg-study .sr-ib .tok .tv{display:inline-block}
.pg-study .sr-ib .tok.swap .tv{animation:ib-tokSwap .42s cubic-bezier(.3,1.4,.5,1)}
@keyframes ib-tokSwap{0%{opacity:0;transform:translateY(.35em)}100%{opacity:1;transform:none}}
.pg-study .sr-ib .ib-free{display:flex;flex-direction:column;gap:8px}
.pg-study .sr-ib .ib-free textarea{width:100%;min-height:150px;resize:vertical;border:1px solid var(--border-2);border-radius:16px;background:var(--card);padding:16px 18px;font:500 clamp(19px,1.8vw,24px)/1.45 var(--display);color:var(--fg);outline:0}
.pg-study .sr-ib .ib-free textarea:focus{border-color:var(--primary);box-shadow:0 0 0 4px color-mix(in srgb,var(--primary) 13%,transparent)}
.pg-study .sr-ib .ib-free .help{font-size:13px;color:var(--muted)}
.pg-study .sr-ib .sent-foot{display:flex;align-items:center;gap:12px;flex-wrap:wrap;border-top:1px solid var(--border);padding-top:16px}
.pg-study .sr-ib .linkish{font-size:14px;font-weight:700;color:var(--primary-ink);padding:8px 6px;border-radius:8px}
.pg-study .sr-ib .linkish:hover{background:var(--card-2)}
.pg-study .sr-ib .sent-foot .help{margin-left:auto;font-size:13px;color:var(--muted);display:inline-flex;gap:6px;align-items:center}
.pg-study .sr-ib .peek{position:absolute;right:40px;top:-50px;width:84px;height:52px;overflow:hidden;pointer-events:none}
.pg-study .sr-ib .peek .hoot{display:block}
.pg-study .sr-ib .say{position:absolute;right:136px;top:-44px;max-width:260px;background:var(--fg);color:var(--bg);font-size:13px;font-weight:600;line-height:1.35;padding:8px 12px;border-radius:12px 12px 4px 12px;box-shadow:var(--sh-2);transition:opacity .2s}
.pg-study .sr-ib .say.dim{opacity:.2}
.pg-study .sr-ib .tokpop{position:absolute;z-index:50;width:min(320px,calc(100vw - 32px));background:var(--card);border:1px solid var(--border-2);border-radius:16px;box-shadow:var(--sh-3);padding:10px;display:flex;flex-direction:column;gap:4px;animation:ib-popIn .16s ease}
@keyframes ib-popIn{from{opacity:0;transform:translateY(-4px)}}
.pg-study .sr-ib .tokpop .lbl{font-size:10.5px;font-weight:800;letter-spacing:.18em;text-transform:uppercase;color:var(--muted);padding:6px 8px 4px}
.pg-study .sr-ib .pop-opt{display:flex;align-items:center;gap:10px;width:100%;text-align:left;padding:9px 10px;border-radius:10px;font-size:14.5px;font-weight:600;color:var(--fg)}
.pg-study .sr-ib .pop-opt small{display:block;font-size:12px;font-weight:500;color:var(--muted)}
.pg-study .sr-ib .pop-opt:hover, .pg-study .sr-ib .pop-opt:focus-visible{background:var(--card-2)}
.pg-study .sr-ib .pop-opt[aria-checked="true"]{background:var(--primary-soft);color:var(--primary-ink)}
.pg-study .sr-ib .pop-opt .i{color:var(--primary-ink)}
.pg-study .sr-ib .pop-in{display:flex;gap:6px;padding:4px}
.pg-study .sr-ib .pop-in input{flex:1;min-width:0;height:40px;border:1px solid var(--border-2);border-radius:10px;padding:0 12px;background:var(--card);color:var(--fg);font-size:14.5px;outline:0}
.pg-study .sr-ib .pop-in input:focus{border-color:var(--primary)}
.pg-study .sr-ib .bp{margin-top:34px;padding:22px 22px 20px;display:flex;flex-direction:column;gap:14px;background-color:var(--card);
background-image:linear-gradient(color-mix(in srgb,var(--primary) 7%,transparent) 1px,transparent 1px),linear-gradient(90deg,color-mix(in srgb,var(--primary) 7%,transparent) 1px,transparent 1px);background-size:20px 20px}
.pg-study .sr-ib .bp-top{display:flex;align-items:center;gap:10px}
.pg-study .sr-ib .bp-top .lbl{font-size:10.5px;font-weight:800;letter-spacing:.2em;text-transform:uppercase;color:var(--muted)}
.pg-study .sr-ib .bp-top .kind{margin-left:auto;font-size:11px;font-weight:800;letter-spacing:.12em;text-transform:uppercase;color:var(--primary-ink);background:var(--primary-soft);padding:4px 9px;border-radius:99px}
.pg-study .sr-ib .bp-mech{display:flex;align-items:center;gap:14px}
.pg-study .sr-ib .bp-mech .appicon{width:52px;height:52px;border-radius:14px}
.pg-study .sr-ib .bp-mech h3{font-size:24px;font-weight:800;line-height:1.1}
.pg-study .sr-ib .bp-mech p{font-size:14px;color:var(--muted);margin-top:4px;line-height:1.45}
.pg-study .sr-ib .bp-specs{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));border:1px solid var(--border);border-radius:14px;background:var(--card);overflow:hidden}
.pg-study .sr-ib .bp-specs div{padding:10px 12px;min-width:0}
.pg-study .sr-ib .bp-specs div + div{border-left:1px solid var(--border)}
.pg-study .sr-ib .bp-specs dt{font-size:10.5px;font-weight:800;letter-spacing:.14em;text-transform:uppercase;color:var(--muted)}
.pg-study .sr-ib .bp-specs dd{margin:4px 0 0;font-family:var(--display);font-weight:700;font-size:16px}
.pg-study .sr-ib .bp-src{display:flex;align-items:center;gap:10px;font-size:13.5px;color:var(--fg-2);background:var(--card);border:1px dashed var(--border-2);border-radius:12px;padding:9px 12px}
.pg-study .sr-ib .bp-src .i{color:var(--danger)}
.pg-study .sr-ib .bp-alt .lbl{display:block;font-size:10.5px;font-weight:800;letter-spacing:.18em;text-transform:uppercase;color:var(--muted);margin-bottom:8px}
.pg-study .sr-ib .alts{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}
.pg-study .sr-ib .alt{display:flex;flex-direction:column;gap:2px;text-align:left;padding:10px 12px;border-radius:12px;border:1px solid var(--border);background:var(--card);font-weight:700;font-size:14px;transition:border-color .15s}
.pg-study .sr-ib .alt small{font-weight:500;font-size:12px;color:var(--muted)}
.pg-study .sr-ib .alt:hover{border-color:var(--primary)}
.pg-study .sr-ib .alt[aria-pressed="true"]{border-color:var(--primary);background:var(--primary-soft);color:var(--primary-ink)}
.pg-study .sr-ib .bp .btn-primary{width:100%;margin-top:2px}
.pg-study .sr-ib .bp-steps{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:10px}
.pg-study .sr-ib .bp-steps li{display:flex;gap:10px;align-items:flex-start;font-size:14px;color:var(--muted);line-height:1.4}
.pg-study .sr-ib .bp-steps li.run, .pg-study .sr-ib .bp-steps li.done{color:var(--fg)}
.pg-study .sr-ib .bp-steps .st{width:20px;height:20px;border-radius:50%;border:2px solid var(--border-2);flex:none;display:grid;place-items:center;margin-top:1px}
.pg-study .sr-ib .bp-steps .st .i{width:12px;height:12px;stroke-width:3;opacity:0;color:var(--on-primary)}
.pg-study .sr-ib .bp-steps li.run .st{border-color:var(--primary);border-top-color:transparent;animation:ib-spin .8s linear infinite}
.pg-study .sr-ib .bp-steps li.done .st{background:var(--primary);border-color:var(--primary)}
.pg-study .sr-ib .bp-steps li.done .st .i{opacity:1}
.pg-study .sr-ib .bp-steps small{display:block;font:500 11.5px/1.4 var(--mono);color:var(--muted)}
@keyframes ib-spin{to{transform:rotate(360deg)}}
.pg-study .sr-ib .bp-bar{height:6px;border-radius:6px;background:var(--card-2);overflow:hidden}
.pg-study .sr-ib .bp-bar i{display:block;height:100%;width:0;background:var(--primary);transition:width .4s}
.pg-study .sr-ib .gstage{position:relative;border-radius:var(--r-lg);overflow:hidden;color:var(--stage-fg);min-height:440px;display:flex;flex-direction:column;
background:radial-gradient(90% 70% at 0% 0%,var(--stage-2) 0%,transparent 60%),radial-gradient(60% 60% at 100% 100%,#2a2313 0%,transparent 60%),var(--stage);border:1px solid var(--stage-line)}
.pg-study .sr-ib .g-top{display:flex;align-items:center;gap:12px;flex-wrap:wrap;padding:16px 20px;border-bottom:1px solid var(--stage-line)}
.pg-study .sr-ib .g-top .t{font:700 10.5px/1 var(--mono);letter-spacing:.16em;text-transform:uppercase;color:var(--stage-accent)}
.pg-study .sr-ib .g-top b{font-family:var(--display);font-size:16px;color:#fff}
.pg-study .sr-ib .g-top .sc{margin-left:auto;font:700 12px var(--mono);color:#e9f2f1;background:rgba(255,255,255,.06);border:1px solid var(--stage-line);border-radius:7px;padding:5px 9px}
.pg-study .sr-ib .g-timer{height:4px;background:#15282f}
.pg-study .sr-ib .g-timer i{display:block;height:100%;width:100%;background:var(--stage-gold);transform-origin:left;transition:transform linear}
.pg-study .sr-ib .g-body{flex:1;padding:22px 20px;display:flex;flex-direction:column;gap:14px}
.pg-study .sr-ib .g-q{font:700 clamp(19px,2vw,24px)/1.3 var(--display);color:#fff;letter-spacing:-.01em}
.pg-study .sr-ib .g-sub{font-size:14px;color:var(--stage-muted)}
.pg-study .sr-ib .g-hint{font-size:13.5px;color:var(--stage-gold);display:flex;gap:7px;align-items:flex-start}
.pg-study .sr-ib .g-hint .i{width:15px;height:15px;margin-top:2px}
.pg-study .sr-ib .slots{display:flex;gap:8px;flex-wrap:wrap}
.pg-study .sr-ib .slot{min-width:108px;flex:1 1 108px;height:52px;border-radius:12px;border:1.5px dashed var(--stage-line);display:grid;place-items:center;font:600 12px var(--mono);color:#56717a}
.pg-study .sr-ib .slot.fill{border-style:solid;border-color:#3fbf85;background:rgba(63,191,133,.14);color:#fff;font:700 15px var(--body);animation:ib-pop .3s ease}
@keyframes ib-pop{50%{transform:scale(1.06)}}
.pg-study .sr-ib .pool{display:flex;gap:8px;flex-wrap:wrap}
.pg-study .sr-ib .pchip{height:46px;padding:0 18px;border-radius:12px;border:1px solid #24414a;background:rgba(255,255,255,.04);color:#e9f2f1;font-weight:700;font-size:15px}
.pg-study .sr-ib .pchip:hover{border-color:var(--stage-accent)}
.pg-study .sr-ib .pchip.nope{border-color:#e05a5a;animation:ib-nope .35s}
.pg-study .sr-ib .pchip[disabled]{opacity:.2;cursor:default}
@keyframes ib-nope{25%{transform:translateX(-5px)}75%{transform:translateX(5px)}}
.pg-study .sr-ib .facts{border:1px solid var(--stage-line);border-radius:12px;padding:12px 14px;background:rgba(255,255,255,.03);display:flex;flex-direction:column;gap:6px}
.pg-study .sr-ib .facts b{font:700 10px var(--mono);letter-spacing:.16em;text-transform:uppercase;color:var(--stage-muted)}
.pg-study .sr-ib .facts span{font-size:13.5px;color:#d6e4e3}
.pg-study .sr-ib .gopts{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}
.pg-study .sr-ib .gopt{display:flex;align-items:center;gap:10px;min-height:52px;padding:10px 14px;border-radius:12px;border:1px solid #24414a;background:rgba(255,255,255,.035);color:#e9f2f1;font-weight:600;font-size:15px;text-align:left}
.pg-study .sr-ib .gopt .k{width:24px;height:24px;border-radius:7px;background:#16303a;color:var(--stage-accent);font:700 11.5px/1 var(--mono);display:grid;place-items:center;flex:none}
.pg-study .sr-ib .gopt:hover:not([disabled]){border-color:var(--stage-accent)}
.pg-study .sr-ib .gopt[disabled]{cursor:default}
.pg-study .sr-ib .gopt.good{border-color:#3fbf85;background:rgba(63,191,133,.16)}
.pg-study .sr-ib .gopt.bad{border-color:#e05a5a;background:rgba(224,90,90,.15)}
.pg-study .sr-ib .gopt.dim{opacity:.4}
.pg-study .sr-ib .gfb{border-radius:12px;padding:12px 14px;border:1px solid #2a4a3f;background:rgba(63,191,133,.08);display:flex;align-items:center;gap:12px;flex-wrap:wrap;font-size:14px;color:#d6e4e3}
.pg-study .sr-ib .gfb.bad{border-color:#5a2a2e;background:rgba(224,90,90,.08)}
.pg-study .sr-ib .gfb strong{color:#fff}
.pg-study .sr-ib .cite{display:inline-flex;align-items:center;gap:6px;font:600 12px var(--mono);color:#9fd9cf;border:1px solid var(--stage-line);border-radius:7px;padding:4px 8px}
.pg-study .sr-ib .cite .i{width:13px;height:13px}
.pg-study .sr-ib .sbtn{margin-left:auto;display:inline-flex;align-items:center;gap:8px;height:40px;padding:0 16px;border-radius:999px;font-weight:700;font-size:14px;background:var(--stage-accent);color:#052a24}
.pg-study .sr-ib .sbtn.ghost{background:transparent;color:#fff;border:1px solid rgba(255,255,255,.24);margin-left:0}
.pg-study .sr-ib .g-end{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;gap:10px;padding:30px 20px}
.pg-study .sr-ib .g-end .big{font:800 54px/1 var(--display);color:#fff}
.pg-study .sr-ib .g-end h3{font-size:24px;font-weight:800;color:#fff}
.pg-study .sr-ib .g-end p{color:#b9cfd0;font-size:14.5px;max-width:40ch}
.pg-study .sr-ib .g-end .row-b{display:flex;gap:8px;flex-wrap:wrap;justify-content:center;margin-top:8px}
.pg-study .sr-ib .g-end .row-b .sbtn{margin-left:0}
.pg-study .sr-ib .g-build{position:absolute;inset:0;z-index:3;display:grid;place-items:center;background:var(--stage);color:var(--stage-muted);font:600 12px var(--mono);letter-spacing:.14em;text-transform:uppercase}
.pg-study .sr-ib .g-build span{display:inline-flex;gap:10px;align-items:center}
.pg-study .sr-ib .g-build i{width:10px;height:10px;border-radius:50%;background:var(--stage-accent);animation:ib-pulse 1s infinite}
@keyframes ib-pulse{50%{opacity:.25}}
.pg-study .sr-ib .seg{display:inline-flex;padding:3px;border-radius:999px;background:var(--card-2);border:1px solid var(--border)}
.pg-study .sr-ib .seg button{height:30px;padding:0 12px;border-radius:999px;font-weight:700;font-size:13px;color:var(--muted)}
.pg-study .sr-ib .seg button[aria-pressed="true"]{background:var(--card);color:var(--primary-ink);box-shadow:var(--sh-1),inset 0 0 0 1px color-mix(in srgb,var(--primary) 40%,transparent)}
.pg-study .sr-ib .switch{width:44px;height:26px;border-radius:99px;background:var(--border-2);position:relative;transition:background .2s;flex:none}
.pg-study .sr-ib .switch::after{content:"";position:absolute;top:3px;left:3px;width:20px;height:20px;border-radius:50%;background:#fff;box-shadow:0 1px 3px rgba(0,0,0,.25);transition:transform .2s}
.pg-study .sr-ib .switch[aria-checked="true"]{background:var(--primary)}
.pg-study .sr-ib .switch[aria-checked="true"]::after{transform:translateX(18px)}
.pg-study .sr-ib .step{display:inline-flex;align-items:center;border:1px solid var(--border-2);border-radius:99px;overflow:hidden}
.pg-study .sr-ib .step button{width:34px;height:32px;display:grid;place-items:center;color:var(--fg-2)}
.pg-study .sr-ib .step button:hover{background:var(--card-2)}
.pg-study .sr-ib .step b{min-width:28px;text-align:center;font-variant-numeric:tabular-nums}
.pg-study .sr-ib .vis{display:flex;flex-direction:column;gap:6px}
.pg-study .sr-ib .vis label{display:flex;gap:10px;align-items:center;padding:9px 12px;border:1px solid var(--border);border-radius:12px;cursor:pointer;font-size:14px;font-weight:600}
.pg-study .sr-ib .vis label:has(input:checked){border-color:var(--primary);background:var(--primary-soft)}
.pg-study .sr-ib .vis input{accent-color:var(--primary);width:16px;height:16px;margin:0}
.pg-study .sr-ib .chk{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:6px;font-size:13.5px;color:var(--fg-2)}
.pg-study .sr-ib .chk li{display:flex;gap:8px;align-items:flex-start}
.pg-study .sr-ib .chk .i{color:var(--success);margin-top:1px}
.pg-study .sr-ib .done-box{display:flex;flex-direction:column;gap:10px;background:var(--primary-soft);border-radius:14px;padding:14px}
.pg-study .sr-ib .done-box b{font-family:var(--display);font-size:17px}
.pg-study .sr-ib .copy{display:flex;gap:8px}
.pg-study .sr-ib .copy input{flex:1;min-width:0;height:40px;border:1px solid var(--border-2);border-radius:10px;padding:0 10px;background:var(--card);color:var(--fg);font:500 12.5px var(--mono)}
.pg-study .sr-ib .lib-bar{display:flex;align-items:center;gap:12px;flex-wrap:wrap;margin-top:44px}
.pg-study .sr-ib .lib-bar .chips{display:flex;gap:8px;flex-wrap:wrap}
.pg-study .sr-ib .lib-bar .field-search{margin-left:auto}
.pg-study .sr-ib .wish{font-style:italic;color:var(--muted);font-size:13px;line-height:1.35;white-space:normal;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
.pg-study .sr-ib .st-tile .tile-meta{align-items:flex-start}
.pg-study .sr-ib .st-tile .by{font-size:12.5px;color:var(--fg-2);font-weight:600;margin-top:2px}
.pg-study .sr-ib .mine-badge{font-size:9.5px;font-weight:800;letter-spacing:.12em;text-transform:uppercase;background:var(--pin-bg);color:var(--pin-ink);padding:3px 7px;border-radius:6px;margin-left:6px;vertical-align:2px}
.pg-study .sr-ib .lib-empty{margin-top:28px;color:var(--muted)}
.pg-study .sr-ib .mine-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:18px}
.pg-study .sr-ib .mkcard{display:flex;flex-direction:column;gap:12px;padding:14px;border:1px solid var(--border);border-radius:18px;background:var(--card)}
.pg-study .sr-ib .mkcard.fresh{border-color:var(--primary);box-shadow:0 0 0 3px var(--primary-soft)}
.pg-study .sr-ib .mkcard h3{font-size:18px;font-weight:700}
.pg-study .sr-ib .mkcard .wish{-webkit-line-clamp:3;font-size:14px}
.pg-study .sr-ib .mmeta{display:flex;align-items:center;gap:10px;flex-wrap:wrap;font-size:13px;color:var(--muted)}
.pg-study .sr-ib .pill{display:inline-flex;flex-wrap:nowrap;white-space:nowrap;flex:none;align-items:center;gap:5px;height:24px;padding:0 9px;border-radius:99px;font-size:11.5px;font-weight:700}
.pg-study .sr-ib .pill .i{width:12px;height:12px}
.pg-study .sr-ib .pill.pub{background:var(--primary-soft);color:var(--primary-ink)}
.pg-study .sr-ib .pill.draft{background:var(--pin-bg);color:var(--pin-ink)}
.pg-study .sr-ib .pill.priv{background:var(--card-2);color:var(--fg-2)}
.pg-study .sr-ib .mine-head{display:flex;align-items:flex-end;gap:16px;flex-wrap:wrap;margin-bottom:20px}
.pg-study .sr-ib .mine-head h1{font-size:30px;font-weight:800}
.pg-study .sr-ib .mine-head p{color:var(--muted);margin-top:4px}
.pg-study .sr-ib .mine-head .btn{margin-left:auto}
@media (max-width:1100px){.pg-study .sr-ib .ib-band, .pg-study .sr-ib .res-grid{grid-template-columns:minmax(0,1fr)}
.pg-study .sr-ib .bp{margin-top:0}}
@media (max-width:620px){.pg-study .sr-ib .ib-sent{padding:28px 20px 20px}
.pg-study .sr-ib .say{display:none}
.pg-study .sr-ib .peek{right:20px}
.pg-study .sr-ib .bp-specs{grid-template-columns:1fr 1fr}
.pg-study .sr-ib .bp-specs div:nth-child(3){grid-column:1/-1;border-left:0;border-top:1px solid var(--border)}
.pg-study .sr-ib .alts, .pg-study .sr-ib .gopts{grid-template-columns:1fr}
.pg-study .sr-ib .sent-foot .help{display:none}
.pg-study .sr-ib .lib-bar .field-search{margin-left:0;width:100%}}
/* Studio */
.pg-study .sr-st button, .pg-study .sr-st input, .pg-study .sr-st textarea, .pg-study .sr-st select{font:inherit;color:inherit}
.pg-study .sr-st button{cursor:pointer;background:none;border:0;padding:0}
.pg-study .sr-st a{color:inherit;text-decoration:none}
.pg-study .sr-st h1, .pg-study .sr-st h2, .pg-study .sr-st h3, .pg-study .sr-st h4{font-family:var(--display);margin:0;text-wrap:balance;letter-spacing:-.015em}
.pg-study .sr-st p{margin:0}
.pg-study .sr-st .eyebrow{font-size:11px;font-weight:800;letter-spacing:.2em;text-transform:uppercase;color:var(--primary-ink);display:inline-flex;align-items:center;gap:8px}
.pg-study .sr-st .tnum{font-variant-numeric:tabular-nums}
.pg-study .sr-st .icon-btn{width:38px;height:38px;border-radius:10px;display:grid;place-items:center;color:var(--fg-2);border:1px solid transparent;transition:background .15s,border-color .15s}
.pg-study .sr-st .icon-btn:hover{background:var(--card-2)}
.pg-study .sr-st .icon-btn.boxed{border-color:var(--border)}
.pg-study .sr-st .btn{display:inline-flex;align-items:center;justify-content:center;gap:9px;height:44px;padding:0 22px;border-radius:999px;font-weight:700;font-size:15px;white-space:nowrap;transition:background .15s,transform .15s,box-shadow .15s,border-color .15s}
.pg-study .sr-st .btn:active{transform:translateY(1px)}
.pg-study .sr-st .btn-primary{background:var(--primary);color:var(--on-primary);box-shadow:0 6px 20px color-mix(in srgb,var(--primary) 32%,transparent)}
.pg-study .sr-st .btn-primary:hover{background:var(--primary-hov)}
.pg-study .sr-st .btn-outline{border:1px solid var(--border-2);background:var(--card);color:var(--fg)}
.pg-study .sr-st .btn-outline:hover{border-color:var(--primary);color:var(--primary-ink)}
.pg-study .sr-st .btn-ghost{color:var(--fg-2)}
.pg-study .sr-st .btn-ghost:hover{background:var(--card-2)}
.pg-study .sr-st .btn-on-stage{border:1px solid rgba(255,255,255,.22);color:#fff;background:rgba(255,255,255,.02)}
.pg-study .sr-st .btn-on-stage:hover{background:rgba(255,255,255,.08)}
.pg-study .sr-st .btn-sm{height:34px;padding:0 14px;font-size:13.5px}
.pg-study .sr-st .btn-lg{height:52px;padding:0 28px;font-size:16px}
.pg-study .sr-st .chip{display:inline-flex;align-items:center;gap:7px;height:38px;padding:0 17px;border-radius:999px;border:1px solid var(--border-2);background:var(--card);font-weight:600;font-size:14.5px;color:var(--fg-2);white-space:nowrap;transition:border-color .15s,background .15s,color .15s}
.pg-study .sr-st .chip:hover{border-color:var(--primary)}
.pg-study .sr-st .chip.on{background:var(--primary-soft);border-color:var(--primary);color:var(--primary-ink)}
.pg-study .sr-st .field-search{display:flex;align-items:center;gap:10px;height:44px;width:min(320px,100%);border:1px solid var(--border-2);border-radius:999px;padding:0 16px;background:var(--card);color:var(--muted)}
.pg-study .sr-st .field-search input{border:0;outline:0;background:transparent;flex:1;min-width:0;font-size:15px;color:var(--fg)}
.pg-study .sr-st .row{margin-top:48px}
.pg-study .sr-st .row-head{display:flex;align-items:flex-end;gap:16px;margin-bottom:18px}
.pg-study .sr-st .row-head h2{font-size:22px;font-weight:700;display:flex;align-items:center;gap:10px}
.pg-study .sr-st .row-head h2 .i{color:var(--primary)}
.pg-study .sr-st .row-head p{color:var(--muted);font-size:15px;margin-top:4px}
.pg-study .sr-st .row-arrows{margin-left:auto;display:flex;gap:8px}
.pg-study .sr-st .row-arrows button{width:38px;height:38px;border-radius:50%;border:1px solid var(--border-2);display:grid;place-items:center;color:var(--fg-2);background:var(--card)}
.pg-study .sr-st .row-arrows button:hover{border-color:var(--primary);color:var(--primary-ink)}
.pg-study .sr-st .rail{display:grid;grid-auto-flow:column;grid-auto-columns:calc((100% - 3*24px)/4);gap:24px;overflow-x:auto;scroll-snap-type:x mandatory;scrollbar-width:none;padding-bottom:4px}
.pg-study .sr-st .rail::-webkit-scrollbar{display:none}
.pg-study .sr-st .rail > *{scroll-snap-align:start;min-width:0}
@media (max-width:1180px){.pg-study .sr-st .rail{grid-auto-columns:calc((100% - 2*20px)/3);gap:20px}}
@media (max-width:820px){.pg-study .sr-st .rail{grid-auto-columns:calc((100% - 16px)/2);gap:16px}}
@media (max-width:520px){.pg-study .sr-st .rail{grid-auto-columns:78%}}
.pg-study .sr-st .tile{display:flex;flex-direction:column;gap:14px;border-radius:14px;outline-offset:4px}
.pg-study .sr-st .tile .thumb{transition:transform .2s ease, box-shadow .2s ease}
.pg-study .sr-st .tile:hover .thumb{transform:translateY(-3px);box-shadow:var(--sh-3)}
.pg-study .sr-st .tile-meta{display:flex;align-items:center;gap:12px;min-width:0}
.pg-study .sr-st .tile-text{display:flex;flex-direction:column;min-width:0}
.pg-study .sr-st .tile-name{font-family:var(--display);font-weight:700;font-size:16px;letter-spacing:-.01em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.pg-study .sr-st .tile-sub{font-size:13px;color:var(--muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.pg-study .sr-st .appicon{width:44px;height:44px;border-radius:12px;display:grid;place-items:center;flex:none;color:#fff;background:var(--ai,linear-gradient(145deg,#1d3a4a,#0e1c26));box-shadow:inset 0 1px 0 rgba(255,255,255,.22),0 3px 8px rgba(10,22,29,.2)}
.pg-study .sr-st .appicon .i{width:22px;height:22px}
.pg-study .sr-st .thumb{position:relative;aspect-ratio:16/9;max-width:100%;border-radius:12px;overflow:hidden;container-type:inline-size;color:var(--stage-fg);border:1px solid var(--stage-line);
background:radial-gradient(110% 80% at 0% 0%, var(--stage-2) 0%, transparent 60%),var(--stage)}
.pg-study .sr-st .thumb.violet{background:radial-gradient(90% 80% at 0% 0%, #2a2256 0%, transparent 62%),#12142a;border-color:#272a4a}
.pg-study .sr-st .thumb.warm{background:radial-gradient(90% 90% at 0% 0%, #3a2410 0%, transparent 60%),#18110c;border-color:#3a2a1c}
.pg-study .sr-st .thumb.ember{background:radial-gradient(90% 90% at 50% 0%, #3b1512 0%, transparent 60%),#140c0b;border-color:#3a1c18}
.pg-study .sr-st .t-in{position:absolute;inset:6cqi 6.5cqi 0;display:flex;flex-direction:column;gap:2.2cqi}
.pg-study .sr-st .t-eye{font:700 2.5cqi/1 var(--mono);letter-spacing:.2em;text-transform:uppercase;color:var(--stage-accent)}
.pg-study .sr-st .thumb.violet .t-eye{color:#a593ff}
.pg-study .sr-st .t-q{font:700 4.4cqi/1.22 var(--body);color:#fff}
.pg-study .sr-st .t-box{border:1px solid var(--stage-line);background:rgba(255,255,255,.025);border-radius:2cqi;padding:2.6cqi 3cqi;display:flex;flex-direction:column;gap:1.6cqi}
.pg-study .sr-st .thumb.violet .t-box{border-color:#343766;background:rgba(124,104,255,.06)}
.pg-study .sr-st .t-line{display:flex;align-items:center;gap:2cqi;font-size:3.3cqi;line-height:1.2;color:#d6e4e3}
.pg-study .sr-st .t-line b{font-weight:700;color:#fff}
.pg-study .sr-st .t-dot{width:3.4cqi;height:3.4cqi;border-radius:50%;border:.5cqi solid var(--stage-accent);flex:none;display:grid;place-items:center}
.pg-study .sr-st .t-dot.done{background:transparent}
.pg-study .sr-st .t-dot.done::after{content:"";width:1.6cqi;height:.9cqi;border-left:.5cqi solid var(--stage-accent);border-bottom:.5cqi solid var(--stage-accent);transform:rotate(-45deg) translate(.1cqi,-.2cqi)}
.pg-study .sr-st .t-dot.open{border-color:var(--stage-gold)}
.pg-study .sr-st .t-num{width:4.6cqi;height:4.6cqi;border-radius:1.2cqi;background:#7c62f0;color:#fff;display:grid;place-items:center;font-size:2.8cqi;font-weight:700;flex:none}
.pg-study .sr-st .t-num.dim{background:#252a45;color:#9aa0c8}
.pg-study .sr-st .t-opt{display:flex;align-items:center;gap:2cqi;border:1px solid var(--stage-line);border-radius:1.6cqi;padding:1.5cqi 2.2cqi;font-size:3cqi}
.pg-study .sr-st .t-opt > span{width:3.6cqi;height:3.6cqi;border-radius:.9cqi;background:#16303a;display:grid;place-items:center;font-size:2.1cqi;font-weight:700;color:var(--stage-accent)}
.pg-study .sr-st .t-opt.bad{border-color:#b54444;background:rgba(181,68,68,.12)}
.pg-study .sr-st .t-opt.good{border-color:var(--stage-accent);background:rgba(47,208,184,.1)}
.pg-study .sr-st .t-opt i{margin-left:auto;font-style:normal;font-weight:700;font-size:2.6cqi}
.pg-study .sr-st .t-opt.bad i{color:#ff7a7a}
.pg-study .sr-st .t-opt.good i{color:var(--stage-accent)}
.pg-study .sr-st .t-tiles{display:flex;gap:1.6cqi;justify-content:center}
.pg-study .sr-st .t-tile{width:6.2cqi;height:6.2cqi;border:1px solid var(--stage-accent);border-radius:1.2cqi;display:grid;place-items:center;font:700 3.2cqi/1 var(--body);color:#fff;background:rgba(47,208,184,.08)}
.pg-study .sr-st .t-tile.ghost{border-color:var(--stage-line);color:#5d7479;background:none}
.pg-study .sr-st .t-tile.hit{background:#2b8f62;border-color:#3fbf85}
.pg-study .sr-st .t-tile.alt{background:#2c5aa0;border-color:#4d7fcb}
.pg-study .sr-st .t-pill{display:inline-flex;align-items:center;justify-content:center;gap:1cqi;align-self:center;padding:1.2cqi 3cqi;border-radius:1.4cqi;background:var(--stage-accent);color:#052a24;font:700 2.2cqi/1 var(--body);letter-spacing:.12em;text-transform:uppercase}
.pg-study .sr-st .t-card{border-radius:2cqi;border:1px solid var(--stage-line);display:grid;place-items:center;text-align:center;padding:2cqi}
.pg-study .sr-st .t-grid{display:grid;gap:.8cqi}
.pg-study .sr-st .t-cell{aspect-ratio:1;border-radius:.6cqi;display:grid;place-items:center;font:700 2.7cqi/1 var(--body);color:#cfe3e1;background:#0f242b}
.pg-study .sr-st .t-cell.x{background:#06090b}
.pg-study .sr-st .t-cell.hl{background:#2e7f74;color:#fff}
.pg-study .sr-st .t-cell.gold{outline:.4cqi solid var(--stage-gold);color:var(--stage-gold)}
.pg-study .sr-st .t-mono{font:400 2.8cqi/1.55 var(--mono);color:#7be0cf}
.pg-study .sr-st .t-serif{font-family:"EB Garamond","Cormorant Garamond",Georgia,serif;font-style:italic}
.pg-study .sr-st .t-bubble{align-self:flex-end;max-width:72%;background:var(--stage-accent);color:#04241f;font-weight:700;font-size:3.4cqi;line-height:1.2;padding:2.2cqi 3cqi;border-radius:2cqi}
.pg-study .sr-st .t-bar{height:.9cqi;border-radius:1cqi;background:#1c3138;overflow:hidden}
.pg-study .sr-st .t-bar > i{display:block;height:100%;background:var(--stage-accent);width:var(--w,40%)}
.pg-study .sr-st .hoot{display:inline-block;flex:none;width:72px;height:78px;overflow:visible}
.pg-study .sr-st .hoot-bob{animation:st-hootbob 4.2s ease-in-out infinite;transform-origin:50% 100%}
@keyframes st-hootbob{0%,100%{transform:translateY(0) rotate(0)}50%{transform:translateY(-3px) rotate(-1.5deg)}}
.pg-study .sr-st .view{animation:st-viewIn .32s ease both}
@keyframes st-viewIn{from{opacity:.35;transform:translateY(6px)}to{opacity:1;transform:none}}
.pg-study .sr-st .pill{display:inline-flex;align-items:center;gap:5px;height:24px;padding:0 9px;border-radius:999px;font-size:11.5px;font-weight:700;letter-spacing:.02em;white-space:nowrap;flex:none}
.pg-study .sr-st .pill .i{width:12px;height:12px}
.pg-study .sr-st .pill-draft{background:var(--pin-bg);color:var(--pin-ink)}
.pg-study .sr-st .pill-private{background:var(--card-2);color:var(--fg-2);box-shadow:inset 0 0 0 1px var(--border)}
.pg-study .sr-st .pill-pub{background:var(--primary-soft);color:var(--primary-ink)}
.pg-study .sr-st .lbl{font-size:11px;font-weight:800;letter-spacing:.18em;text-transform:uppercase;color:var(--muted)}
.pg-study .sr-st .seg{display:inline-flex;padding:3px;border-radius:999px;background:var(--card-2);border:1px solid var(--border);gap:2px}
.pg-study .sr-st .seg button{display:inline-flex;align-items:center;gap:7px;height:32px;padding:0 14px;border-radius:999px;font-weight:700;font-size:13.5px;color:var(--muted);transition:background .15s,color .15s}
.pg-study .sr-st .seg button:hover{color:var(--fg)}
.pg-study .sr-st .seg button[aria-checked="true"]{background:var(--primary-soft);color:var(--primary-ink);box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--primary) 45%,transparent)}
.pg-study .sr-st .seg-icon button{width:34px;padding:0;justify-content:center}
@keyframes st-cmpPulse{0%{box-shadow:var(--sh-2),0 0 0 0 color-mix(in srgb,var(--primary) 40%,transparent)}100%{box-shadow:var(--sh-2),0 0 0 14px transparent}}
.pg-study .sr-st #prompt{width:100%;border:0;outline:0;resize:vertical;min-height:118px;background:transparent;font-family:var(--display);font-size:clamp(19px,1.65vw,23px);font-weight:500;line-height:1.38;letter-spacing:-.012em;color:var(--fg);padding:6px 2px 2px}
.pg-study .sr-st #prompt::placeholder{color:var(--muted)}
.pg-study .sr-st .doc-chip{display:inline-flex;align-items:center;gap:10px;padding:6px 6px 6px 7px;border-radius:12px;background:var(--card-2);border:1px solid var(--border);min-width:0;max-width:100%}
.pg-study .sr-st .doc-ic{width:30px;height:30px;border-radius:8px;display:grid;place-items:center;flex:none;background:var(--ta-bg);color:var(--danger)}
.pg-study .sr-st .doc-ic .i{width:16px;height:16px}
.pg-study .sr-st .doc-tx{display:flex;flex-direction:column;min-width:0;line-height:1.25}
.pg-study .sr-st .doc-tx b{font-size:13.5px;font-weight:700;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.pg-study .sr-st .doc-tx span{font-size:12px;color:var(--muted);white-space:nowrap}
.pg-study .sr-st .doc-x{width:26px;height:26px;border-radius:7px;display:grid;place-items:center;color:var(--muted);flex:none}
.pg-study .sr-st .doc-x:hover{background:var(--border);color:var(--fg)}
.pg-study .sr-st .speech{position:relative;background:var(--card);border:1px solid var(--border);border-radius:16px;padding:14px 16px;font-size:14.5px;line-height:1.55;color:var(--fg-2);box-shadow:var(--sh-1)}
.pg-study .sr-st .speech::before{content:"";position:absolute;top:-7px;left:38px;width:12px;height:12px;background:var(--card);border-left:1px solid var(--border);border-top:1px solid var(--border);transform:rotate(45deg)}
.pg-study .sr-st .speech-sig{display:block;margin-top:8px;font-size:12px;font-weight:700;color:var(--muted)}
.pg-study .sr-st .trust{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:9px}
.pg-study .sr-st .trust li{display:flex;gap:9px;align-items:flex-start;font-size:13px;color:var(--muted);line-height:1.4}
.pg-study .sr-st .trust .i{color:var(--primary-ink);margin-top:1px}
.pg-study .sr-st .sec-head{display:flex;align-items:flex-end;gap:16px;margin-bottom:16px;flex-wrap:wrap}
.pg-study .sr-st .sec-head h2{font-size:22px;font-weight:700;display:flex;align-items:center;gap:10px}
.pg-study .sr-st .sec-head h2 .i{color:var(--primary)}
.pg-study .sr-st .sec-head p{color:var(--muted);font-size:15px;margin-top:4px}
.pg-study .sr-st .sec-head .row-arrows{margin-left:auto}
.pg-study .sr-st .mine{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:16px}
.pg-study .sr-st .mine-card{display:flex;gap:14px;align-items:center;padding:12px;border:1px solid var(--border);border-radius:16px;background:var(--card);text-align:left;min-width:0;transition:border-color .15s,box-shadow .15s}
.pg-study .sr-st .mine-card:hover{border-color:var(--border-2);box-shadow:var(--sh-2)}
.pg-study .sr-st .mine-card .thumb{width:136px;flex:none;border-radius:10px}
.pg-study .sr-st .mine-tx{display:flex;flex-direction:column;gap:6px;min-width:0;flex:1}
.pg-study .sr-st .mine-tx b{font-family:var(--display);font-size:16px;font-weight:700;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.pg-study .sr-st .mine-tx .sub{font-size:13px;color:var(--muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.pg-study .sr-st .mine-tx .pill{align-self:flex-start}
.pg-study .sr-st .mine-card.fresh{border-color:var(--primary);box-shadow:0 0 0 3px var(--primary-soft)}
.pg-study .sr-st .ctile .tile-meta{gap:12px}
.pg-study .sr-st .cavatar{width:40px;height:40px;border-radius:50%;display:grid;place-items:center;flex:none;font-weight:800;font-size:13px;color:#fff;background:var(--ca,#2b5a63);box-shadow:inset 0 1px 0 rgba(255,255,255,.22)}
.pg-study .sr-st .rate{margin-left:auto;display:inline-flex;align-items:center;gap:5px;font-size:12.5px;font-weight:700;color:var(--primary-ink);flex:none;padding-left:4px}
.pg-study .sr-st .rate .i{width:14px;height:14px}
.pg-study .sr-st .kind-tag{position:absolute;top:8px;right:8px;z-index:2;font:700 9.5px/1 var(--mono);letter-spacing:.14em;text-transform:uppercase;color:#cfe3e1;background:rgba(4,12,16,.72);border:1px solid rgba(255,255,255,.14);padding:4px 7px;border-radius:6px}
.pg-study .sr-st .name-row{display:flex;align-items:center;gap:8px;min-width:0}
.pg-study .sr-st .badge-new{font-size:9.5px;font-weight:800;letter-spacing:.12em;text-transform:uppercase;background:var(--pin-bg);color:var(--pin-ink);padding:3px 7px;border-radius:6px;flex:none}
.pg-study .sr-st .tile.just-pub .thumb{box-shadow:0 0 0 3px var(--primary),var(--sh-3);animation:st-justPub 1.6s ease 2}
@keyframes st-justPub{50%{box-shadow:0 0 0 6px color-mix(in srgb,var(--primary) 40%,transparent),var(--sh-3)}}
.pg-study .sr-st .make-thumb{position:relative;aspect-ratio:16/9;max-width:100%;border-radius:12px;border:1.5px dashed var(--border-2);display:flex;flex-direction:column;align-items:center;justify-content:center;gap:6px;text-align:center;padding:12px;color:var(--fg-2);
background-color:var(--card);background-image:linear-gradient(var(--bp-page) 1px,transparent 1px),linear-gradient(90deg,var(--bp-page) 1px,transparent 1px);background-size:18px 18px;transition:border-color .2s,transform .2s}
.pg-study .sr-st .make-tile:hover .make-thumb{border-color:var(--primary);transform:translateY(-3px)}
.pg-study .sr-st .mk-plus{width:44px;height:44px;border-radius:50%;display:grid;place-items:center;background:var(--primary);color:var(--on-primary);box-shadow:0 6px 18px color-mix(in srgb,var(--primary) 35%,transparent)}
.pg-study .sr-st .mk-t{font-family:var(--display);font-weight:700;font-size:16px;color:var(--fg)}
.pg-study .sr-st .mk-s{font-size:12.5px;color:var(--muted);max-width:26ch}
.pg-study .sr-st .mk-ic{background:var(--primary-soft)!important;color:var(--primary-ink);box-shadow:none}
.pg-study .sr-st .how{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));border:1px solid var(--border);border-radius:20px;background:var(--card);overflow:hidden}
.pg-study .sr-st .how-step{padding:22px 24px;display:flex;gap:14px;align-items:flex-start;min-width:0}
.pg-study .sr-st .how-step + .how-step{border-left:1px solid var(--border)}
.pg-study .sr-st .how-ic{width:40px;height:40px;border-radius:12px;display:grid;place-items:center;flex:none;background:var(--primary-soft);color:var(--primary-ink)}
.pg-study .sr-st .how-step h3{font-size:17px;font-weight:700;display:flex;align-items:baseline;gap:8px}
.pg-study .sr-st .how-step h3 span{font:700 11px var(--mono);color:var(--muted);letter-spacing:.1em}
.pg-study .sr-st .how-step p{font-size:14px;color:var(--muted);margin-top:4px;line-height:1.5}
.pg-study .sr-st .st-head{display:flex;align-items:center;gap:14px;flex-wrap:wrap;margin-bottom:16px}
.pg-study .sr-st .st-title{display:flex;flex-direction:column;gap:5px;min-width:0;flex:1}
.pg-study .sr-st .st-title h1{font-size:24px;font-weight:800;letter-spacing:-.02em}
.pg-study .sr-st .st-meta{display:flex;gap:10px;align-items:center;flex-wrap:wrap;font-size:13px;color:var(--muted)}
.pg-study .sr-st .st-saved{display:inline-flex;align-items:center;gap:6px;font-size:12.5px;color:var(--muted);font-weight:600}
.pg-study .sr-st .st-saved .i{color:var(--success)}
.pg-study .sr-st .studio{display:grid;grid-template-columns:minmax(330px,390px) minmax(0,1fr);gap:18px;height:var(--studio-h)}
.pg-study .sr-st .pane-chat, .pg-study .sr-st .pane-preview{display:flex;flex-direction:column;min-height:0;min-width:0;background:var(--card);border:1px solid var(--border);border-radius:20px;overflow:hidden}
.pg-study .sr-st .chat-log{flex:1;min-height:0;overflow-y:auto;padding:18px 16px;display:flex;flex-direction:column;gap:16px;scroll-behavior:smooth}
.pg-study .sr-st .chat-foot{border-top:1px solid var(--border);padding:12px 14px 14px;display:flex;flex-direction:column;gap:10px;background:var(--card)}
.pg-study .sr-st .msg{display:flex;gap:10px;align-items:flex-start;min-width:0}
.pg-study .sr-st .msg.me{justify-content:flex-end}
.pg-study .sr-st .bubble-me{background:var(--primary-soft);color:var(--fg);border-radius:16px 16px 4px 16px;padding:10px 14px;max-width:88%;font-size:14.5px;line-height:1.5;display:flex;flex-direction:column;gap:8px;min-width:0}
.pg-study .sr-st .bubble-me .att{display:inline-flex;align-items:center;gap:6px;font-size:12px;font-weight:600;color:var(--primary-ink)}
.pg-study .sr-st .msg-av{width:34px;height:34px;border-radius:50%;background:var(--card-2);border:1px solid var(--border);display:grid;place-items:center;flex:none;overflow:hidden}
.pg-study .sr-st .msg-av .hoot{margin-top:6px}
.pg-study .sr-st .msg-body{min-width:0;flex:1;display:flex;flex-direction:column;gap:8px}
.pg-study .sr-st .msg-name{font-size:12px;font-weight:700;color:var(--muted)}
.pg-study .sr-st .msg-text{font-size:14.5px;line-height:1.55;color:var(--fg)}
.pg-study .sr-st .blog{list-style:none;margin:0;padding:12px;border:1px solid var(--border);border-radius:14px;background:var(--bg);display:flex;flex-direction:column;gap:11px}
.pg-study .sr-st .blog li{display:grid;grid-template-columns:20px minmax(0,1fr);gap:10px;align-items:start;font-size:13.5px;line-height:1.4;color:var(--muted);transition:color .2s}
.pg-study .sr-st .blog li.run{color:var(--fg)}
.pg-study .sr-st .blog li.done{color:var(--fg-2)}
.pg-study .sr-st .blog .d{display:block;font:500 11.5px/1.4 var(--mono);color:var(--muted);margin-top:2px}
.pg-study .sr-st .st{width:20px;height:20px;border-radius:50%;border:2px solid var(--border-2);display:grid;place-items:center;color:var(--on-primary)}
.pg-study .sr-st .st .i{width:12px;height:12px;stroke-width:3;opacity:0}
.pg-study .sr-st .blog li.run .st{border-color:var(--primary);border-top-color:transparent;animation:st-spin .8s linear infinite}
.pg-study .sr-st .blog li.done .st{background:var(--primary);border-color:var(--primary)}
.pg-study .sr-st .blog li.done .st .i{opacity:1}
@keyframes st-spin{to{transform:rotate(360deg)}}
.pg-study .sr-st .typing{display:inline-flex;gap:4px;padding:10px 12px;border-radius:12px;background:var(--card-2);align-self:flex-start}
.pg-study .sr-st .typing i{width:6px;height:6px;border-radius:50%;background:var(--muted);animation:st-dots 1s infinite}
.pg-study .sr-st .typing i:nth-child(2){animation-delay:.15s}
.pg-study .sr-st .typing i:nth-child(3){animation-delay:.3s}
@keyframes st-dots{0%,100%{opacity:.3;transform:none}50%{opacity:1;transform:translateY(-2px)}}
.pg-study .sr-st .vchip{align-self:flex-start;display:inline-flex;align-items:center;gap:8px;height:30px;padding:0 10px 0 8px;border-radius:9px;border:1px solid var(--border);background:var(--card);font-size:12.5px;font-weight:600;color:var(--fg-2);max-width:100%}
.pg-study .sr-st .vchip .vn{font:700 11px var(--mono);background:var(--card-2);color:var(--fg);padding:2px 6px;border-radius:5px}
.pg-study .sr-st .vchip:hover{border-color:var(--primary)}
.pg-study .sr-st .vchip .viewing{font-size:10px;font-weight:800;letter-spacing:.12em;text-transform:uppercase;color:var(--primary-ink)}
.pg-study .sr-st .vchip.cur{border-color:color-mix(in srgb,var(--primary) 55%,var(--border))}
.pg-study .sr-st .refine-wrap{display:flex;flex-direction:column;gap:8px}
.pg-study .sr-st .refine{display:flex;flex-wrap:wrap;gap:6px}
.pg-study .sr-st .rchip{height:32px;padding:0 12px 0 10px;font-size:13px;gap:6px}
.pg-study .sr-st .rchip .i{width:14px;height:14px;color:var(--primary-ink)}
.pg-study .sr-st .rchip.used{opacity:.55;cursor:default;border-style:dashed}
.pg-study .sr-st .rchip:disabled{cursor:default}
.pg-study .sr-st .chat-form{display:flex;align-items:center;gap:8px;border:1px solid var(--border-2);border-radius:999px;padding:4px 4px 4px 16px;background:var(--card);transition:border-color .15s}
.pg-study .sr-st .chat-form:focus-within{border-color:var(--primary)}
.pg-study .sr-st #chatInput{flex:1;min-width:0;border:0;outline:0;background:transparent;font-size:14.5px;height:38px;color:var(--fg)}
.pg-study .sr-st #chatInput::placeholder{color:var(--muted)}
.pg-study .sr-st .send{width:38px;height:38px;border-radius:50%;background:var(--primary);color:var(--on-primary);display:grid;place-items:center;flex:none}
.pg-study .sr-st .send:hover{background:var(--primary-hov)}
.pg-study .sr-st .send:disabled, .pg-study .sr-st .chat-form.off .send{opacity:.4;cursor:not-allowed}
.pg-study .sr-st .pv-bar{display:flex;align-items:center;gap:10px;padding:10px 12px;border-bottom:1px solid var(--border);flex-wrap:wrap}
.pg-study .sr-st .ver{position:relative;display:inline-flex;align-items:center;gap:8px;height:36px;padding:0 10px;border:1px solid var(--border-2);border-radius:10px;background:var(--card);color:var(--fg-2);min-width:0}
.pg-study .sr-st .ver:focus-within{border-color:var(--primary)}
.pg-study .sr-st .ver select{appearance:none;-webkit-appearance:none;border:0;outline:0;background:transparent;font-size:13.5px;font-weight:700;color:var(--fg);padding-right:20px;cursor:pointer;max-width:230px;text-overflow:ellipsis}
.pg-study .sr-st .ver select option{background:var(--card);color:var(--fg)}
.pg-study .sr-st .ver .chev{position:absolute;right:8px;pointer-events:none;color:var(--muted)}
.pg-study .sr-st .pv-spacer{flex:1}
.pg-study .sr-st .pv-live{display:inline-flex;align-items:center;gap:6px;font-size:12px;font-weight:700;color:var(--muted)}
.pg-study .sr-st .pv-live i{width:7px;height:7px;border-radius:50%;background:var(--success);box-shadow:0 0 0 3px color-mix(in srgb,var(--success) 22%,transparent)}
.pg-study .sr-st .pv-live.building i{background:var(--pin-btn);box-shadow:0 0 0 3px color-mix(in srgb,var(--pin-btn) 25%,transparent)}
.pg-study .sr-st .stage{flex:1;min-height:0;position:relative;display:flex;flex-direction:column;color:var(--stage-fg);
background:radial-gradient(90% 70% at 50% 0%, var(--stage-2) 0%, transparent 70%),var(--stage)}
.pg-study .sr-st .stage-top{display:flex;align-items:center;gap:10px;padding:10px 14px 0;font:600 10.5px/1 var(--mono);letter-spacing:.14em;text-transform:uppercase;color:var(--stage-muted);flex-wrap:wrap;min-height:28px}
.pg-study .sr-st .stage-top .ok{color:var(--stage-accent)}
.pg-study .sr-st .stage-top .bld{color:#7fcdea}
.pg-study .sr-st .ftag{border:1px solid var(--stage-line);border-radius:5px;padding:3px 6px;color:#bcd2d3;letter-spacing:.08em}
.pg-study .sr-st .ftag.new{border-color:var(--stage-accent);color:var(--stage-accent)}
.pg-study .sr-st .stage-prog{position:absolute;left:0;right:0;top:0;height:2px;background:transparent}
.pg-study .sr-st .stage-prog i{display:block;height:100%;width:0;background:linear-gradient(90deg,#5fc8e8,var(--stage-accent));transition:width .5s ease}
.pg-study .sr-st .stage-center{flex:1;min-height:0;display:flex;justify-content:center;align-items:stretch;padding:12px 14px}
.pg-study .sr-st .device{width:100%;max-width:920px;min-height:0;display:flex;position:relative}
.pg-study .sr-st .device.phone{width:min(330px,100%);height:min(640px,100%);align-self:center;border:9px solid #03080a;border-radius:40px;box-shadow:0 0 0 1px #2a3f45,0 30px 60px rgba(0,0,0,.5);overflow:hidden}
.pg-study .sr-st .device.phone::before{content:"";position:absolute;top:8px;left:50%;width:84px;height:20px;border-radius:12px;background:#03080a;transform:translateX(-50%);z-index:20}
.pg-study .sr-st .device.phone .arena{border-radius:30px;padding-top:24px}
.pg-study .sr-st .stage-foot{display:flex;align-items:center;gap:12px;padding:0 14px 10px;font:600 10.5px/1 var(--mono);letter-spacing:.12em;text-transform:uppercase;color:var(--stage-muted);flex-wrap:wrap}
.pg-study .sr-st .stage-foot button{margin-left:auto;display:inline-flex;align-items:center;gap:6px;color:#b8cfd0;font:600 11.5px/1 var(--body);letter-spacing:0;text-transform:none;padding:6px 8px;border-radius:7px}
.pg-study .sr-st .stage-foot button:hover{background:rgba(255,255,255,.06);color:#fff}
.pg-study .sr-st .stage-foot button .i{width:13px;height:13px}
.pg-study .sr-st .arena{position:relative;flex:1;min-width:0;min-height:0;container:arena/inline-size;border-radius:16px;overflow:auto;overflow-x:hidden;scrollbar-width:thin;
background:radial-gradient(120% 90% at 50% 0%, #0b2730 0%, #071219 62%);border:1px solid #17323a;color:#e9f2f1;display:flex;flex-direction:column}
.pg-study .sr-st .bp-grid{position:absolute;inset:0;pointer-events:none;opacity:0;transition:opacity 1.1s ease;z-index:0;
background-color:#06182a;
background-image:linear-gradient(rgba(95,200,232,.08) 1px,transparent 1px),linear-gradient(90deg,rgba(95,200,232,.08) 1px,transparent 1px),linear-gradient(rgba(95,200,232,.16) 1px,transparent 1px),linear-gradient(90deg,rgba(95,200,232,.16) 1px,transparent 1px);
background-size:20px 20px,20px 20px,100px 100px,100px 100px}
.pg-study .sr-st .bp-grid span{position:absolute;left:12px;bottom:10px;font:600 9.5px/1 var(--mono);letter-spacing:.16em;color:rgba(140,210,236,.7);text-transform:uppercase}
.pg-study .sr-st .bp-grid span + span{left:auto;right:12px}
.pg-study .sr-st .arena.building .bp-grid{opacity:1}
.pg-study .sr-st .arena.building::after{content:"";position:absolute;left:0;right:0;height:90px;top:-90px;z-index:6;pointer-events:none;background:linear-gradient(transparent,rgba(95,200,232,.12));border-bottom:1px solid rgba(120,215,245,.5);animation:st-scan 2.4s linear infinite}
@keyframes st-scan{to{top:100%}}
.pg-study .sr-st .arena > *:not(.bp-grid):not(.ar-end){position:relative;z-index:1}
.pg-study .sr-st .arena [data-part].bp{position:relative}
.pg-study .sr-st .arena [data-part].bp > *{visibility:hidden}
.pg-study .sr-st .arena [data-part].bp::after{content:attr(data-label);position:absolute;inset:0;z-index:3;border:1.5px dashed rgba(110,200,235,.55);border-radius:12px;background:rgba(95,200,232,.045);
font:600 9.5px/1.2 var(--mono);letter-spacing:.16em;color:rgba(150,215,240,.85);padding:7px 9px;text-transform:uppercase;pointer-events:none;display:block}
.pg-study .sr-st .arena .ar-bg.bp::after{inset:8px;border-color:rgba(110,200,235,.25);background:none}
.pg-study .sr-st .arena .ar-q.bp::before{content:"";position:absolute;left:14px;right:28%;top:32px;height:30px;z-index:3;
background:linear-gradient(rgba(110,200,235,.2),rgba(110,200,235,.2)) 0 0/100% 9px no-repeat,linear-gradient(rgba(110,200,235,.2),rgba(110,200,235,.2)) 0 17px/64% 9px no-repeat;border-radius:3px}
.pg-study .sr-st .arena .ans.bp::after{display:none}
.pg-study .sr-st .arena .ans.bp > *{visibility:visible}
.pg-study .sr-st .arena .ans.bp .aopt{border-style:dashed;border-color:rgba(110,200,235,.5);background:rgba(95,200,232,.04);color:transparent;pointer-events:none}
.pg-study .sr-st .arena .ans.bp .aopt .k{background:transparent;border:1px dashed rgba(110,200,235,.6);color:rgba(150,215,240,.85)}
.pg-study .sr-st .arena .ar-boss.bp::after{border:0;background:none}
.pg-study .sr-st .arena .ar-boss.bp > .boss-wire{visibility:visible}
.pg-study .sr-st .mat{animation:st-mat .8s cubic-bezier(.2,.7,.2,1)}
@keyframes st-mat{0%{clip-path:inset(0 0 100% 0);filter:brightness(2) saturate(.2)}70%{filter:brightness(1.2)}100%{clip-path:inset(0 0 0 0);filter:none}}
.pg-study .sr-st .mat-up{animation:st-matUp 1.1s cubic-bezier(.2,.7,.2,1)}
@keyframes st-matUp{0%{clip-path:inset(100% 0 0 0);filter:brightness(2.2) saturate(0)}75%{filter:brightness(1.25)}100%{clip-path:inset(0 0 0 0);filter:none}}
.pg-study .sr-st .aopt.verified::after{content:"";position:absolute;right:10px;top:50%;width:18px;height:18px;margin-top:-9px;border-radius:50%;background:var(--stage-accent);
-webkit-mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Ccircle cx='12' cy='12' r='12' fill='black'/%3E%3Cpath d='M7 12.5l3.2 3.2L17 9' stroke='white' stroke-width='2.6' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center/contain no-repeat,linear-gradient(#000,#000);
-webkit-mask-composite:xor;mask-composite:exclude;animation:st-verify 1.1s ease forwards}
@keyframes st-verify{0%{opacity:0;transform:scale(.4)}25%{opacity:1;transform:scale(1.1)}70%{opacity:1;transform:scale(1)}100%{opacity:0}}
.pg-study .sr-st .ar-bg{position:absolute!important;inset:0;z-index:0!important;pointer-events:none}
.pg-study .sr-st .ar-bg .glow{position:absolute;left:4%;top:18%;width:46%;height:70%;background:radial-gradient(closest-side,rgba(120,98,214,.28),transparent)}
.pg-study .sr-st .ar-bg .glow2{position:absolute;right:0;top:0;width:60%;height:50%;background:radial-gradient(closest-side,rgba(47,208,184,.10),transparent)}
.pg-study .sr-st .ar-bg .floor{position:absolute;left:3%;width:40%;bottom:9%;height:16%;border-radius:50%;background:radial-gradient(closest-side,rgba(242,195,91,.14),rgba(47,208,184,.05) 60%,transparent);border:1px solid rgba(242,195,91,.12)}
.pg-study .sr-st .ar-hud{display:flex;align-items:center;gap:14px 18px;padding:14px 16px 8px;flex-wrap:wrap}
.pg-study .sr-st .hud-l{display:flex;align-items:center;gap:12px}
.pg-study .sr-st .round-chip{font:700 10.5px/1 var(--mono);letter-spacing:.14em;text-transform:uppercase;padding:6px 9px;border-radius:7px;background:rgba(255,255,255,.06);border:1px solid #23404a;color:#cfe3e1;white-space:nowrap}
.pg-study .sr-st .round-chip.r3{color:#ffb38a;border-color:#6a3524;background:rgba(255,120,80,.08)}
.pg-study .sr-st .hearts{display:inline-flex;gap:3px}
.pg-study .sr-st .hearts svg{width:19px;height:19px;fill:#ff6f7d;stroke:#ff6f7d;stroke-width:1.5}
.pg-study .sr-st .hearts svg.lost{fill:none;stroke:#4a5f66}
.pg-study .sr-st .hearts svg.pop{animation:st-heartPop .5s ease}
@keyframes st-heartPop{40%{transform:scale(1.5)}}
.pg-study .sr-st .hud-boss{flex:1;min-width:180px;max-width:380px;margin-left:auto;display:flex;flex-direction:column;gap:6px}
.pg-study .sr-st .hp-top{display:flex;align-items:baseline;gap:10px}
.pg-study .sr-st .hp-name{font-family:var(--display);font-weight:700;font-size:14.5px;color:#fff;white-space:nowrap}
.pg-study .sr-st .hp-num{margin-left:auto;font:600 11.5px var(--mono);color:#9fb9bb}
.pg-study .sr-st .hp{height:10px;border-radius:6px;background:#15282f;border:1px solid #23404a;overflow:hidden}
.pg-study .sr-st .hp i{display:block;height:100%;width:100%;border-radius:6px;background:linear-gradient(90deg,#f2c35b,#ff7a4d);transition:width .6s cubic-bezier(.3,.8,.3,1)}
.pg-study .sr-st .ar-body{flex:1;display:grid;grid-template-columns:minmax(0,.85fr) minmax(0,1.15fr);gap:12px 20px;padding:4px 16px 16px;align-items:center}
.pg-study .sr-st .ar-boss{position:relative;display:grid;place-items:center;min-height:210px;align-self:stretch}
.pg-study .sr-st .boss-wire, .pg-study .sr-st .boss-real{grid-area:1/1;width:100%;display:grid;place-items:center}
.pg-study .sr-st .golem{width:min(250px,100%);height:auto;overflow:visible}
.pg-study .sr-st .boss-wire .golem *{fill:none!important;stroke:rgba(120,205,240,.8)!important;stroke-width:1.2px!important;stroke-dasharray:4 3}
.pg-study .sr-st .ar-boss:not(.bp) .boss-wire{opacity:0;transition:opacity 1s ease .5s}
.pg-study .sr-st .boss-real .golem{animation:st-breathe 3.6s ease-in-out infinite;transform-origin:50% 95%}
@keyframes st-breathe{50%{transform:scale(1.015,1.03)}}
.pg-study .sr-st .boss-real.hit .golem{animation:st-hit .5s ease}
@keyframes st-hit{0%{filter:brightness(3) saturate(0)}20%{transform:translateX(-7px) rotate(-2deg)}40%{transform:translateX(6px) rotate(2deg)}60%{transform:translateX(-4px)}100%{filter:none;transform:none}}
.pg-study .sr-st .boss-real.lunge .golem{animation:st-lunge .55s ease}
@keyframes st-lunge{40%{transform:translate(14px,-6px) scale(1.08)}}
.pg-study .sr-st .boss-real.rage .g-eye{fill:#ff6a4d}
.pg-study .sr-st .boss-real.rage .g-crack{stroke:#ff8a5c;opacity:.95}
.pg-study .sr-st .boss-real.rage .golem{filter:drop-shadow(0 0 14px rgba(255,106,77,.25))}
.pg-study .sr-st .boss-real.down .golem{animation:none;transform:translateY(18px) rotate(-4deg);filter:grayscale(1) brightness(.5);opacity:.5;transition:all 1s ease}
.pg-study .sr-st .dmg{position:absolute;left:50%;top:30%;font:800 26px/1 var(--display);color:#f2c35b;text-shadow:0 2px 12px rgba(0,0,0,.6);pointer-events:none;animation:st-dmg 1s ease forwards;z-index:4}
.pg-study .sr-st .dmg.bad{color:#ff6f7d}
@keyframes st-dmg{0%{opacity:0;transform:translate(-50%,10px) scale(.7)}20%{opacity:1;transform:translate(-50%,-6px) scale(1.15)}100%{opacity:0;transform:translate(-50%,-46px)}}
.pg-study .sr-st .arena.hurt::before{content:"";position:absolute;inset:0;z-index:8;pointer-events:none;box-shadow:inset 0 0 70px rgba(255,70,70,.45);animation:st-hurt .6s ease forwards}
@keyframes st-hurt{to{opacity:0}}
.pg-study .sr-st .ar-play{display:flex;flex-direction:column;gap:10px;min-width:0}
.pg-study .sr-st .ar-q{border:1px solid #1f3a42;background:rgba(255,255,255,.03);border-radius:14px;padding:12px 14px;display:flex;flex-direction:column;gap:8px;min-height:96px}
.pg-study .sr-st .q-meta{display:flex;justify-content:space-between;gap:10px;font:600 10px/1 var(--mono);letter-spacing:.14em;text-transform:uppercase;color:#7fe0cf}
.pg-study .sr-st .q-meta span:last-child{color:#7c9599}
.pg-study .sr-st .q-text{font-family:var(--display);font-weight:700;font-size:clamp(16px,2.6cqi,20px);line-height:1.3;color:#fff;letter-spacing:-.01em}
.pg-study .sr-st .q-hint{font-size:13px;color:#f2c35b;display:flex;gap:6px;align-items:flex-start}
.pg-study .sr-st .q-hint .i{width:14px;height:14px;margin-top:2px}
.pg-study .sr-st .q-timer{display:flex;align-items:center;gap:8px}
.pg-study .sr-st .q-timer .tr{flex:1;height:6px;border-radius:4px;background:#15282f;overflow:hidden}
.pg-study .sr-st .q-timer .tr i{display:block;height:100%;width:100%;background:var(--stage-accent);border-radius:4px;transition:width .2s linear,background .3s}
.pg-study .sr-st .q-timer .tr i.low{background:#ff7a4d}
.pg-study .sr-st .q-timer span{font:700 11.5px var(--mono);color:#cfe3e1;min-width:28px;text-align:right}
.pg-study .sr-st .ans{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}
.pg-study .sr-st .aopt{position:relative;display:flex;align-items:center;gap:10px;min-height:50px;padding:9px 12px;border-radius:12px;border:1px solid #24414a;background:rgba(255,255,255,.035);color:#e9f2f1;font-weight:600;font-size:14px;line-height:1.3;text-align:left;transition:border-color .15s,background .15s,opacity .2s;min-width:0}
.pg-study .sr-st .aopt .t{min-width:0}
.pg-study .sr-st .aopt:hover:not(:disabled){border-color:var(--stage-accent);background:rgba(47,208,184,.08)}
.pg-study .sr-st .aopt:focus-visible{outline:2px solid var(--stage-accent);outline-offset:2px}
.pg-study .sr-st .aopt .k{width:24px;height:24px;border-radius:7px;background:#16303a;color:var(--stage-accent);font:700 11.5px/1 var(--mono);display:grid;place-items:center;flex:none}
.pg-study .sr-st .aopt:disabled{cursor:default}
.pg-study .sr-st .aopt.good{border-color:#3fbf85;background:rgba(63,191,133,.16)}
.pg-study .sr-st .aopt.good .k{background:#3fbf85;color:#062114}
.pg-study .sr-st .aopt.bad{border-color:#e05a5a;background:rgba(224,90,90,.15);animation:st-nope .35s}
.pg-study .sr-st .aopt.bad .k{background:#e05a5a;color:#2a0707}
.pg-study .sr-st .aopt.dim{opacity:.45}
@keyframes st-nope{25%{transform:translateX(-5px)}75%{transform:translateX(5px)}}
.pg-study .sr-st .fb{border-radius:14px;padding:12px 14px;display:flex;flex-direction:column;gap:8px;border:1px solid #2a4a3f;background:rgba(63,191,133,.08);animation:st-viewIn .25s ease}
.pg-study .sr-st .fb.bad{border-color:#5a2a2e;background:rgba(224,90,90,.08)}
.pg-study .sr-st .fb-head{display:flex;align-items:center;gap:8px;font-weight:700;font-size:14.5px;color:#fff}
.pg-study .sr-st .fb-head .i{width:18px;height:18px;color:#5fdca0}
.pg-study .sr-st .fb.bad .fb-head .i{color:#ff8a8a}
.pg-study .sr-st .fb-head em{font-style:normal;margin-left:auto;font:700 11.5px var(--mono);color:#f2c35b}
.pg-study .sr-st .fb.bad .fb-head em{color:#ff8a8a}
.pg-study .sr-st .fb-text{font-size:13.5px;line-height:1.5;color:#cfe0df}
.pg-study .sr-st .fb-why{font-size:13.5px;line-height:1.55;color:#e3eeed;border-left:2px solid var(--stage-gold);padding:2px 0 2px 10px}
.pg-study .sr-st .fb-why b{display:block;font:700 10px/1.6 var(--mono);letter-spacing:.16em;text-transform:uppercase;color:var(--stage-gold)}
.pg-study .sr-st .fb-foot{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
.pg-study .sr-st .cite{display:inline-flex;align-items:center;gap:6px;font-size:12px;font-weight:600;color:#9fd9cf;border:1px solid #23404a;border-radius:7px;padding:4px 8px;background:rgba(47,208,184,.06)}
.pg-study .sr-st .cite .i{width:13px;height:13px}
.pg-study .sr-st .sbtn{display:inline-flex;align-items:center;justify-content:center;gap:8px;height:38px;padding:0 16px;border-radius:999px;font-weight:700;font-size:14px;background:var(--stage-accent);color:#052a24;white-space:nowrap}
.pg-study .sr-st .sbtn:hover{background:#4fe0ca}
.pg-study .sr-st .sbtn .i{width:16px;height:16px}
.pg-study .sr-st .sbtn.ghost{background:transparent;color:#fff;border:1px solid rgba(255,255,255,.24)}
.pg-study .sr-st .sbtn.ghost:hover{background:rgba(255,255,255,.08)}
.pg-study .sr-st .sbtn:focus-visible{outline:2px solid #fff;outline-offset:2px}
.pg-study .sr-st .fb-foot .sbtn{margin-left:auto}
.pg-study .sr-st .ar-end{position:absolute;inset:0;z-index:12;display:grid;place-items:center;padding:20px;background:rgba(4,11,15,.86);backdrop-filter:blur(3px);animation:st-viewIn .35s ease;overflow:auto}
.pg-study .sr-st .end-card{max-width:400px;width:100%;text-align:center;display:flex;flex-direction:column;align-items:center;gap:10px}
.pg-study .sr-st .end-card .eb{font:700 10.5px/1 var(--mono);letter-spacing:.2em;text-transform:uppercase;color:var(--stage-accent)}
.pg-study .sr-st .end-card.lose .eb{color:#ff8a8a}
.pg-study .sr-st .end-card h3{font-size:clamp(22px,4cqi,28px);font-weight:800;color:#fff}
.pg-study .sr-st .end-card p{font-size:14px;color:#b9cfd0}
.pg-study .sr-st .end-stats{display:flex;gap:8px;flex-wrap:wrap;justify-content:center;margin:4px 0}
.pg-study .sr-st .end-stats span{font:700 11.5px var(--mono);color:#e9f2f1;background:rgba(255,255,255,.06);border:1px solid #23404a;border-radius:7px;padding:5px 8px}
.pg-study .sr-st .end-miss{width:100%;text-align:left;border:1px solid #23404a;border-radius:12px;padding:10px 12px;display:flex;flex-direction:column;gap:6px;font-size:13px;color:#cfe0df}
.pg-study .sr-st .end-miss b{font:700 10px var(--mono);letter-spacing:.16em;text-transform:uppercase;color:#7c9599}
.pg-study .sr-st .end-miss div{display:flex;gap:8px;justify-content:space-between}
.pg-study .sr-st .end-miss div span:last-child{color:#9fd9cf;white-space:nowrap}
.pg-study .sr-st .end-btns{display:flex;gap:8px;flex-wrap:wrap;justify-content:center;margin-top:6px}
.pg-study .sr-st .end-trophy{width:54px;height:54px;border-radius:50%;display:grid;place-items:center;background:rgba(242,195,91,.12);color:#f2c35b;border:1px solid rgba(242,195,91,.3)}
.pg-study .sr-st .end-card.lose .end-trophy{background:rgba(224,90,90,.12);color:#ff8a8a;border-color:rgba(224,90,90,.3)}
@container arena (max-width:560px){.pg-study .sr-st .ar-body{grid-template-columns:1fr;padding:0 12px 14px}
.pg-study .sr-st .ar-boss{min-height:0;align-self:auto}
.pg-study .sr-st .golem{width:min(150px,60%)}
.pg-study .sr-st .ar-hud{padding:12px 12px 6px}
.pg-study .sr-st .hud-boss{max-width:none;min-width:100%;order:-1}}
@container arena (max-width:420px){.pg-study .sr-st .ans{grid-template-columns:1fr}
.pg-study .sr-st .aopt{min-height:44px}}
.pg-study .sr-st .tl-head, .pg-study .sr-st .tf-head{display:flex;align-items:center;gap:12px;padding:14px 16px 10px;flex-wrap:wrap}
.pg-study .sr-st .tl-ic{width:38px;height:38px;border-radius:11px;display:grid;place-items:center;flex:none;color:#fff;background:linear-gradient(145deg,#4b8ff0,#2a5bc4);box-shadow:inset 0 1px 0 rgba(255,255,255,.25)}
.pg-study .sr-st .tf-head .tl-ic{background:linear-gradient(145deg,#f0a43a,#d27716)}
.pg-study .sr-st .tl-name{display:flex;flex-direction:column;min-width:0;flex:1}
.pg-study .sr-st .tl-name b{font-family:var(--display);font-size:16px;color:#fff}
.pg-study .sr-st .tl-name span{font-size:12px;color:#8fa9ab}
.pg-study .sr-st .ring{--p:0;width:52px;height:52px;border-radius:50%;display:grid;place-items:center;flex:none;background:conic-gradient(var(--stage-accent) calc(var(--p)*1%),#1b3036 0);position:relative}
.pg-study .sr-st .ring::before{content:"";position:absolute;inset:5px;border-radius:50%;background:#0a1a20}
.pg-study .sr-st .ring span{position:relative;font:700 12px var(--mono);color:#fff}
.pg-study .sr-st .tl-body{flex:1;display:grid;grid-template-columns:minmax(0,1.05fr) minmax(0,1fr);gap:12px;padding:0 16px 16px;align-items:start}
.pg-study .sr-st .tl-draft{border:1px solid #1f3a42;border-radius:14px;background:#f4f1e8;color:#1d2a2e;padding:14px 16px;display:flex;flex-direction:column;gap:10px;font-size:13px;line-height:1.55}
.pg-study .sr-st .arena .tl-draft.bp{background:transparent}
.pg-study .sr-st .tl-draft .dh{display:flex;justify-content:space-between;gap:8px;font:700 10px/1.3 var(--mono);letter-spacing:.12em;text-transform:uppercase;color:#6a7a7d}
.pg-study .sr-st .tl-draft h4{font-family:var(--display);font-size:15px;color:#13232a}
.pg-study .sr-st .tl-draft section b{display:block;font:700 10px/1.4 var(--mono);letter-spacing:.14em;text-transform:uppercase;color:#4c6a6e}
.pg-study .sr-st .tl-draft mark{background:transparent;color:inherit;border-radius:3px;transition:background .25s,box-shadow .25s}
.pg-study .sr-st .tl-draft mark.on{background:#ffe3a3;box-shadow:0 0 0 2px #ffe3a3}
.pg-study .sr-st .tl-draft mark.on.fail{background:#ffd0cc;box-shadow:0 0 0 2px #ffd0cc}
.pg-study .sr-st .tl-side{display:flex;flex-direction:column;gap:10px;min-width:0}
.pg-study .sr-st .tl-sum{border:1px solid #1f3a42;border-radius:14px;padding:12px 14px;background:rgba(255,255,255,.03);display:flex;flex-direction:column;gap:8px}
.pg-study .sr-st .tl-sum p{font-size:14px;font-weight:700;color:#fff}
.pg-study .sr-st .tl-sum span{font-size:12.5px;color:#8fa9ab}
.pg-study .sr-st .tl-bar{height:6px;border-radius:4px;background:#15282f;overflow:hidden}
.pg-study .sr-st .tl-bar i{display:block;height:100%;width:0;background:linear-gradient(90deg,#3fbf85,var(--stage-accent));transition:width .5s}
.pg-study .sr-st .tl-checks{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:6px}
.pg-study .sr-st .tl-check{border:1px solid #1f3a42;border-radius:12px;background:rgba(255,255,255,.025);overflow:hidden}
.pg-study .sr-st .tl-check > button{width:100%;display:flex;align-items:flex-start;gap:10px;padding:10px 12px;text-align:left;color:#e3eeed;font-size:13.5px;font-weight:600;line-height:1.35}
.pg-study .sr-st .tl-check > button:focus-visible{outline:2px solid var(--stage-accent);outline-offset:-2px}
.pg-study .sr-st .tl-dot{width:20px;height:20px;border-radius:50%;flex:none;display:grid;place-items:center;border:2px solid #2c4850;margin-top:-1px}
.pg-study .sr-st .tl-dot .i{width:12px;height:12px;stroke-width:3}
.pg-study .sr-st .tl-check.pass .tl-dot{background:#3fbf85;border-color:#3fbf85;color:#062114}
.pg-study .sr-st .tl-check.fail .tl-dot{background:#e05a5a;border-color:#e05a5a;color:#2a0707}
.pg-study .sr-st .tl-check.warn .tl-dot{background:#f2c35b;border-color:#f2c35b;color:#2a1d05}
.pg-study .sr-st .tl-check.running .tl-dot{border-color:var(--stage-accent);border-top-color:transparent;animation:st-spin .7s linear infinite}
.pg-study .sr-st .tl-check .rl{margin-left:auto;font:600 10.5px/1.4 var(--mono);color:#7c9599;white-space:nowrap;padding-left:6px}
.pg-study .sr-st .tl-check.new-item{border-color:var(--stage-accent)}
.pg-study .sr-st .tl-detail{padding:0 12px 12px 42px;display:flex;flex-direction:column;gap:8px;font-size:13px;line-height:1.5;color:#cfe0df}
.pg-study .sr-st .tl-detail .quote{border-left:2px solid #5fc8e8;padding-left:10px;color:#b8dcea;font-style:italic}
.pg-study .sr-st .tl-detail .fix{border:1px dashed #3a5a62;border-radius:9px;padding:8px 10px;color:#e3eeed}
.pg-study .sr-st .tl-detail .fix b{display:block;font:700 10px/1.6 var(--mono);letter-spacing:.14em;text-transform:uppercase;color:var(--stage-accent)}
.pg-study .sr-st .tl-run{display:flex;gap:8px;flex-wrap:wrap}
@container arena (max-width:600px){.pg-study .sr-st .tl-body{grid-template-columns:1fr;padding:0 12px 14px}
.pg-study .sr-st .tl-side{order:-1}}
.pg-study .sr-st .tf-prog{font:700 12px var(--mono);color:#cfe3e1;background:rgba(255,255,255,.06);border:1px solid #23404a;border-radius:7px;padding:5px 8px}
.pg-study .sr-st .tf-body{flex:1;display:grid;grid-template-columns:minmax(0,210px) minmax(0,1fr);gap:12px;padding:0 16px 16px;align-items:start}
.pg-study .sr-st .tf-list{list-style:none;margin:0;padding:10px;border:1px solid #1f3a42;border-radius:14px;background:rgba(255,255,255,.025);display:flex;flex-direction:column;gap:4px}
.pg-study .sr-st .tf-list .tfp{font:700 9.5px/1 var(--mono);letter-spacing:.16em;text-transform:uppercase;color:#7c9599;padding:8px 6px 4px}
.pg-study .sr-st .tf-list li.f{display:flex;align-items:center;gap:8px;padding:7px 8px;border-radius:8px;font-size:13px;color:#b9cfd0}
.pg-study .sr-st .tf-list li.f.cur{background:rgba(47,208,184,.1);color:#fff}
.pg-study .sr-st .tf-list li.f i{width:9px;height:9px;border-radius:50%;border:2px solid #3a5a62;flex:none}
.pg-study .sr-st .tf-list li.f.ok i{background:#3fbf85;border-color:#3fbf85}
.pg-study .sr-st .tf-list li.f.miss i{background:#f2c35b;border-color:#f2c35b}
.pg-study .sr-st .tf-main{display:flex;flex-direction:column;gap:10px;min-width:0}
.pg-study .sr-st .tf-card{border:1px solid #1f3a42;border-radius:14px;padding:14px 16px;background:rgba(255,255,255,.03);display:flex;flex-direction:column;gap:10px}
.pg-study .sr-st .tf-formula{font:700 clamp(22px,4.4cqi,32px)/1.2 var(--mono);color:#fff;letter-spacing:-.01em;word-break:break-word}
.pg-study .sr-st .tf-formula .blank{display:inline-block;min-width:2.6em;border-bottom:3px solid var(--stage-accent);color:var(--stage-accent);text-align:center}
.pg-study .sr-st .tf-word{font-family:var(--display);font-weight:700;font-size:17px;line-height:1.35;color:#fff}
.pg-study .sr-st .tf-units{font:600 12px var(--mono);color:#9fd9cf;display:flex;gap:6px;flex-wrap:wrap}
.pg-study .sr-st .tf-units span{border:1px solid #23404a;border-radius:6px;padding:3px 6px}
@container arena (max-width:600px){.pg-study .sr-st .tf-body{grid-template-columns:1fr;padding:0 12px 14px}
.pg-study .sr-st .tf-list{flex-direction:row;flex-wrap:wrap}
.pg-study .sr-st .tf-list .tfp{display:none}
.pg-study .sr-st .tf-list li.f{font-size:12px;padding:5px 7px}}
.pg-study .sr-st .row-empty{color:var(--muted);font-size:14px;padding:6px 0}
@keyframes st-fadeIn{from{opacity:0}}
@keyframes st-sheetIn{from{transform:translateX(40px);opacity:.4}}
.pg-study .sr-st .pub-form{display:flex;flex-direction:column;gap:22px}
.pg-study .sr-st .fld{display:flex;flex-direction:column;gap:8px;border:0;margin:0;padding:0;min-width:0}
.pg-study .sr-st .fld > label, .pg-study .sr-st .fld > legend{font-size:11px;font-weight:800;letter-spacing:.18em;text-transform:uppercase;color:var(--muted);padding:0;margin-bottom:0}
.pg-study .sr-st .fld legend{margin-bottom:8px}
.pg-study .sr-st .inp{height:46px;border:1px solid var(--border-2);border-radius:12px;background:var(--card);padding:0 14px;font-size:15px;color:var(--fg);outline:0;width:100%;min-width:0;transition:border-color .15s,box-shadow .15s}
.pg-study .sr-st .inp:focus{border-color:var(--primary);box-shadow:0 0 0 3px color-mix(in srgb,var(--primary) 15%,transparent)}
.pg-study .sr-st .fld-err{font-size:12.5px;color:var(--danger);font-weight:600}
.pg-study .sr-st .covers{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px}
.pg-study .sr-st .cover{position:relative;display:flex;flex-direction:column;gap:6px;cursor:pointer;min-width:0}
.pg-study .sr-st .cover input{position:absolute;opacity:0;pointer-events:none}
.pg-study .sr-st .cover .thumb{border-radius:10px;transition:box-shadow .15s}
.pg-study .sr-st .cover span.cap{font-size:12.5px;font-weight:600;color:var(--muted)}
.pg-study .sr-st .cover:has(input:checked) .thumb{box-shadow:0 0 0 2px var(--card),0 0 0 4px var(--primary)}
.pg-study .sr-st .cover:has(input:checked) span.cap{color:var(--primary-ink)}
.pg-study .sr-st .cover:has(input:focus-visible) .thumb{outline:2px solid var(--primary);outline-offset:4px}
.pg-study .sr-st .cover .ck{position:absolute;top:6px;left:6px;z-index:3;width:22px;height:22px;border-radius:50%;background:var(--primary);color:var(--on-primary);display:none;place-items:center}
.pg-study .sr-st .cover .ck .i{width:13px;height:13px;stroke-width:3}
.pg-study .sr-st .cover:has(input:checked) .ck{display:grid}
.pg-study .sr-st .shelf{display:flex;flex-wrap:wrap;gap:8px}
.pg-study .sr-st .shelf label{cursor:pointer;position:relative}
.pg-study .sr-st .shelf input{position:absolute;opacity:0;pointer-events:none}
.pg-study .sr-st .shelf label:has(input:checked){background:var(--primary-soft);border-color:var(--primary);color:var(--primary-ink)}
.pg-study .sr-st .shelf label:has(input:focus-visible){outline:2px solid var(--primary);outline-offset:2px}
.pg-study .sr-st .vis{display:flex;flex-direction:column;gap:8px}
.pg-study .sr-st .vis label{display:flex;align-items:flex-start;gap:12px;padding:12px 14px;border:1px solid var(--border);border-radius:14px;cursor:pointer;transition:border-color .15s,background .15s}
.pg-study .sr-st .vis label:hover{border-color:var(--border-2)}
.pg-study .sr-st .vis label:has(input:checked){border-color:var(--primary);background:var(--primary-soft)}
.pg-study .sr-st .vis input{margin:3px 0 0;width:18px;height:18px;accent-color:var(--primary);flex:none}
.pg-study .sr-st .vis .vi{width:32px;height:32px;border-radius:9px;display:grid;place-items:center;background:var(--card-2);color:var(--fg-2);flex:none}
.pg-study .sr-st .vis label:has(input:checked) .vi{background:var(--card);color:var(--primary-ink)}
.pg-study .sr-st .vis b{display:block;font-size:14.5px}
.pg-study .sr-st .vis span.s{display:block;font-size:13px;color:var(--muted);margin-top:1px}
.pg-study .sr-st .checks{border:1px solid var(--border);border-radius:16px;background:var(--card-2);padding:14px 16px;display:flex;flex-direction:column;gap:12px}
.pg-study .sr-st .checks-head{display:flex;align-items:center;gap:10px}
.pg-study .sr-st .checks-head .hoot{margin-bottom:-4px}
.pg-study .sr-st .checks-head b{font-family:var(--display);font-size:16px}
.pg-study .sr-st .checks-head span{margin-left:auto;font-size:12.5px;font-weight:700;color:var(--muted)}
.pg-study .sr-st .checks-head span.ok{color:var(--tb-ink)}
.pg-study .sr-st .checks ul{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:10px}
.pg-study .sr-st .checks li{display:grid;grid-template-columns:20px minmax(0,1fr);gap:10px;font-size:14px;font-weight:600;color:var(--fg)}
.pg-study .sr-st .checks li small{display:block;font-size:12.5px;font-weight:500;color:var(--muted)}
.pg-study .sr-st .checks li.wait{color:var(--muted)}
.pg-study .sr-st .checks .note{font-size:12.5px;color:var(--muted);display:flex;gap:6px;align-items:flex-start;border-top:1px solid var(--border);padding-top:10px}
.pg-study .sr-st .checks .note .i{width:14px;height:14px;margin-top:2px}
.pg-study .sr-st .btn:disabled{opacity:.55;cursor:not-allowed;box-shadow:none}
.pg-study .sr-st .done{display:flex;flex-direction:column;gap:20px}
.pg-study .sr-st .done-top{display:flex;gap:14px;align-items:flex-start}
.pg-study .sr-st .done-top h2{font-size:24px;font-weight:800;margin-top:6px}
.pg-study .sr-st .done-top p{color:var(--muted);font-size:14.5px;margin-top:6px}
.pg-study .sr-st .done-eb{font-size:11px;font-weight:800;letter-spacing:.2em;text-transform:uppercase;color:var(--tb-ink);display:inline-flex;align-items:center;gap:6px}
.pg-study .sr-st .done-preview{border:1px solid var(--border);border-radius:16px;padding:16px;background:var(--bg);display:flex;flex-direction:column;gap:12px}
.pg-study .sr-st .done-tile{max-width:300px}
.pg-study .sr-st .copy-row{display:flex;gap:8px}
.pg-study .sr-st .copy-row .inp{font-family:var(--mono);font-size:13px}
.pg-study .sr-st .hint{font-size:12.5px;color:var(--muted);display:flex;gap:6px;align-items:center}
.pg-study .sr-st .done-actions{display:flex;gap:10px;flex-wrap:wrap}
@media (max-width:1180px){.pg-study .sr-st .speech::before{top:22px;left:-7px;transform:rotate(-45deg)}
.pg-study .sr-st .trust{flex:1;min-width:240px;padding-top:6px}
.pg-study .sr-st .studio{grid-template-columns:1fr;height:auto}
.pg-study .sr-st .pane-preview{order:-1}
.pg-study .sr-st .stage{min-height:560px}
.pg-study .sr-st .device:not(.phone){min-height:520px}
.pg-study .sr-st .chat-log{max-height:none;overflow:visible}}
@media (max-width:820px){.pg-study .sr-st .how{grid-template-columns:1fr}
.pg-study .sr-st .how-step + .how-step{border-left:0;border-top:1px solid var(--border)}}
@media (max-width:600px){@keyframes st-sheetUp{from{transform:translateY(40px);opacity:.4}}
.pg-study .sr-st .covers{gap:8px}
.pg-study .sr-st .cover span.cap{font-size:11.5px}}
@media (max-width:520px){.pg-study .sr-st .speech{font-size:14px}
.pg-study .sr-st .mine{grid-template-columns:1fr}
.pg-study .sr-st .mine-card .thumb{width:108px}
.pg-study .sr-st .stage{min-height:0}
.pg-study .sr-st .stage-center{padding:10px 8px}
.pg-study .sr-st .device:not(.phone){min-height:0}
.pg-study .sr-st .pv-bar .ver select{max-width:150px}
.pg-study .sr-st .pv-live{display:none}
.pg-study .sr-st .st-title h1{font-size:21px}}
/* Community */
.pg-study .sr-cm button, .pg-study .sr-cm input, .pg-study .sr-cm textarea, .pg-study .sr-cm select{font:inherit;color:inherit}
.pg-study .sr-cm button{cursor:pointer;background:none;border:0;padding:0}
.pg-study .sr-cm a{color:inherit;text-decoration:none}
.pg-study .sr-cm h1, .pg-study .sr-cm h2, .pg-study .sr-cm h3, .pg-study .sr-cm h4{font-family:var(--display);margin:0;text-wrap:balance;letter-spacing:-.015em}
.pg-study .sr-cm p{margin:0}
.pg-study .sr-cm .eyebrow{font-size:11px;font-weight:800;letter-spacing:.2em;text-transform:uppercase;color:var(--primary-ink);display:inline-flex;align-items:center;gap:8px}
.pg-study .sr-cm .tnum{font-variant-numeric:tabular-nums}
.pg-study .sr-cm .icon-btn{width:38px;height:38px;border-radius:10px;display:grid;place-items:center;color:var(--fg-2);border:1px solid transparent;transition:background .15s,border-color .15s}
.pg-study .sr-cm .icon-btn:hover{background:var(--card-2)}
.pg-study .sr-cm .icon-btn.boxed{border-color:var(--border)}
.pg-study .sr-cm .btn{display:inline-flex;align-items:center;justify-content:center;gap:9px;height:44px;padding:0 22px;border-radius:999px;font-weight:700;font-size:15px;white-space:nowrap;transition:background .15s,transform .15s,box-shadow .15s,border-color .15s}
.pg-study .sr-cm .btn:active{transform:translateY(1px)}
.pg-study .sr-cm .btn-primary{background:var(--primary);color:var(--on-primary);box-shadow:0 6px 20px color-mix(in srgb,var(--primary) 32%,transparent)}
.pg-study .sr-cm .btn-primary:hover{background:var(--primary-hov)}
.pg-study .sr-cm .btn-outline{border:1px solid var(--border-2);background:var(--card);color:var(--fg)}
.pg-study .sr-cm .btn-outline:hover{border-color:var(--primary);color:var(--primary-ink)}
.pg-study .sr-cm .btn-ghost{color:var(--fg-2)}
.pg-study .sr-cm .btn-ghost:hover{background:var(--card-2)}
.pg-study .sr-cm .btn-on-stage{border:1px solid rgba(255,255,255,.22);color:#fff;background:rgba(255,255,255,.02)}
.pg-study .sr-cm .btn-on-stage:hover{background:rgba(255,255,255,.08)}
.pg-study .sr-cm .btn-sm{height:34px;padding:0 14px;font-size:13.5px}
.pg-study .sr-cm .btn-lg{height:52px;padding:0 28px;font-size:16px}
.pg-study .sr-cm .chip{display:inline-flex;align-items:center;gap:7px;height:38px;padding:0 17px;border-radius:999px;border:1px solid var(--border-2);background:var(--card);font-weight:600;font-size:14.5px;color:var(--fg-2);white-space:nowrap;transition:border-color .15s,background .15s,color .15s}
.pg-study .sr-cm .chip:hover{border-color:var(--primary)}
.pg-study .sr-cm .chip.on{background:var(--primary-soft);border-color:var(--primary);color:var(--primary-ink)}
.pg-study .sr-cm .field-search{display:flex;align-items:center;gap:10px;height:44px;width:min(320px,100%);border:1px solid var(--border-2);border-radius:999px;padding:0 16px;background:var(--card);color:var(--muted)}
.pg-study .sr-cm .field-search input{border:0;outline:0;background:transparent;flex:1;min-width:0;font-size:15px;color:var(--fg)}
.pg-study .sr-cm .row{margin-top:48px}
.pg-study .sr-cm .row-head{display:flex;align-items:flex-end;gap:16px;margin-bottom:18px}
.pg-study .sr-cm .row-head h2{font-size:22px;font-weight:700;display:flex;align-items:center;gap:10px}
.pg-study .sr-cm .row-head h2 .i{color:var(--primary)}
.pg-study .sr-cm .row-head p{color:var(--muted);font-size:15px;margin-top:4px}
.pg-study .sr-cm .row-arrows{margin-left:auto;display:flex;gap:8px}
.pg-study .sr-cm .row-arrows button{width:38px;height:38px;border-radius:50%;border:1px solid var(--border-2);display:grid;place-items:center;color:var(--fg-2);background:var(--card)}
.pg-study .sr-cm .row-arrows button:hover{border-color:var(--primary);color:var(--primary-ink)}
.pg-study .sr-cm .rail{display:grid;grid-auto-flow:column;grid-auto-columns:calc((100% - 3*24px)/4);gap:24px;overflow-x:auto;scroll-snap-type:x mandatory;scrollbar-width:none;padding-bottom:4px}
.pg-study .sr-cm .rail::-webkit-scrollbar{display:none}
.pg-study .sr-cm .rail > *{scroll-snap-align:start;min-width:0}
@media (max-width:1180px){.pg-study .sr-cm .rail{grid-auto-columns:calc((100% - 2*20px)/3);gap:20px}}
@media (max-width:820px){.pg-study .sr-cm .rail{grid-auto-columns:calc((100% - 16px)/2);gap:16px}}
@media (max-width:520px){.pg-study .sr-cm .rail{grid-auto-columns:78%}}
.pg-study .sr-cm .tile{display:flex;flex-direction:column;gap:14px;border-radius:14px;outline-offset:4px}
.pg-study .sr-cm .tile .thumb{transition:transform .2s ease, box-shadow .2s ease}
.pg-study .sr-cm .tile:hover .thumb{transform:translateY(-3px);box-shadow:var(--sh-3)}
.pg-study .sr-cm .tile-meta{display:flex;align-items:center;gap:12px;min-width:0}
.pg-study .sr-cm .tile-text{display:flex;flex-direction:column;min-width:0}
.pg-study .sr-cm .tile-name{font-family:var(--display);font-weight:700;font-size:16px;letter-spacing:-.01em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.pg-study .sr-cm .tile-sub{font-size:13px;color:var(--muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.pg-study .sr-cm .appicon{width:44px;height:44px;border-radius:12px;display:grid;place-items:center;flex:none;color:#fff;background:var(--ai,linear-gradient(145deg,#1d3a4a,#0e1c26));box-shadow:inset 0 1px 0 rgba(255,255,255,.22),0 3px 8px rgba(10,22,29,.2)}
.pg-study .sr-cm .appicon .i{width:22px;height:22px}
.pg-study .sr-cm .thumb{position:relative;aspect-ratio:16/9;max-width:100%;border-radius:12px;overflow:hidden;container-type:inline-size;color:var(--stage-fg);border:1px solid var(--stage-line);
background:radial-gradient(110% 80% at 0% 0%, var(--stage-2) 0%, transparent 60%),var(--stage)}
.pg-study .sr-cm .thumb.violet{background:radial-gradient(90% 80% at 0% 0%, #2a2256 0%, transparent 62%),#12142a;border-color:#272a4a}
.pg-study .sr-cm .thumb.warm{background:radial-gradient(90% 90% at 0% 0%, #3a2410 0%, transparent 60%),#18110c;border-color:#3a2a1c}
.pg-study .sr-cm .thumb.ember{background:radial-gradient(90% 90% at 50% 0%, #3b1512 0%, transparent 60%),#140c0b;border-color:#3a1c18}
.pg-study .sr-cm .t-in{position:absolute;inset:6cqi 6.5cqi 0;display:flex;flex-direction:column;gap:2.2cqi}
.pg-study .sr-cm .t-eye{font:700 2.5cqi/1 var(--mono);letter-spacing:.2em;text-transform:uppercase;color:var(--stage-accent)}
.pg-study .sr-cm .thumb.violet .t-eye{color:#a593ff}
.pg-study .sr-cm .t-q{font:700 4.4cqi/1.22 var(--body);color:#fff}
.pg-study .sr-cm .t-box{border:1px solid var(--stage-line);background:rgba(255,255,255,.025);border-radius:2cqi;padding:2.6cqi 3cqi;display:flex;flex-direction:column;gap:1.6cqi}
.pg-study .sr-cm .thumb.violet .t-box{border-color:#343766;background:rgba(124,104,255,.06)}
.pg-study .sr-cm .t-line{display:flex;align-items:center;gap:2cqi;font-size:3.3cqi;line-height:1.2;color:#d6e4e3}
.pg-study .sr-cm .t-line b{font-weight:700;color:#fff}
.pg-study .sr-cm .t-dot{width:3.4cqi;height:3.4cqi;border-radius:50%;border:.5cqi solid var(--stage-accent);flex:none;display:grid;place-items:center}
.pg-study .sr-cm .t-dot.done{background:transparent}
.pg-study .sr-cm .t-dot.done::after{content:"";width:1.6cqi;height:.9cqi;border-left:.5cqi solid var(--stage-accent);border-bottom:.5cqi solid var(--stage-accent);transform:rotate(-45deg) translate(.1cqi,-.2cqi)}
.pg-study .sr-cm .t-dot.open{border-color:var(--stage-gold)}
.pg-study .sr-cm .t-num{width:4.6cqi;height:4.6cqi;border-radius:1.2cqi;background:#7c62f0;color:#fff;display:grid;place-items:center;font-size:2.8cqi;font-weight:700;flex:none}
.pg-study .sr-cm .t-num.dim{background:#252a45;color:#9aa0c8}
.pg-study .sr-cm .t-opt{display:flex;align-items:center;gap:2cqi;border:1px solid var(--stage-line);border-radius:1.6cqi;padding:1.5cqi 2.2cqi;font-size:3cqi}
.pg-study .sr-cm .t-opt > span{width:3.6cqi;height:3.6cqi;border-radius:.9cqi;background:#16303a;display:grid;place-items:center;font-size:2.1cqi;font-weight:700;color:var(--stage-accent)}
.pg-study .sr-cm .t-opt.bad{border-color:#b54444;background:rgba(181,68,68,.12)}
.pg-study .sr-cm .t-opt.good{border-color:var(--stage-accent);background:rgba(47,208,184,.1)}
.pg-study .sr-cm .t-opt i{margin-left:auto;font-style:normal;font-weight:700;font-size:2.6cqi}
.pg-study .sr-cm .t-opt.bad i{color:#ff7a7a}
.pg-study .sr-cm .t-opt.good i{color:var(--stage-accent)}
.pg-study .sr-cm .t-tiles{display:flex;gap:1.6cqi;justify-content:center}
.pg-study .sr-cm .t-tile{width:6.2cqi;height:6.2cqi;border:1px solid var(--stage-accent);border-radius:1.2cqi;display:grid;place-items:center;font:700 3.2cqi/1 var(--body);color:#fff;background:rgba(47,208,184,.08)}
.pg-study .sr-cm .t-tile.ghost{border-color:var(--stage-line);color:#5d7479;background:none}
.pg-study .sr-cm .t-tile.hit{background:#2b8f62;border-color:#3fbf85}
.pg-study .sr-cm .t-tile.alt{background:#2c5aa0;border-color:#4d7fcb}
.pg-study .sr-cm .t-pill{display:inline-flex;align-items:center;justify-content:center;gap:1cqi;align-self:center;padding:1.2cqi 3cqi;border-radius:1.4cqi;background:var(--stage-accent);color:#052a24;font:700 2.2cqi/1 var(--body);letter-spacing:.12em;text-transform:uppercase}
.pg-study .sr-cm .t-card{border-radius:2cqi;border:1px solid var(--stage-line);display:grid;place-items:center;text-align:center;padding:2cqi}
.pg-study .sr-cm .t-grid{display:grid;gap:.8cqi}
.pg-study .sr-cm .t-cell{aspect-ratio:1;border-radius:.6cqi;display:grid;place-items:center;font:700 2.7cqi/1 var(--body);color:#cfe3e1;background:#0f242b}
.pg-study .sr-cm .t-cell.x{background:#06090b}
.pg-study .sr-cm .t-cell.hl{background:#2e7f74;color:#fff}
.pg-study .sr-cm .t-cell.gold{outline:.4cqi solid var(--stage-gold);color:var(--stage-gold)}
.pg-study .sr-cm .t-mono{font:400 2.8cqi/1.55 var(--mono);color:#7be0cf}
.pg-study .sr-cm .t-serif{font-family:"EB Garamond","Cormorant Garamond",Georgia,serif;font-style:italic}
.pg-study .sr-cm .t-bubble{align-self:flex-end;max-width:72%;background:var(--stage-accent);color:#04241f;font-weight:700;font-size:3.4cqi;line-height:1.2;padding:2.2cqi 3cqi;border-radius:2cqi}
.pg-study .sr-cm .t-bar{height:.9cqi;border-radius:1cqi;background:#1c3138;overflow:hidden}
.pg-study .sr-cm .t-bar > i{display:block;height:100%;background:var(--stage-accent);width:var(--w,40%)}
.pg-study .sr-cm .hoot{display:inline-block;flex:none;width:72px;height:78px;overflow:visible}
.pg-study .sr-cm .hoot-bob{animation:cm-hootbob 4.2s ease-in-out infinite;transform-origin:50% 100%}
@keyframes cm-hootbob{0%,100%{transform:translateY(0) rotate(0)}50%{transform:translateY(-3px) rotate(-1.5deg)}}
.pg-study .sr-cm .cm-top{display:flex;align-items:center;gap:14px;flex-wrap:wrap;margin-bottom:18px}
.pg-study .sr-cm .seg{display:inline-flex;padding:4px;border-radius:999px;background:var(--card);border:1px solid var(--border-2);gap:2px;max-width:100%;overflow-x:auto;scrollbar-width:none}
.pg-study .sr-cm .seg button{display:inline-flex;align-items:center;gap:7px;height:34px;padding:0 15px;border-radius:999px;font-weight:700;font-size:14px;color:var(--muted);white-space:nowrap}
.pg-study .sr-cm .seg button .i{width:15px;height:15px}
.pg-study .sr-cm .seg button[aria-pressed="true"]{background:var(--primary-soft);color:var(--primary-ink)}
.pg-study .sr-cm .cm-top .stat{margin-left:auto;font-size:13.5px;color:var(--muted);display:inline-flex;align-items:center;gap:8px}
.pg-study .sr-cm .cm-top .stat b{color:var(--fg)}
.pg-study .sr-cm .cm-hero{position:relative;display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1.05fr);gap:28px;align-items:center;border-radius:var(--r-lg);overflow:hidden;padding:40px 40px 40px 44px;color:var(--stage-fg);
background:radial-gradient(70% 110% at 0% 0%,#0f4a48 0%,transparent 60%),radial-gradient(50% 80% at 100% 100%,#2b2312 0%,transparent 60%),linear-gradient(135deg,#0a2a30,#081820 55%,#0a141c)}
.pg-study .sr-cm .cm-hero .eyebrow{color:var(--stage-gold)}
.pg-study .sr-cm .cm-hero h1{font-size:clamp(28px,3vw,40px);font-weight:800;color:#fff;margin-top:10px}
.pg-study .sr-cm .cm-hero p.d{color:#cfe0df;font-size:16px;line-height:1.65;max-width:48ch;margin-top:10px}
.pg-study .sr-cm .who{display:flex;align-items:center;gap:12px;margin-top:18px}
.pg-study .sr-cm .av{width:40px;height:40px;border-radius:50%;display:grid;place-items:center;flex:none;color:#fff;font-weight:800;font-size:13px;background:var(--av,#2b5a63);box-shadow:inset 0 1px 0 rgba(255,255,255,.22)}
.pg-study .sr-cm .av.sm{width:30px;height:30px;font-size:11px}
.pg-study .sr-cm .who b{display:block;color:#fff;font-size:15px}
.pg-study .sr-cm .who span{font-size:13px;color:#a9c1c1}
.pg-study .sr-cm .hstats{display:flex;gap:22px;flex-wrap:wrap;margin-top:18px}
.pg-study .sr-cm .hstats div b{display:block;font:800 22px/1 var(--display);color:#fff}
.pg-study .sr-cm .hstats div span{font-size:12px;color:#a9c1c1}
.pg-study .sr-cm .hcta{display:flex;gap:10px;flex-wrap:wrap;margin-top:22px}
.pg-study .sr-cm .badge{display:inline-flex;align-items:center;gap:5px;height:24px;padding:0 9px;border-radius:999px;font-size:11.5px;font-weight:800;white-space:nowrap}
.pg-study .sr-cm .badge .i{width:12px;height:12px}
.pg-study .sr-cm .b-orig{background:var(--primary-soft);color:var(--primary-ink)}
.pg-study .sr-cm .b-stud{background:var(--pin-bg);color:var(--pin-ink)}
.pg-study .sr-cm .b-check{background:rgba(47,208,184,.14);color:var(--stage-accent);border:1px solid rgba(47,208,184,.3)}
.pg-study .sr-cm .badges{display:flex;gap:6px;flex-wrap:wrap}
.pg-study .sr-cm .cm-art{border-radius:16px;overflow:hidden;border:1px solid #1e363d;background:#071219;aspect-ratio:16/10}
.pg-study .sr-cm .cm-art svg{display:block;width:100%;height:100%}
.pg-study .sr-cm .chal{margin-top:24px;display:grid;grid-template-columns:auto minmax(0,1fr) auto;gap:22px;align-items:center;padding:22px 26px;border-radius:var(--r-lg);border:1px solid color-mix(in srgb,var(--pin-btn) 45%,var(--border));
background:linear-gradient(100deg,var(--pin-bg),var(--card) 70%)}
.pg-study .sr-cm .chal h2{font-size:22px;font-weight:800;margin-top:4px}
.pg-study .sr-cm .chal p{font-size:14.5px;color:var(--fg-2);margin-top:4px;max-width:62ch}
.pg-study .sr-cm .chal .eyebrow{color:var(--pin-ink)}
.pg-study .sr-cm .chal-meta{display:flex;gap:16px;flex-wrap:wrap;margin-top:10px;font-size:13px;color:var(--muted)}
.pg-study .sr-cm .chal-meta span{display:inline-flex;gap:6px;align-items:center}
.pg-study .sr-cm .clock{display:flex;gap:6px}
.pg-study .sr-cm .clock div{min-width:52px;text-align:center;padding:8px 6px;border-radius:12px;background:var(--card);border:1px solid var(--border)}
.pg-study .sr-cm .clock b{display:block;font:800 22px/1 var(--display);font-variant-numeric:tabular-nums}
.pg-study .sr-cm .clock span{font-size:10.5px;font-weight:700;letter-spacing:.12em;text-transform:uppercase;color:var(--muted)}
.pg-study .sr-cm .chal-r{display:flex;flex-direction:column;align-items:flex-end;gap:12px}
.pg-study .sr-cm .ctile{position:relative}
.pg-study .sr-cm .ctile .thumb .badge{position:absolute;left:8px;top:8px;z-index:2;background:rgba(4,12,16,.74);color:#e9f2f1;border:1px solid rgba(255,255,255,.14)}
.pg-study .sr-cm .ctile .thumb .badge.orig{color:#7fe3d2}
.pg-study .sr-cm .ctile .thumb .badge.stud{color:#f2c35b}
.pg-study .sr-cm .remix{position:absolute;right:8px;top:8px;z-index:3;display:inline-flex;align-items:center;gap:6px;height:32px;padding:0 12px;border-radius:999px;background:var(--card);color:var(--fg);font-weight:700;font-size:13px;box-shadow:var(--sh-2);opacity:0;transform:translateY(-4px);transition:opacity .15s,transform .15s}
.pg-study .sr-cm .remix .i{width:15px;height:15px;color:var(--primary-ink)}
.pg-study .sr-cm .ctile:hover .remix, .pg-study .sr-cm .ctile:focus-within .remix{opacity:1;transform:none}
@media (hover:none),(max-width:820px){.pg-study .sr-cm .remix{opacity:1;transform:none}}
.pg-study .sr-cm .cmeta{display:flex;gap:10px;align-items:flex-start;min-width:0}
.pg-study .sr-cm .cmeta .tile-text{flex:1}
.pg-study .sr-cm .cline{font-size:12.5px;color:var(--muted);display:flex;gap:10px;flex-wrap:wrap;margin-top:2px}
.pg-study .sr-cm .cline span{display:inline-flex;gap:4px;align-items:center}
.pg-study .sr-cm .cline .i{width:13px;height:13px}
.pg-study .sr-cm .yours{font-size:9.5px;font-weight:800;letter-spacing:.12em;text-transform:uppercase;background:var(--pin-bg);color:var(--pin-ink);padding:3px 7px;border-radius:6px;margin-left:6px;vertical-align:2px}
.pg-study .sr-cm .fresh .thumb{box-shadow:0 0 0 3px var(--primary),var(--sh-3)}
.pg-study .sr-cm .lb{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:12px}
.pg-study .sr-cm .lbc{display:flex;flex-direction:column;gap:10px;padding:16px;border-radius:16px;border:1px solid var(--border);background:var(--card);position:relative;min-width:0}
.pg-study .sr-cm .lbc .rk{position:absolute;right:14px;top:12px;font:800 22px/1 var(--display);color:var(--border-2)}
.pg-study .sr-cm .lbc.topc .rk{color:var(--pin-ink)}
.pg-study .sr-cm .lbc.topc{border-color:color-mix(in srgb,var(--pin-ink) 40%,var(--border))}
.pg-study .sr-cm .lbc b{font-size:15px}
.pg-study .sr-cm .lbc .s{font-size:12.5px;color:var(--muted)}
.pg-study .sr-cm .lbc dl{display:grid;grid-template-columns:1fr 1fr;gap:6px;margin:4px 0 0}
.pg-study .sr-cm .lbc dt{font-size:10px;font-weight:800;letter-spacing:.14em;text-transform:uppercase;color:var(--muted)}
.pg-study .sr-cm .lbc dd{margin:2px 0 0;font:700 16px/1.1 var(--display)}
.pg-study .sr-cm .back{display:inline-flex;align-items:center;gap:8px;font-weight:700;font-size:14px;color:var(--fg-2);margin-bottom:16px;padding:6px 8px 6px 4px;border-radius:8px}
.pg-study .sr-cm .back:hover{color:var(--primary-ink)}
.pg-study .sr-cm .dt{display:grid;grid-template-columns:minmax(0,1.45fr) minmax(300px,1fr);gap:26px;align-items:start}
.pg-study .sr-cm .dstage{position:relative;border-radius:var(--r-lg);overflow:hidden;background:#071219;border:1px solid var(--stage-line);color:var(--stage-fg);min-height:420px;display:flex;flex-direction:column}
.pg-study .sr-cm .dstage .cm-art{border:0;border-radius:0;aspect-ratio:auto;flex:1;min-height:300px}
.pg-study .sr-cm .dplay{position:absolute;inset:0;display:grid;place-items:center;background:linear-gradient(transparent 40%,rgba(4,11,15,.75))}
.pg-study .sr-cm .dplay .btn{height:56px;padding:0 30px;font-size:17px}
.pg-study .sr-cm .dside{display:flex;flex-direction:column;gap:16px;min-width:0}
.pg-study .sr-cm .dside h1{font-size:clamp(26px,2.6vw,34px);font-weight:800}
.pg-study .sr-cm .dside .lede{font-size:15.5px;color:var(--fg-2);line-height:1.6}
.pg-study .sr-cm .dwho{display:flex;align-items:center;gap:12px}
.pg-study .sr-cm .dwho b{display:block}
.pg-study .sr-cm .dwho span{font-size:13px;color:var(--muted)}
.pg-study .sr-cm .dact{display:flex;gap:10px;flex-wrap:wrap}
.pg-study .sr-cm .box{border:1px solid var(--border);border-radius:16px;background:var(--card);padding:14px 16px;display:flex;flex-direction:column;gap:10px}
.pg-study .sr-cm .box h3{font-size:15px;font-weight:700;display:flex;align-items:center;gap:8px}
.pg-study .sr-cm .box h3 .i{color:var(--primary-ink)}
.pg-study .sr-cm .rate-row{display:flex;align-items:center;gap:14px;flex-wrap:wrap}
.pg-study .sr-cm .rate-row .pct{font:800 30px/1 var(--display)}
.pg-study .sr-cm .rate-row .cnt{font-size:13px;color:var(--muted)}
.pg-study .sr-cm .rbar{height:8px;border-radius:6px;background:var(--ta-bg);overflow:hidden;flex:1 1 140px}
.pg-study .sr-cm .rbar i{display:block;height:100%;background:var(--success);transition:width .5s}
.pg-study .sr-cm .vote{display:flex;gap:8px}
.pg-study .sr-cm .vote button{display:inline-flex;align-items:center;gap:6px;height:36px;padding:0 14px;border-radius:999px;border:1px solid var(--border-2);font-weight:700;font-size:13.5px;background:var(--card)}
.pg-study .sr-cm .vote button[aria-pressed="true"]{border-color:var(--primary);background:var(--primary-soft);color:var(--primary-ink)}
.pg-study .sr-cm .vote .down .i{transform:rotate(180deg)}
.pg-study .sr-cm .docs{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:6px;font-size:13.5px}
.pg-study .sr-cm .docs li{display:flex;gap:8px;align-items:center;color:var(--fg-2)}
.pg-study .sr-cm .docs .i{color:var(--danger);width:15px;height:15px}
.pg-study .sr-cm .lin{display:flex;align-items:center;gap:8px;flex-wrap:wrap;font-size:13.5px}
.pg-study .sr-cm .lin .node{display:inline-flex;align-items:center;gap:8px;padding:6px 10px 6px 6px;border-radius:999px;border:1px solid var(--border);background:var(--card-2);font-weight:700}
.pg-study .sr-cm .lin .node.cur{border-color:var(--primary);background:var(--primary-soft);color:var(--primary-ink)}
.pg-study .sr-cm .lin .node .av{width:24px;height:24px;font-size:9.5px}
.pg-study .sr-cm .lin > .i{color:var(--muted);width:16px;height:16px}
.pg-study .sr-cm .remixes{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px}
.pg-study .sr-cm .rm{display:flex;flex-direction:column;gap:6px;font-size:13px;min-width:0}
.pg-study .sr-cm .rm .thumb{border-radius:10px}
.pg-study .sr-cm .rm b{font-size:13.5px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.pg-study .sr-cm .rm span{color:var(--muted);font-size:12px}
.pg-study .sr-cm .report{font-size:13px;font-weight:700;color:var(--muted);display:inline-flex;gap:6px;align-items:center;align-self:flex-start;padding:4px 2px}
.pg-study .sr-cm .report:hover{color:var(--danger)}
.pg-study .sr-cm .rform{display:flex;flex-direction:column;gap:8px}
.pg-study .sr-cm .rform select, .pg-study .sr-cm .rform textarea, .pg-study .sr-cm .inp{border:1px solid var(--border-2);border-radius:10px;background:var(--card);color:var(--fg);padding:8px 10px;font-size:14px;outline:0;width:100%}
.pg-study .sr-cm .rform textarea{min-height:70px;resize:vertical}
.pg-study .sr-cm .td{flex:1;display:flex;flex-direction:column}
.pg-study .sr-cm .td-hud{display:flex;align-items:center;gap:12px;flex-wrap:wrap;padding:12px 16px;border-bottom:1px solid var(--stage-line);font:700 11px var(--mono);letter-spacing:.14em;text-transform:uppercase;color:var(--stage-muted)}
.pg-study .sr-cm .td-hud .lives{display:inline-flex;gap:3px;color:#ff6f7d}
.pg-study .sr-cm .td-hud .lives svg{width:16px;height:16px;fill:#ff6f7d;stroke:#ff6f7d}
.pg-study .sr-cm .td-hud .lives svg.lost{fill:none;stroke:#4a5f66}
.pg-study .sr-cm .td-hud .sc{margin-left:auto;color:#e9f2f1}
.pg-study .sr-cm .td-field{position:relative;height:190px;flex:none}
.pg-study .sr-cm .td-field svg{position:absolute;inset:0;width:100%;height:100%}
.pg-study .sr-cm .pg{transition:transform .9s cubic-bezier(.4,0,.2,1),opacity .4s}
.pg-study .sr-cm .pg.zapped{opacity:0;transform:scale(.2)}
.pg-study .sr-cm .zap{stroke:#f2c35b;stroke-width:3;stroke-linecap:round;opacity:0}
.pg-study .sr-cm .zap.on{animation:cm-zap .5s ease}
@keyframes cm-zap{20%{opacity:1}100%{opacity:0}}
.pg-study .sr-cm .td-q{padding:16px;display:flex;flex-direction:column;gap:12px}
.pg-study .sr-cm .td-q h3{font:700 19px/1.3 var(--display);color:#fff}
.pg-study .sr-cm .tdo{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}
.pg-study .sr-cm .tdo button{display:flex;align-items:center;gap:10px;min-height:48px;padding:8px 12px;border-radius:12px;border:1px solid #24414a;background:rgba(255,255,255,.035);color:#e9f2f1;font-weight:600;font-size:14.5px;text-align:left}
.pg-study .sr-cm .tdo button:hover:not([disabled]){border-color:var(--stage-accent)}
.pg-study .sr-cm .tdo button.good{border-color:#3fbf85;background:rgba(63,191,133,.16)}
.pg-study .sr-cm .tdo button.bad{border-color:#e05a5a;background:rgba(224,90,90,.15)}
.pg-study .sr-cm .tdo button[disabled]{cursor:default}
.pg-study .sr-cm .tdo .k{width:24px;height:24px;border-radius:7px;background:#16303a;color:var(--stage-accent);font:700 11.5px var(--mono);display:grid;place-items:center;flex:none}
.pg-study .sr-cm .tdfb{display:flex;align-items:center;gap:10px;flex-wrap:wrap;font-size:14px;color:#d6e4e3;border-radius:12px;padding:10px 12px;border:1px solid #2a4a3f;background:rgba(63,191,133,.08)}
.pg-study .sr-cm .tdfb.bad{border-color:#5a2a2e;background:rgba(224,90,90,.08)}
.pg-study .sr-cm .tdfb strong{color:#fff}
.pg-study .sr-cm .cite{display:inline-flex;align-items:center;gap:6px;font:600 12px var(--mono);color:#9fd9cf;border:1px solid var(--stage-line);border-radius:7px;padding:4px 8px}
.pg-study .sr-cm .cite .i{width:13px;height:13px}
.pg-study .sr-cm .sbtn{margin-left:auto;display:inline-flex;align-items:center;gap:8px;height:38px;padding:0 16px;border-radius:999px;font-weight:700;font-size:14px;background:var(--stage-accent);color:#052a24}
.pg-study .sr-cm .sbtn.ghost{background:transparent;color:#fff;border:1px solid rgba(255,255,255,.25);margin-left:0}
.pg-study .sr-cm .td-end{padding:26px 18px;display:flex;flex-direction:column;align-items:center;text-align:center;gap:10px}
.pg-study .sr-cm .td-end h3{font:800 24px/1.1 var(--display);color:#fff}
.pg-study .sr-cm .td-end p{color:#b9cfd0;font-size:14.5px}
.pg-study .sr-cm .td-end .vote button{background:rgba(255,255,255,.04);border-color:rgba(255,255,255,.22);color:#fff}
.pg-study .sr-cm .td-end .vote button[aria-pressed="true"]{border-color:var(--stage-accent);background:rgba(47,208,184,.16);color:#fff}
@keyframes cm-dIn{from{transform:translateX(40px);opacity:.4}}
.pg-study .sr-cm .dh{display:flex;align-items:flex-start;gap:12px}
.pg-study .sr-cm .dh h2{font-size:24px;font-weight:800;margin-top:6px}
.pg-study .sr-cm .dh .icon-btn{margin-left:auto;border:1px solid var(--border)}
.pg-study .sr-cm .src-card{display:flex;gap:12px;align-items:center;padding:10px;border:1px solid var(--border);border-radius:14px;background:var(--card-2)}
.pg-study .sr-cm .src-card .thumb{width:120px;flex:none;border-radius:9px}
.pg-study .sr-cm .src-card b{display:block;font-size:15px}
.pg-study .sr-cm .src-card span{font-size:13px;color:var(--muted)}
.pg-study .sr-cm .fld{display:flex;flex-direction:column;gap:8px;border:0;margin:0;padding:0;min-width:0}
.pg-study .sr-cm .fld > label, .pg-study .sr-cm .fld > legend{font-size:11px;font-weight:800;letter-spacing:.18em;text-transform:uppercase;color:var(--muted);padding:0}
.pg-study .sr-cm .fld textarea{min-height:110px;resize:vertical;border:1px solid var(--border-2);border-radius:14px;padding:12px 14px;font:500 17px/1.45 var(--display);color:var(--fg);background:var(--card);outline:0}
.pg-study .sr-cm .fld textarea:focus{border-color:var(--primary)}
.pg-study .sr-cm .doc{display:inline-flex;align-items:center;gap:8px;align-self:flex-start;padding:6px 12px 6px 8px;border-radius:10px;border:1px solid var(--border);background:var(--card-2);font-size:13.5px;font-weight:600}
.pg-study .sr-cm .doc .i{color:var(--danger);width:16px;height:16px}
.pg-study .sr-cm .keep{display:flex;gap:8px;flex-wrap:wrap}
.pg-study .sr-cm .keep button{display:inline-flex;align-items:center;gap:7px;height:36px;padding:0 14px;border-radius:999px;border:1px solid var(--border-2);font-weight:600;font-size:14px;background:var(--card)}
.pg-study .sr-cm .keep button[aria-pressed="true"]{border-color:var(--primary);background:var(--primary-soft);color:var(--primary-ink)}
.pg-study .sr-cm .keep button .i{width:14px;height:14px}
.pg-study .sr-cm .credit{display:flex;gap:10px;align-items:flex-start;font-size:13.5px;color:var(--fg-2);background:var(--card-2);border-radius:12px;padding:10px 12px}
.pg-study .sr-cm .credit .i{color:var(--primary-ink);margin-top:1px}
.pg-study .sr-cm .dfoot{display:flex;gap:10px;justify-content:flex-end;flex-wrap:wrap;margin-top:auto;padding-top:14px;border-top:1px solid var(--border)}
.pg-study .sr-cm .seg2{display:inline-flex;padding:3px;border-radius:999px;background:var(--card-2);border:1px solid var(--border);align-self:flex-start}
.pg-study .sr-cm .seg2 button{height:32px;padding:0 14px;border-radius:999px;font-weight:700;font-size:13.5px;color:var(--muted);display:inline-flex;gap:6px;align-items:center}
.pg-study .sr-cm .seg2 button[aria-pressed="true"]{background:var(--card);color:var(--primary-ink);box-shadow:var(--sh-1)}
.pg-study .sr-cm .steps{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:12px}
.pg-study .sr-cm .steps li{display:flex;gap:10px;align-items:flex-start;font-size:14.5px;color:var(--muted)}
.pg-study .sr-cm .steps li.run, .pg-study .sr-cm .steps li.done{color:var(--fg)}
.pg-study .sr-cm .steps .st{width:20px;height:20px;border-radius:50%;border:2px solid var(--border-2);flex:none;display:grid;place-items:center;margin-top:1px}
.pg-study .sr-cm .steps .st .i{width:12px;height:12px;stroke-width:3;opacity:0;color:var(--on-primary)}
.pg-study .sr-cm .steps li.run .st{border-color:var(--primary);border-top-color:transparent;animation:cm-spin .8s linear infinite}
.pg-study .sr-cm .steps li.done .st{background:var(--primary);border-color:var(--primary)}
.pg-study .sr-cm .steps li.done .st .i{opacity:1}
.pg-study .sr-cm .steps small{display:block;font:500 12px/1.4 var(--mono);color:var(--muted)}
@keyframes cm-spin{to{transform:rotate(360deg)}}
.pg-study .sr-cm .hootline{display:flex;gap:12px;align-items:flex-end}
.pg-study .sr-cm .hootline p{background:var(--card-2);border-radius:14px 14px 14px 4px;padding:10px 14px;font-size:14px;color:var(--fg-2)}
.pg-study .sr-cm .mini{border-radius:16px;overflow:hidden;background:var(--stage);border:1px solid var(--stage-line);color:var(--stage-fg)}
.pg-study .sr-cm .vis{display:flex;flex-direction:column;gap:6px}
.pg-study .sr-cm .vis label{display:flex;gap:10px;align-items:center;padding:10px 12px;border:1px solid var(--border);border-radius:12px;cursor:pointer;font-weight:600;font-size:14px}
.pg-study .sr-cm .vis label small{display:block;font-weight:500;font-size:12.5px;color:var(--muted)}
.pg-study .sr-cm .vis label:has(input:checked){border-color:var(--primary);background:var(--primary-soft)}
.pg-study .sr-cm .vis input{accent-color:var(--primary);width:16px;height:16px;margin:0;flex:none}
.pg-study .sr-cm .copy{display:flex;gap:8px}
.pg-study .sr-cm .copy input{flex:1;min-width:0;height:42px;border:1px solid var(--border-2);border-radius:10px;padding:0 10px;background:var(--card);color:var(--fg);font:500 13px var(--mono)}
@media (max-width:1180px){.pg-study .sr-cm .lb{grid-template-columns:repeat(3,minmax(0,1fr))}
.pg-study .sr-cm .lb .lbc:nth-child(n+4){display:none}}
@media (max-width:1100px){.pg-study .sr-cm .cm-hero, .pg-study .sr-cm .dt{grid-template-columns:minmax(0,1fr)}
.pg-study .sr-cm .chal{grid-template-columns:minmax(0,1fr)}
.pg-study .sr-cm .chal-r{align-items:flex-start}
.pg-study .sr-cm .chal > .hoot{display:none}}
@media (max-width:620px){.pg-study .sr-cm .cm-hero{padding:28px 20px}
.pg-study .sr-cm .lb{grid-template-columns:minmax(0,1fr)}
.pg-study .sr-cm .lb .lbc:nth-child(n+4){display:flex}
.pg-study .sr-cm .tdo, .pg-study .sr-cm .remixes{grid-template-columns:minmax(0,1fr)}
.pg-study .sr-cm .cm-top .stat{display:none}}
</style>
<svg width="0" height="0" style="position:absolute" aria-hidden="true" focusable="false"><defs><symbol id="i-home" viewBox="0 0 24 24"><path d="M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8"/><path d="M3 10a2 2 0 0 1 .709-1.528l7-5.999a2 2 0 0 1 2.582 0l7 5.999A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/></symbol><symbol id="i-search" viewBox="0 0 24 24"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></symbol><symbol id="i-gamepad" viewBox="0 0 24 24"><path d="M6 11h4M8 9v4M15 12h.01M18 10h.01"/><path d="M17.32 5H6.68a4 4 0 0 0-3.978 3.59C2.604 9.416 2 14.456 2 16a3 3 0 0 0 3 3c1 0 1.5-.5 2-1l1.414-1.414A2 2 0 0 1 9.828 16h4.344a2 2 0 0 1 1.414.586L17 18c.5.5 1 1 2 1a3 3 0 0 0 3-3c0-1.545-.604-6.584-.685-7.258A4 4 0 0 0 17.32 5z"/></symbol><symbol id="i-panel" viewBox="0 0 24 24"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M9 3v18"/></symbol><symbol id="i-bolt" viewBox="0 0 24 24"><path d="M4 14a1 1 0 0 1-.78-1.63l9.9-10.2a.5.5 0 0 1 .86.46l-1.92 6.02A1 1 0 0 0 13 10h7a1 1 0 0 1 .78 1.63l-9.9 10.2a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14z"/></symbol><symbol id="i-moon" viewBox="0 0 24 24"><path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"/></symbol><symbol id="i-sun" viewBox="0 0 24 24"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41"/></symbol><symbol id="i-bell" viewBox="0 0 24 24"><path d="M10.268 21a2 2 0 0 0 3.464 0"/><path d="M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326"/></symbol><symbol id="i-card" viewBox="0 0 24 24"><rect width="20" height="14" x="2" y="5" rx="2"/><path d="M2 10h20"/></symbol><symbol id="i-up" viewBox="0 0 24 24"><path d="m18 15-6-6-6 6"/></symbol><symbol id="i-down" viewBox="0 0 24 24"><path d="m6 9 6 6 6-6"/></symbol><symbol id="i-right" viewBox="0 0 24 24"><path d="m9 18 6-6-6-6"/></symbol><symbol id="i-left" viewBox="0 0 24 24"><path d="m15 18-6-6 6-6"/></symbol><symbol id="i-arrow" viewBox="0 0 24 24"><path d="M5 12h14"/><path d="m12 5 7 7-7 7"/></symbol><symbol id="i-arrow-up" viewBox="0 0 24 24"><path d="m5 12 7-7 7 7"/><path d="M12 19V5"/></symbol><symbol id="i-back" viewBox="0 0 24 24"><path d="m12 19-7-7 7-7"/><path d="M19 12H5"/></symbol><symbol id="i-sparkle" viewBox="0 0 24 24"><path d="M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .963 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.581a.5.5 0 0 1 0 .964L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.963 0z"/><path d="M20 3v4M22 5h-4M4 17v2M5 18H3"/></symbol><symbol id="i-play" viewBox="0 0 24 24"><path d="M6 4.5v15a1 1 0 0 0 1.5.86l12.5-7.5a1 1 0 0 0 0-1.72L7.5 3.64A1 1 0 0 0 6 4.5z"/></symbol><symbol id="i-plus" viewBox="0 0 24 24"><path d="M5 12h14M12 5v14"/></symbol><symbol id="i-sprout" viewBox="0 0 24 24"><path d="M7 20h10"/><path d="M10 20c5.5-2.5.8-6.4 3-10"/><path d="M9.5 9.4c1.1.8 1.8 2.2 2.3 3.7-2 .4-3.5.4-4.8-.3-1.2-.6-2.3-1.9-3-4.2 2.8-.5 4.4 0 5.5.8z"/><path d="M14.1 6a7 7 0 0 0-1.1 4c1.9-.1 3.3-.6 4.3-1.4 1-1 1.6-2.3 1.7-4.6-2.7.1-4 1-4.9 2z"/></symbol><symbol id="i-trophy" viewBox="0 0 24 24"><path d="M6 9H4.5a2.5 2.5 0 0 1 0-5H6M18 9h1.5a2.5 2.5 0 0 0 0-5H18M4 22h16M10 14.66V17c0 .55-.47.98-.97 1.21C7.85 18.75 7 20.24 7 22M14 14.66V17c0 .55.47.98.97 1.21C16.15 18.75 17 20.24 17 22"/><path d="M18 2H6v7a6 6 0 0 0 12 0V2Z"/></symbol><symbol id="i-clip" viewBox="0 0 24 24"><path d="m21.44 11.05-9.19 9.19a6 6 0 0 1-8.49-8.49l8.57-8.57A4 4 0 1 1 18 8.84l-8.59 8.57a2 2 0 0 1-2.83-2.83l8.49-8.48"/></symbol><symbol id="i-file" viewBox="0 0 24 24"><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4M16 13H8M16 17H8M10 9H8"/></symbol><symbol id="i-check" viewBox="0 0 24 24"><path d="M20 6 9 17l-5-5"/></symbol><symbol id="i-x" viewBox="0 0 24 24"><path d="M18 6 6 18M6 6l12 12"/></symbol><symbol id="i-thumb" viewBox="0 0 24 24"><path d="M7 10v12"/><path d="M15 5.88 14 10h5.83a2 2 0 0 1 1.92 2.56l-2.33 8A2 2 0 0 1 17.5 22H4a2 2 0 0 1-2-2v-8a2 2 0 0 1 2-2h2.76a2 2 0 0 0 1.79-1.11L12 2a3.13 3.13 0 0 1 3 3.88Z"/></symbol><symbol id="i-users" viewBox="0 0 24 24"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87M16 3.13a4 4 0 0 1 0 7.75"/></symbol><symbol id="i-lock" viewBox="0 0 24 24"><rect width="18" height="11" x="3" y="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></symbol><symbol id="i-globe" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20M2 12h20"/></symbol><symbol id="i-remix" viewBox="0 0 24 24"><circle cx="12" cy="18" r="3"/><circle cx="6" cy="6" r="3"/><circle cx="18" cy="6" r="3"/><path d="M18 9v2c0 .6-.4 1-1 1H7c-.6 0-1-.4-1-1V9M12 12v3"/></symbol><symbol id="i-dice" viewBox="0 0 24 24"><rect width="12" height="12" x="2" y="10" rx="2"/><path d="m17.92 14 3.5-3.5a2.24 2.24 0 0 0 0-3l-5-4.92a2.24 2.24 0 0 0-3 0L10 6M6 18h.01M10 14h.01M15 6h.01M18 9h.01"/></symbol><symbol id="i-wand" viewBox="0 0 24 24"><path d="m21.64 3.64-1.28-1.28a1.21 1.21 0 0 0-1.72 0L2.36 18.64a1.21 1.21 0 0 0 0 1.72l1.28 1.28a1.2 1.2 0 0 0 1.72 0L21.64 5.36a1.2 1.2 0 0 0 0-1.72M14 7l3 3M5 6v4M19 14v4M10 2v2M7 8H3M21 16h-4M11 3H9"/></symbol><symbol id="i-shield" viewBox="0 0 24 24"><path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"/><path d="m9 12 2 2 4-4"/></symbol><symbol id="i-clock" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></symbol><symbol id="i-monitor" viewBox="0 0 24 24"><rect width="20" height="14" x="2" y="3" rx="2"/><path d="M8 21h8M12 17v4"/></symbol><symbol id="i-phone" viewBox="0 0 24 24"><rect width="14" height="20" x="5" y="2" rx="2"/><path d="M12 18h.01"/></symbol><symbol id="i-history" viewBox="0 0 24 24"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5M12 7v5l4 2"/></symbol><symbol id="i-share" viewBox="0 0 24 24"><circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><path d="m8.59 13.51 6.83 3.98M15.41 6.51l-6.82 3.98"/></symbol><symbol id="i-copy" viewBox="0 0 24 24"><rect width="14" height="14" x="8" y="8" rx="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></symbol><symbol id="i-eye" viewBox="0 0 24 24"><path d="M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0"/><circle cx="12" cy="12" r="3"/></symbol><symbol id="i-flag" viewBox="0 0 24 24"><path d="M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1zM4 22v-7"/></symbol><symbol id="i-wrench" viewBox="0 0 24 24"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/></symbol><symbol id="i-grid" viewBox="0 0 24 24"><rect width="7" height="7" x="3" y="3" rx="1"/><rect width="7" height="7" x="14" y="3" rx="1"/><rect width="7" height="7" x="14" y="14" rx="1"/><rect width="7" height="7" x="3" y="14" rx="1"/></symbol><symbol id="i-layers" viewBox="0 0 24 24"><path d="m12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83Z"/><path d="m22 17.65-9.17 4.16a2 2 0 0 1-1.66 0L2 17.65M22 12.65l-9.17 4.16a2 2 0 0 1-1.66 0L2 12.65"/></symbol><symbol id="i-message" viewBox="0 0 24 24"><path d="M7.9 20A9 9 0 1 0 4 16.1L2 22Z"/></symbol><symbol id="i-book" viewBox="0 0 24 24"><path d="M12 7v14M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z"/></symbol><symbol id="i-cap" viewBox="0 0 24 24"><path d="M21.42 10.922a1 1 0 0 0-.019-1.838L12.83 5.18a2 2 0 0 0-1.66 0L2.6 9.08a1 1 0 0 0 0 1.832l8.57 3.908a2 2 0 0 0 1.66 0zM22 10v6M6 12.5V16a6 3 0 0 0 12 0v-3.5"/></symbol><symbol id="i-cards" viewBox="0 0 24 24"><rect width="12" height="16" x="3" y="5" rx="2"/><path d="M19 7.5 21 8a1 1 0 0 1 .7 1.2l-3 11.6a1 1 0 0 1-1.2.7L14 20.6"/></symbol><symbol id="i-flame" viewBox="0 0 24 24"><path d="M8.5 14.5A2.5 2.5 0 0 0 11 12c0-1.38-.5-2-1-3-1.072-2.143-.224-4.054 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 1 1-14 0c0-1.153.433-2.294 1-3a2.5 2.5 0 0 0 2.5 2.5z"/></symbol><symbol id="i-star" viewBox="0 0 24 24"><path d="M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z"/></symbol><symbol id="i-upload" viewBox="0 0 24 24"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M17 8l-5-5-5 5M12 3v12"/></symbol><symbol id="i-sliders" viewBox="0 0 24 24"><path d="M21 4h-7M10 4H3M21 12h-9M8 12H3M21 20h-5M12 20H3M14 2v4M8 10v4M16 18v4"/></symbol><symbol id="i-heart" viewBox="0 0 24 24"><path d="M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.3 1.5 4.05 3 5.5l7 7Z"/></symbol><symbol id="i-refresh" viewBox="0 0 24 24"><path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8M21 3v5h-5M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16M8 16H3v5"/></symbol><symbol id="i-more" viewBox="0 0 24 24"><circle cx="12" cy="12" r="1"/><circle cx="19" cy="12" r="1"/><circle cx="5" cy="12" r="1"/></symbol><symbol id="sb-logo" viewBox="0 0 32 32"><circle cx="16" cy="16" r="13.6" fill="none" stroke="currentColor" stroke-width="2.6"/><path d="M17.8 6.8 9.6 17.9h5.6l-1.6 7.3 8.6-11.7h-5.9z" fill="currentColor"/></symbol><symbol id="hoot-body" viewBox="0 0 120 130"><ellipse cx="60" cy="124" rx="34" ry="4" fill="#000" opacity=".12"/>
<ellipse cx="60" cy="75" rx="44" ry="48" fill="#8c5b3a"/>
<ellipse cx="18" cy="86" rx="11" ry="25" fill="#704529" transform="rotate(14 18 86)"/>
<ellipse cx="102" cy="86" rx="11" ry="25" fill="#704529" transform="rotate(-14 102 86)"/>
<ellipse cx="60" cy="94" rx="27" ry="26" fill="#ebcfa9"/>
<path d="M45 88q4.5 5 9 0M57 88q4.5 5 9 0M69 88q-4.5 5-9 0M51 98q4.5 5 9 0M63 98q4.5 5 9 0M57 108q4.5 4 9 0" fill="none" stroke="#c9a175" stroke-width="2.2" stroke-linecap="round"/>
<path d="M60 41C45 29 19 34 21 57c2 18 24 20 39 10 15 10 37 8 39-10 2-23-24-28-39-16Z" fill="#f3e0c4"/>
<circle cx="41" cy="55" r="14.5" fill="#fff" stroke="#14a594" stroke-width="4.2"/>
<circle cx="79" cy="55" r="14.5" fill="#fff" stroke="#14a594" stroke-width="4.2"/>
<path d="M55.5 53q4.5-4 9 0" fill="none" stroke="#14a594" stroke-width="4.2" stroke-linecap="round"/>
<circle cx="43" cy="57" r="6.4" fill="#22160f"/>
<circle cx="81" cy="57" r="6.4" fill="#22160f"/>
<circle cx="45.4" cy="54.4" r="2.2" fill="#fff"/>
<circle cx="83.4" cy="54.4" r="2.2" fill="#fff"/>
<path d="M54.5 69h11L60 79z" fill="#eda23a"/>
<path d="M47 122.5h9M64 122.5h9" stroke="#eda23a" stroke-width="5" stroke-linecap="round"/></symbol><symbol id="hoot" viewBox="0 0 120 130"><path d="M29 38 22 9l26 19Z" fill="#704529"/>
<path d="M91 38 98 9 72 28Z" fill="#704529"/>
<use href="#hoot-body"/></symbol><symbol id="hoot-prof" viewBox="0 0 120 130"><use href="#hoot-body"/>
<path d="M38 22v10c8 6 36 6 44 0V22Z" fill="#1d2b33"/>
<path d="M60 4 101 19 60 33 19 19Z" fill="#26363f"/>
<path d="M60 4 101 19 60 33 19 19Z" fill="none" stroke="#33474f" stroke-width="1.5" stroke-linejoin="round"/>
<circle cx="60" cy="19" r="2.6" fill="#e9b23c"/>
<path d="M60 19 92 25v13" fill="none" stroke="#e9b23c" stroke-width="2.4" stroke-linecap="round"/>
<path d="M89 37h6l-1 7h-4Z" fill="#e9b23c"/></symbol><symbol id="i-pin" viewBox="0 0 24 24"><path d="M12 21s-7-6.2-7-11.5a7 7 0 0 1 14 0C19 14.8 12 21 12 21z"/><circle cx="12" cy="9.5" r="2.5"/></symbol><symbol id="i-room" viewBox="0 0 24 24"><path d="M3 19v-6h18v6M3 19v2M21 19v2M3 13V7a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v6"/></symbol><symbol id="i-doc" viewBox="0 0 24 24"><path d="M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8z"/><path d="M14 3v5h5M9 13h6M9 17h4"/></symbol><symbol id="i-game" viewBox="0 0 24 24"><rect x="2.5" y="7" width="19" height="10" rx="5"/><path d="M7 10.5v3M5.5 12h3M15.5 11h.01M18 13h.01"/></symbol><symbol id="i-chev-r" viewBox="0 0 24 24"><path d="m9 6 6 6-6 6"/></symbol><symbol id="i-chev-l" viewBox="0 0 24 24"><path d="m15 6-6 6 6 6"/></symbol><symbol id="i-chev-d" viewBox="0 0 24 24"><path d="m6 9 6 6 6-6"/></symbol><symbol id="i-arrow-r" viewBox="0 0 24 24"><path d="M5 12h14M13 6l6 6-6 6"/></symbol><symbol id="i-mail" viewBox="0 0 24 24"><rect x="3" y="5" width="18" height="14" rx="2"/><path d="m3 7 9 6 9-6"/></symbol><symbol id="i-hash" viewBox="0 0 24 24"><path d="M5 9h14M5 15h14M10 4 8 20M16 4l-2 16"/></symbol><symbol id="i-target" viewBox="0 0 24 24"><circle cx="12" cy="12" r="7"/><circle cx="12" cy="12" r="2.2"/><path d="M12 2v3M12 19v3M2 12h3M19 12h3"/></symbol><symbol id="i-menu" viewBox="0 0 24 24"><path d="M4 7h16M4 12h16M4 17h16"/></symbol><symbol id="i-abc" viewBox="0 0 24 24"><path d="M3 17l3.5-10L10 17M4.3 13.5h4.4M13.5 7h3.2a2.3 2.3 0 0 1 0 4.6h-3.2zM13.5 11.6h3.8a2.7 2.7 0 0 1 0 5.4h-3.8z"/></symbol><symbol id="i-paw" viewBox="0 0 24 24"><circle cx="5.5" cy="10.5" r="1.8"/><circle cx="9.5" cy="6.5" r="1.8"/><circle cx="14.5" cy="6.5" r="1.8"/><circle cx="18.5" cy="10.5" r="1.8"/><path d="M8 17.5c0-3 1.8-5.5 4-5.5s4 2.5 4 5.5c0 1.6-1.6 2.5-4 2.5s-4-.9-4-2.5z"/></symbol><symbol id="i-building" viewBox="0 0 24 24"><path d="M3 21h18M5 21V10M19 21V10M9.5 21v-5h5v5M2 10l10-6 10 6M9 13h.01M15 13h.01"/></symbol><symbol id="i-laptop" viewBox="0 0 24 24"><rect x="4" y="5" width="16" height="11" rx="1.5"/><path d="M2 19h20"/></symbol><symbol id="i-pencil" viewBox="0 0 24 24"><path d="M4 20l1-4L16.5 4.5a2.1 2.1 0 0 1 3 3L8 19z"/><path d="m14.5 6.5 3 3"/></symbol><symbol id="i-exam" viewBox="0 0 24 24"><rect x="5" y="3" width="14" height="18" rx="2"/><path d="m9 13 2 2 4-4M9 7.5h6"/></symbol><symbol id="i-send" viewBox="0 0 24 24"><path d="M4 12 20 4l-6 16-2.5-6.5z"/><path d="m11.5 13.5 3-3"/></symbol><symbol id="c1" viewBox="0 0 24 24"><path d="M8 8l-4 4 4 4M16 8l4 4-4 4M13.5 5.5l-3 13"/></symbol><symbol id="c2" viewBox="0 0 24 24"><path d="M5 6.5h6M8 3.5v6M14 6.5h5M5 15.5l5 5M10 15.5l-5 5M14 16h5M14 20h5"/></symbol><symbol id="c3" viewBox="0 0 24 24"><circle cx="12" cy="12" r="3"/><path d="M12 3v3M12 18v3M3 12h3M18 12h3M5.6 5.6l2.1 2.1M16.3 16.3l2.1 2.1M5.6 18.4l2.1-2.1M16.3 7.7l2.1-2.1"/></symbol><symbol id="c4" viewBox="0 0 24 24"><path d="M5 19c0-8 5-14 14-14 0 9-6 14-14 14z"/><path d="M5 19l7-7"/></symbol><symbol id="c5" viewBox="0 0 24 24"><path d="M4 20h16M7 16v-4M12 16V8M17 16V5"/></symbol><symbol id="c6" viewBox="0 0 24 24"><circle cx="9" cy="8" r="3"/><path d="M3 20c0-3.3 2.7-6 6-6s6 2.7 6 6"/><path d="M16 5.2a3 3 0 0 1 0 5.6M18 14.4c1.8.8 3 2.6 3 4.6"/></symbol><symbol id="c7" viewBox="0 0 24 24"><path d="M3 5.5C5.5 4.5 9 4.5 12 6.5c3-2 6.5-2 9-1V19c-2.5-1-6-1-9 1-3-2-6.5-2-9-1z"/><path d="M12 6.5V20"/></symbol><symbol id="c8" viewBox="0 0 24 24"><path d="M12 20s-8-4.6-8-10.2A4.3 4.3 0 0 1 12 7.5a4.3 4.3 0 0 1 8 2.3C20 15.4 12 20 12 20z"/><path d="M7 12h2.5l1.5-2.5 2 4.5 1.5-2h2.5"/></symbol><symbol id="c9" viewBox="0 0 24 24"><path d="M12 4v16M7.5 20h9M5 7h14"/><path d="M5 7l-2.5 5.5a2.6 2.6 0 0 0 5 0zM19 7l-2.5 5.5a2.6 2.6 0 0 0 5 0z"/></symbol><symbol id="c10" viewBox="0 0 24 24"><path d="M4 5h16v11H9.5L4 20z"/><path d="M8 9h8M8 12.5h5"/></symbol><symbol id="hoot64" viewBox="0 0 64 64"><path d="M15 15 19.5 4 25.5 14z" fill="#6B4426"/>
<path d="M49 15 44.5 4 38.5 14z" fill="#6B4426"/>
<ellipse cx="32" cy="36" rx="21" ry="23" fill="#8A5A36"/>
<path d="M12 34c-3 10 1 18 8 21-3-7-3-14 0-21z" fill="#6B4426"/>
<path d="M52 34c3 10-1 18-8 21 3-7 3-14 0-21z" fill="#6B4426"/>
<ellipse cx="32" cy="45.5" rx="12.5" ry="12" fill="#E2C49B"/>
<path d="M26.5 43.5q2 2 4 0M33.5 43.5q2 2 4 0M30 49q2 2 4 0" stroke="#B9895C" stroke-width="1.5" fill="none" stroke-linecap="round"/>
<ellipse cx="32" cy="27" rx="18" ry="13.5" fill="#B9895C"/>
<circle cx="24" cy="26" r="7" fill="#FFFFFF"/>
<circle cx="40" cy="26" r="7" fill="#FFFFFF"/>
<circle cx="24.8" cy="26.8" r="3.2" fill="#1E140D"/>
<circle cx="40.8" cy="26.8" r="3.2" fill="#1E140D"/>
<circle cx="25.9" cy="25.6" r="1" fill="#FFFFFF"/>
<circle cx="41.9" cy="25.6" r="1" fill="#FFFFFF"/>
<circle cx="24" cy="26" r="8.2" fill="none" stroke="#12A089" stroke-width="2.6"/>
<circle cx="40" cy="26" r="8.2" fill="none" stroke="#12A089" stroke-width="2.6"/>
<path d="M30.2 25.2q1.8-1.6 3.6 0" stroke="#12A089" stroke-width="2.6" fill="none" stroke-linecap="round"/>
<path d="M29.5 32.5h5L32 37.5z" fill="#EDA33A"/>
<path d="M26 58.5h4.5M33.5 58.5H38" stroke="#EDA33A" stroke-width="2.6" stroke-linecap="round"/></symbol><symbol id="i-logo" viewBox="0 0 32 32"><circle cx="16" cy="16" r="13" fill="none"/><path d="M17.6 7.2 10.4 17.6h5.2l-1.2 7.2 7.2-10.4h-5.2z" fill="currentColor" stroke="none"/></symbol><symbol id="i-cal" viewBox="0 0 24 24"><rect width="18" height="18" x="3" y="4" rx="2"/><path d="M16 2v4M8 2v4M3 10h18"/></symbol><symbol id="i-chev" viewBox="0 0 24 24"><path d="m9 18 6-6-6-6"/></symbol><symbol id="i-bookmark" viewBox="0 0 24 24"><path d="m19 21-7-4-7 4V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v16z"/></symbol><symbol id="i-notebook" viewBox="0 0 24 24"><path d="M2 6h4M2 10h4M2 14h4M2 18h4"/><rect width="16" height="20" x="4" y="2" rx="2"/><path d="M15 2v20"/></symbol><symbol id="i-sparkles" viewBox="0 0 24 24"><path d="m12 3-1.9 5.8a2 2 0 0 1-1.3 1.3L3 12l5.8 1.9a2 2 0 0 1 1.3 1.3L12 21l1.9-5.8a2 2 0 0 1 1.3-1.3L21 12l-5.8-1.9a2 2 0 0 1-1.3-1.3Z"/><path d="M19 2v4M21 4h-4"/></symbol><symbol id="i-max" viewBox="0 0 24 24"><path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7"/></symbol><symbol id="i-minus" viewBox="0 0 24 24"><path d="M5 12h14"/></symbol><symbol id="i-zin" viewBox="0 0 24 24"><circle cx="11" cy="11" r="7.5"/><path d="m21 21-4.6-4.6M11 8v6M8 11h6"/></symbol><symbol id="i-zout" viewBox="0 0 24 24"><circle cx="11" cy="11" r="7.5"/><path d="m21 21-4.6-4.6M8 11h6"/></symbol><symbol id="i-rotate" viewBox="0 0 24 24"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/></symbol><symbol id="i-chat" viewBox="0 0 24 24"><path d="M7.9 20A9 9 0 1 0 4 16.1L2 22Z"/></symbol><symbol id="i-quiz" viewBox="0 0 24 24"><path d="m3 17 2 2 4-4M3 7l2 2 4-4M13 6h8M13 12h8M13 18h8"/></symbol><symbol id="i-text" viewBox="0 0 24 24"><path d="M17 6.1H3M21 12.1H3M15.1 18H3"/></symbol><symbol id="i-docs" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M7 3h7l5 5v13H7z"/><path d="M14 3v5h5"/><path d="M10 13h6M10 17h6"/></g></symbol><symbol id="i-shelf" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round"><path d="M4 20h16M6 17V7M10 17V5M14 17v-8M18 17l-2.5-9"/></g></symbol><symbol id="i-chevup" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 15l6-6 6 6"/></g></symbol><symbol id="i-leaf" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M5 19c0-8 5-13 14-14-1 9-6 14-14 14z"/><path d="M5 19l7-7"/></g></symbol><symbol id="i-loc" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-width="2"><path d="M12 21s7-6.2 7-11.5a7 7 0 0 0-14 0C5 14.8 12 21 12 21z"/><circle cx="12" cy="9.5" r="2.5"/></g></symbol><symbol id="i-settings" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><circle cx="12" cy="12" r="3.2"/><path d="M12 2.5v3M12 18.5v3M2.5 12h3M18.5 12h3M5.3 5.3l2.1 2.1M16.6 16.6l2.1 2.1M5.3 18.7l2.1-2.1M16.6 7.4l2.1-2.1"/></g></symbol><symbol id="i-help" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><circle cx="12" cy="12" r="9"/><path d="M9.6 9.3a2.5 2.5 0 1 1 3.4 2.3c-.6.3-1 .9-1 1.6v.4M12 16.8h.01"/></g></symbol><symbol id="i-out" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M14 4h5v16h-5M10 8l-4 4 4 4M6 12h10"/></g></symbol><symbol id="i-replay" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4.5 12a7.5 7.5 0 1 0 2.2-5.3"/><path d="M4.5 4.5v4h4"/></g></symbol><symbol id="i-mask" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M5 4h14v7a7 7 0 0 1-14 0z"/><path d="M9 9h.01M15 9h.01M9 13c1.6 1.4 4.4 1.4 6 0"/></g></symbol><symbol id="i-sum" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><rect x="5.5" y="3" width="13" height="18" rx="2"/><path d="M9 8h6M9 12h6M9 16h3.5"/></g></symbol><symbol id="i-q" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><rect x="3.5" y="4" width="17" height="16" rx="3"/><path d="M7.5 9h9M7.5 13h6M7.5 16.5h3"/></g></symbol><symbol id="i-hang" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><path d="M4 21h9M7 21V3h8v3"/><circle cx="15" cy="9" r="2.4"/><path d="M15 11.5v4M13 14l2-1 2 1"/></g></symbol><symbol id="i-type" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"><rect x="3" y="7" width="18" height="11" rx="2"/><path d="M7 11h.01M10.5 11h.01M14 11h.01M17.5 11h.01M8 14.5h8"/></g></symbol><symbol id="i-spark" viewBox="0 0 24 24"><path d="M12 3c.6 4.5 2.5 6.4 7 7-4.5.6-6.4 2.5-7 7-.6-4.5-2.5-6.4-7-7 4.5-.6 6.4-2.5 7-7Z"/><path d="M19 17c.2 1.4.8 2 2 2.2-1.2.2-1.8.8-2 2.2-.2-1.4-.8-2-2-2.2 1.2-.2 1.8-.8 2-2.2Z"/></symbol><symbol id="i-note" viewBox="0 0 24 24"><path d="M5 4h11l3 3v13H5Z"/><path d="M9 10h6M9 14h6"/></symbol><symbol id="i-user" viewBox="0 0 24 24"><circle cx="12" cy="8" r="4"/><path d="M4 21c1-4 4-6 8-6s7 2 8 6"/></symbol><symbol id="i-gear" viewBox="0 0 24 24"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.7 1.7 0 0 0 .3 1.8l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.7 1.7 0 0 0-1.8-.3 1.7 1.7 0 0 0-1 1.5V21a2 2 0 1 1-4 0v-.1a1.7 1.7 0 0 0-1.1-1.5 1.7 1.7 0 0 0-1.8.3l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1a1.7 1.7 0 0 0 .3-1.8 1.7 1.7 0 0 0-1.5-1H3a2 2 0 1 1 0-4h.1a1.7 1.7 0 0 0 1.5-1.1 1.7 1.7 0 0 0-.3-1.8l-.1-.1a2 2 0 1 1 2.8-2.8l.1.1a1.7 1.7 0 0 0 1.8.3H9a1.7 1.7 0 0 0 1-1.5V3a2 2 0 1 1 4 0v.1a1.7 1.7 0 0 0 1 1.5 1.7 1.7 0 0 0 1.8-.3l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.7 1.7 0 0 0-.3 1.8V9a1.7 1.7 0 0 0 1.5 1H21a2 2 0 1 1 0 4h-.1a1.7 1.7 0 0 0-1.5 1Z"/></symbol><symbol id="i-camera" viewBox="0 0 24 24"><path d="M4 8a2 2 0 0 1 2-2h2l1.5-2h5L16 6h2a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2Z"/><circle cx="12" cy="13" r="3.5"/></symbol><symbol id="i-mic" viewBox="0 0 24 24"><rect x="9" y="3" width="6" height="11" rx="3"/><path d="M5.5 11a6.5 6.5 0 0 0 13 0M12 17.5V21"/></symbol><symbol id="i-vault" viewBox="0 0 24 24"><rect width="18" height="18" x="3" y="3" rx="2"/><circle cx="12" cy="12" r="4.5"/><path d="M12 7.5V10M12 14v2.5M7.5 12H10M14 12h2.5M6 21v1M18 21v1"/></symbol><symbol id="i-tower" viewBox="0 0 24 24"><path d="M22 20v-9H2v9a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2Z"/><path d="M18 11V4H6v7"/><path d="M15 22v-4a3 3 0 0 0-6 0v4M22 11V9M2 11V9M6 4V2M18 4V2M10 4V2M14 4V2"/></symbol><symbol id="i-door" viewBox="0 0 24 24"><path d="M18 20V6a2 2 0 0 0-2-2H8a2 2 0 0 0-2 2v14"/><path d="M2 20h20M14 12v.01"/></symbol><symbol id="i-calendar" viewBox="0 0 24 24"><rect width="18" height="18" x="3" y="4" rx="2"/><path d="M16 2v4M8 2v4M3 10h18M8 14h.01M12 14h.01M16 14h.01M8 18h.01M12 18h.01"/></symbol><symbol id="i-list" viewBox="0 0 24 24"><path d="M8 6h13M8 12h13M8 18h13M3 6h.01M3 12h.01M3 18h.01"/></symbol><symbol id="i-smile" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><path d="M8 14s1.5 2 4 2 4-2 4-2M9 9h.01M15 9h.01"/></symbol><symbol id="i-unlock" viewBox="0 0 24 24"><rect width="18" height="11" x="3" y="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 9.9-1"/></symbol><symbol id="i-volume" viewBox="0 0 24 24"><path d="M11 4.7a.7.7 0 0 0-1.2-.5L6.4 7.6A1.4 1.4 0 0 1 5.4 8H3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2.4a1.4 1.4 0 0 1 1 .4l3.4 3.4a.7.7 0 0 0 1.2-.5z"/><path d="M16 9a5 5 0 0 1 0 6M19.4 18.4a9 9 0 0 0 0-12.8"/></symbol><symbol id="i-palette" viewBox="0 0 24 24"><circle cx="13.5" cy="6.5" r=".5"/><circle cx="17.5" cy="10.5" r=".5"/><circle cx="8.5" cy="7.5" r=".5"/><circle cx="6.5" cy="12.5" r=".5"/><path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10c.93 0 1.65-.75 1.65-1.69 0-.44-.18-.84-.44-1.13-.29-.29-.44-.65-.44-1.12a1.64 1.64 0 0 1 1.67-1.67h2c3.05 0 5.55-2.5 5.55-5.55C21.97 6.01 17.46 2 12 2z"/></symbol><symbol id="i-kanban" viewBox="0 0 24 24"><rect x="3" y="4" width="5" height="16" rx="1.5"/><rect x="9.5" y="4" width="5" height="11" rx="1.5"/><rect x="16" y="4" width="5" height="7" rx="1.5"/></symbol><symbol id="i-table" viewBox="0 0 24 24"><rect x="3" y="5" width="18" height="14" rx="2"/><path d="M3 10h18M3 14.5h18M9 10v9M15 10v9"/></symbol><symbol id="i-grip" viewBox="0 0 24 24"><circle cx="9" cy="6" r="1.2"/><circle cx="15" cy="6" r="1.2"/><circle cx="9" cy="12" r="1.2"/><circle cx="15" cy="12" r="1.2"/><circle cx="9" cy="18" r="1.2"/><circle cx="15" cy="18" r="1.2"/></symbol><symbol id="i-filter" viewBox="0 0 24 24"><path d="M4 5h16l-6 7v6l-4 2v-8z"/></symbol><symbol id="i-inbox" viewBox="0 0 24 24"><path d="M22 12h-6l-2 3h-4l-2-3H2"/><path d="M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"/></symbol><symbol id="i-pennant" viewBox="0 0 24 24"><path d="M5 21V3l14 6-14 6"/></symbol><symbol id="i-external" viewBox="0 0 24 24"><path d="M15 3h6v6M10 14 21 3M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/></symbol><symbol id="i-link" viewBox="0 0 24 24"><path d="M10 13a5 5 0 0 0 7.5.5l3-3a5 5 0 0 0-7-7l-1.7 1.7"/><path d="M14 11a5 5 0 0 0-7.5-.5l-3 3a5 5 0 0 0 7 7l1.7-1.7"/></symbol><symbol id="i-edit" viewBox="0 0 24 24"><path d="M12 20h9"/><path d="M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4z"/></symbol><symbol id="i-rows" viewBox="0 0 24 24"><rect x="3" y="4" width="18" height="6" rx="1.5"/><rect x="3" y="14" width="18" height="6" rx="1.5"/></symbol><symbol id="i-chev-u" viewBox="0 0 24 24"><path d="m6 15 6-6 6 6"/></symbol><symbol id="i-flask" viewBox="0 0 24 24"><path d="M9 3h6M10 3v6L5 18.8A1.6 1.6 0 0 0 6.4 21h11.2a1.6 1.6 0 0 0 1.4-2.2L14 9V3"/><path d="M7.6 15h8.8"/></symbol><symbol id="i-trash" viewBox="0 0 24 24"><path d="M3 6h18M8 6V4h8v2M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/></symbol><symbol id="i-play-sm" viewBox="0 0 24 24"><path d="M7 4v16l13-8z"/></symbol><symbol id="i-bookmark-fill" viewBox="0 0 24 24"><path d="M6 3h12v18l-6-4-6 4z" fill="currentColor"/></symbol><symbol id="i-thumb-down" viewBox="0 0 24 24"><path d="M17 14V2"/><path d="M9 18.12 10 14H4.17a2 2 0 0 1-1.92-2.56l2.33-8A2 2 0 0 1 6.5 2H20a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2h-2.76a2 2 0 0 0-1.79 1.11L12 22a3.13 3.13 0 0 1-3-3.88Z"/></symbol></defs></svg>
<div class="app" id="app">
<div class="amb" aria-hidden="true"><i class="b1"></i><i class="b2"></i><i class="b3"></i><i class="b4"></i><i class="lw"></i><i class="lc"></i></div>
<aside class="nav" id="nav" aria-label="Main navigation"></aside>
<aside class="rpanel" id="rpanel" aria-label="Room panel" hidden></aside>
<div class="main">
<header class="top" id="top"></header>
<main class="page" id="page" tabindex="-1"></main>
</div>
</div>
<div id="hoot-root"></div>
<button type="button" class="proto" id="proto" data-act="proto" aria-haspopup="dialog" aria-expanded="false"><svg class="i" aria-hidden="true"><use href="#i-sliders"/></svg>Prototype</button>
<div id="ovl"></div>
<noscript><p style="padding:24px;font:16px system-ui">This prototype needs JavaScript.</p></noscript>
<script>
const CAT_ART = {"1": "<svg class=\"cat-art\" viewBox=\"0 0 320 240\" aria-hidden=\"true\"> <circle cx=\"160\" cy=\"118\" r=\"96\" fill=\"#C9CFFB\" opacity=\"0.45\"/> <ellipse cx=\"160\" cy=\"204\" rx=\"122\" ry=\"9\" fill=\"#3340A8\" opacity=\"0.1\"/> <g> <animateTransform attributeName=\"transform\" type=\"translate\" values=\"0 0;0 -5;0 0\" dur=\"4.5s\" repeatCount=\"indefinite\"/> <rect x=\"28\" y=\"34\" width=\"64\" height=\"48\" rx=\"9\" fill=\"#FFFFFF\" stroke=\"#3340A8\" stroke-width=\"3.5\"/> <path d=\"M28 49 H92\" stroke=\"#3340A8\" stroke-width=\"3\"/> <circle cx=\"38\" cy=\"41.5\" r=\"2.6\" fill=\"#5B6BEA\"/> <circle cx=\"46.5\" cy=\"41.5\" r=\"2.6\" fill=\"#C9CFFB\"/> <circle cx=\"55\" cy=\"41.5\" r=\"2.6\" fill=\"#F4B63F\"/> <path d=\"M39 61 H74 M39 71 H62\" stroke=\"#C9CFFB\" stroke-width=\"4\" stroke-linecap=\"round\"/> </g> <rect x=\"84\" y=\"54\" width=\"152\" height=\"106\" rx=\"12\" fill=\"#FFFFFF\" stroke=\"#3340A8\" stroke-width=\"4\"/> <rect x=\"96\" y=\"66\" width=\"128\" height=\"82\" rx=\"6\" fill=\"#3340A8\"/> <path d=\"M134 90 L120 107 L134 124\" fill=\"none\" stroke=\"#C9CFFB\" stroke-width=\"6\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/> <path d=\"M186 90 L200 107 L186 124\" fill=\"none\" stroke=\"#C9CFFB\" stroke-width=\"6\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/> <path d=\"M168 86 L152 128\" stroke=\"#F4B63F\" stroke-width=\"6\" stroke-linecap=\"round\"/> <rect x=\"206\" y=\"132\" width=\"11\" height=\"4\" rx=\"2\" fill=\"#F4B63F\"> <animate attributeName=\"opacity\" values=\"1;1;0;0\" keyTimes=\"0;0.5;0.5;1\" dur=\"1.1s\" repeatCount=\"indefinite\"/> </rect> <path d=\"M66 160 H254 L268 175 Q271 182 262 182 H58 Q49 182 52 175 Z\" fill=\"#C9CFFB\" stroke=\"#3340A8\" stroke-width=\"4\" stroke-linejoin=\"round\"/> <path d=\"M140 160 H180 V163 Q180 167 176 167 H144 Q140 167 140 163 Z\" fill=\"#5B6BEA\"/> <path d=\"M264 92 L288 124 L260 146 Z\" fill=\"none\" stroke=\"#5B6BEA\" stroke-width=\"3\" stroke-linejoin=\"round\"/> <circle cx=\"264\" cy=\"92\" r=\"8\" fill=\"#FFFFFF\" stroke=\"#3340A8\" stroke-width=\"3.5\"/> <circle cx=\"288\" cy=\"124\" r=\"8\" fill=\"#5B6BEA\" stroke=\"#3340A8\" stroke-width=\"3.5\"/> <circle cx=\"260\" cy=\"146\" r=\"6\" fill=\"#FFFFFF\" stroke=\"#3340A8\" stroke-width=\"3.5\"/> <g transform=\"translate(270 44)\"> <path d=\"M0 -12 C1.2 -4 4 -1.2 12 0 C4 1.2 1.2 4 0 12 C-1.2 4 -4 1.2 -12 0 C-4 -1.2 -1.2 -4 0 -12 Z\" fill=\"#F4B63F\"> <animate attributeName=\"opacity\" values=\"1;0.35;1\" dur=\"3.2s\" repeatCount=\"indefinite\"/> </path> </g> <circle cx=\"40\" cy=\"198\" r=\"4\" fill=\"#5B6BEA\" opacity=\"0.5\"/> <circle cx=\"292\" cy=\"186\" r=\"3\" fill=\"#3340A8\" opacity=\"0.35\"/> </svg>", "2": "<svg class=\"cat-art\" viewBox=\"0 0 320 240\" aria-hidden=\"true\"> <circle cx=\"160\" cy=\"118\" r=\"96\" fill=\"#DCCFFA\" opacity=\"0.45\"/> <ellipse cx=\"160\" cy=\"206\" rx=\"122\" ry=\"9\" fill=\"#5B36B0\" opacity=\"0.1\"/> <g transform=\"rotate(-6 140 124)\"> <rect x=\"62\" y=\"62\" width=\"156\" height=\"120\" rx=\"10\" fill=\"#FFFFFF\" stroke=\"#5B36B0\" stroke-width=\"4\"/> <path d=\"M88 64 V180 M114 64 V180 M140 64 V180 M166 64 V180 M192 64 V180 M64 88 H216 M64 112 H216 M64 136 H216 M64 160 H216\" stroke=\"#DCCFFA\" stroke-width=\"2\"/> <path d=\"M80 168 H204 M80 168 V76\" stroke=\"#5B36B0\" stroke-width=\"3\" stroke-linecap=\"round\"/> <path d=\"M80 150 C100 90 120 86 138 120 C156 154 176 150 200 92\" fill=\"none\" stroke=\"#8C66E0\" stroke-width=\"5\" stroke-linecap=\"round\"/> <circle cx=\"138\" cy=\"120\" r=\"5.5\" fill=\"#F4B63F\" stroke=\"#5B36B0\" stroke-width=\"2.5\"/> </g> <path d=\"M34 198 V134 L98 198 Z\" fill=\"#DCCFFA\" stroke=\"#5B36B0\" stroke-width=\"4\" stroke-linejoin=\"round\"/> <path d=\"M46 186 V163 L69 186 Z\" fill=\"#FFFFFF\" stroke=\"#5B36B0\" stroke-width=\"3\" stroke-linejoin=\"round\"/> <g transform=\"translate(246 80)\"> <ellipse rx=\"36\" ry=\"13\" fill=\"none\" stroke=\"#8C66E0\" stroke-width=\"3\"/> <ellipse rx=\"36\" ry=\"13\" fill=\"none\" stroke=\"#8C66E0\" stroke-width=\"3\" transform=\"rotate(60)\"/> <ellipse rx=\"36\" ry=\"13\" fill=\"none\" stroke=\"#8C66E0\" stroke-width=\"3\" transform=\"rotate(120)\"/> <g transform=\"rotate(60)\"> <circle r=\"5\" fill=\"#5B36B0\"> <animateMotion dur=\"5s\" repeatCount=\"indefinite\" path=\"M36 0 A36 13 0 1 1 -36 0 A36 13 0 1 1 36 0\"/> </circle> </g> <circle r=\"10\" fill=\"#F4B63F\" stroke=\"#5B36B0\" stroke-width=\"3\"/> </g> <g transform=\"translate(266 176) rotate(-18)\"> <circle r=\"17\" fill=\"#8C66E0\" stroke=\"#5B36B0\" stroke-width=\"3.5\"/> <ellipse rx=\"31\" ry=\"8\" fill=\"none\" stroke=\"#5B36B0\" stroke-width=\"3.5\"/> </g> <g transform=\"translate(52 64)\"> <path d=\"M0 -12 C1.2 -4 4 -1.2 12 0 C4 1.2 1.2 4 0 12 C-1.2 4 -4 1.2 -12 0 C-4 -1.2 -1.2 -4 0 -12 Z\" fill=\"#F4B63F\"> <animate attributeName=\"opacity\" values=\"1;0.35;1\" dur=\"3.2s\" repeatCount=\"indefinite\"/> </path> </g> <circle cx=\"300\" cy=\"128\" r=\"3.5\" fill=\"#8C66E0\" opacity=\"0.5\"/> <circle cx=\"118\" cy=\"40\" r=\"3\" fill=\"#5B36B0\" opacity=\"0.35\"/> </svg>", "3": "<svg class=\"cat-art\" viewBox=\"0 0 320 240\" aria-hidden=\"true\"> <circle cx=\"160\" cy=\"118\" r=\"96\" fill=\"#FDD5B5\" opacity=\"0.5\"/> <ellipse cx=\"160\" cy=\"206\" rx=\"122\" ry=\"9\" fill=\"#A3480B\" opacity=\"0.1\"/> <path d=\"M36 44 H60 M36 44 V68 M284 196 H260 M284 196 V172\" stroke=\"#F08A3C\" stroke-width=\"3\" stroke-linecap=\"round\" opacity=\"0.7\"/> <g transform=\"translate(132 124)\"> <g> <animateTransform attributeName=\"transform\" type=\"rotate\" from=\"0\" to=\"360\" dur=\"18s\" repeatCount=\"indefinite\"/> <circle r=\"50\" fill=\"none\" stroke=\"#A3480B\" stroke-width=\"14\" stroke-dasharray=\"10.47 10.47\"/> <circle r=\"44\" fill=\"#F08A3C\" stroke=\"#A3480B\" stroke-width=\"4\"/> <circle r=\"30\" fill=\"none\" stroke=\"#FDD5B5\" stroke-width=\"3\" stroke-dasharray=\"4 7\"/> <circle r=\"15\" fill=\"#FFFFFF\" stroke=\"#A3480B\" stroke-width=\"4\"/> <circle r=\"4\" fill=\"#A3480B\"/> </g> </g> <g transform=\"translate(196 76)\"> <g> <animateTransform attributeName=\"transform\" type=\"rotate\" from=\"360\" to=\"0\" dur=\"10.8s\" repeatCount=\"indefinite\"/> <circle r=\"30\" fill=\"none\" stroke=\"#A3480B\" stroke-width=\"12\" stroke-dasharray=\"10.47 10.47\"/> <circle r=\"25\" fill=\"#FDD5B5\" stroke=\"#A3480B\" stroke-width=\"4\"/> <circle r=\"9\" fill=\"#FFFFFF\" stroke=\"#A3480B\" stroke-width=\"3.5\"/> </g> </g> <g transform=\"rotate(-24 248 172)\"> <rect x=\"198\" y=\"160\" width=\"104\" height=\"24\" rx=\"5\" fill=\"#FDD5B5\" stroke=\"#A3480B\" stroke-width=\"3.5\"/> <path d=\"M212 160 V169 M224 160 V166 M236 160 V169 M248 160 V166 M260 160 V169 M272 160 V166 M284 160 V169\" stroke=\"#A3480B\" stroke-width=\"2.5\" stroke-linecap=\"round\"/> </g> <g transform=\"translate(58 186)\"> <path d=\"M14 0 L7 12.1 L-7 12.1 L-14 0 L-7 -12.1 L7 -12.1 Z\" fill=\"#FFFFFF\" stroke=\"#A3480B\" stroke-width=\"3.5\" stroke-linejoin=\"round\"/> <circle r=\"5\" fill=\"#F08A3C\" stroke=\"#A3480B\" stroke-width=\"2.5\"/> </g> <g transform=\"translate(272 46)\"> <path d=\"M0 -12 C1.2 -4 4 -1.2 12 0 C4 1.2 1.2 4 0 12 C-1.2 4 -4 1.2 -12 0 C-4 -1.2 -1.2 -4 0 -12 Z\" fill=\"#F4B63F\"> <animate attributeName=\"opacity\" values=\"1;0.35;1\" dur=\"3.2s\" repeatCount=\"indefinite\"/> </path> </g> <circle cx=\"246\" cy=\"118\" r=\"4\" fill=\"#F08A3C\" opacity=\"0.55\"/> <circle cx=\"64\" cy=\"98\" r=\"3\" fill=\"#A3480B\" opacity=\"0.35\"/> </svg>", "4": "<svg class=\"cat-art\" viewBox=\"0 0 320 240\" aria-hidden=\"true\"> <circle cx=\"160\" cy=\"118\" r=\"96\" fill=\"#BFE9CF\" opacity=\"0.5\"/> <ellipse cx=\"160\" cy=\"206\" rx=\"122\" ry=\"9\" fill=\"#17703F\" opacity=\"0.1\"/> <g> <animateTransform attributeName=\"transform\" type=\"rotate\" values=\"-4 160 62;4 160 62;-4 160 62\" dur=\"5s\" repeatCount=\"indefinite\"/> <path d=\"M160 62 V28\" stroke=\"#17703F\" stroke-width=\"4\" stroke-linecap=\"round\"/> <path d=\"M160 46 C146 46 135 36 135 21 C150 21 160 31 160 46 Z\" fill=\"#3FB477\" stroke=\"#17703F\" stroke-width=\"3.5\" stroke-linejoin=\"round\"/> <path d=\"M160 38 C173 38 184 29 186 16 C171 16 160 25 160 38 Z\" fill=\"#BFE9CF\" stroke=\"#17703F\" stroke-width=\"3.5\" stroke-linejoin=\"round\"/> </g> <path d=\"M146 64 V104 L110 170 Q104 186 122 186 H198 Q216 186 210 170 L174 104 V64\" fill=\"#FFFFFF\" stroke=\"#17703F\" stroke-width=\"4\" stroke-linejoin=\"round\"/> <path d=\"M127 140 Q143 132 160 140 Q177 148 193 140 L206 166 Q211 178 198 178 H122 Q109 178 114 166 Z\" fill=\"#3FB477\"/> <path d=\"M127 140 Q143 132 160 140 Q177 148 193 140\" fill=\"none\" stroke=\"#17703F\" stroke-width=\"3\" stroke-linecap=\"round\"/> <path d=\"M138 62 H182\" stroke=\"#17703F\" stroke-width=\"4.5\" stroke-linecap=\"round\"/> <path d=\"M174 116 H166 M180 128 H170\" stroke=\"#BFE9CF\" stroke-width=\"3\" stroke-linecap=\"round\"/> <circle cx=\"150\" cy=\"166\" r=\"5\" fill=\"#EAF7EF\" stroke=\"#17703F\" stroke-width=\"2\"> <animate attributeName=\"cy\" values=\"168;98;98\" keyTimes=\"0;0.85;1\" dur=\"3.4s\" repeatCount=\"indefinite\"/> <animate attributeName=\"opacity\" values=\"1;1;0\" keyTimes=\"0;0.7;1\" dur=\"3.4s\" repeatCount=\"indefinite\"/> </circle> <circle cx=\"164\" cy=\"170\" r=\"3.5\" fill=\"#EAF7EF\" stroke=\"#17703F\" stroke-width=\"2\"> <animate attributeName=\"cy\" values=\"172;92;92\" keyTimes=\"0;0.85;1\" dur=\"2.8s\" begin=\"1s\" repeatCount=\"indefinite\"/> <animate attributeName=\"opacity\" values=\"1;1;0\" keyTimes=\"0;0.7;1\" dur=\"2.8s\" begin=\"1s\" repeatCount=\"indefinite\"/> </circle> <circle cx=\"176\" cy=\"162\" r=\"4\" fill=\"#EAF7EF\" stroke=\"#17703F\" stroke-width=\"2\"> <animate attributeName=\"cy\" values=\"164;110;110\" keyTimes=\"0;0.85;1\" dur=\"3.1s\" begin=\"1.8s\" repeatCount=\"indefinite\"/> <animate attributeName=\"opacity\" values=\"1;1;0\" keyTimes=\"0;0.6;1\" dur=\"3.1s\" begin=\"1.8s\" repeatCount=\"indefinite\"/> </circle> <g transform=\"translate(250 0)\"> <path d=\"M-14 64 C-14 84 14 92 14 112 C14 132 -14 140 -14 160\" fill=\"none\" stroke=\"#17703F\" stroke-width=\"4\" stroke-linecap=\"round\"/> <path d=\"M14 64 C14 84 -14 92 -14 112 C-14 132 14 140 14 160\" fill=\"none\" stroke=\"#3FB477\" stroke-width=\"4\" stroke-linecap=\"round\"/> <path d=\"M-10 72 H10 M-9 103 H9 M-9 121 H9 M-10 152 H10\" stroke=\"#BFE9CF\" stroke-width=\"3.5\" stroke-linecap=\"round\"/> </g> <circle cx=\"64\" cy=\"170\" r=\"22\" fill=\"#BFE9CF\" stroke=\"#17703F\" stroke-width=\"3.5\"/> <circle cx=\"70\" cy=\"166\" r=\"8\" fill=\"#3FB477\" stroke=\"#17703F\" stroke-width=\"2.5\"/> <circle cx=\"54\" cy=\"178\" r=\"2.5\" fill=\"#17703F\"/> <circle cx=\"58\" cy=\"157\" r=\"2\" fill=\"#17703F\"/> <g transform=\"translate(278 42)\"> <path d=\"M0 -12 C1.2 -4 4 -1.2 12 0 C4 1.2 1.2 4 0 12 C-1.2 4 -4 1.2 -12 0 C-4 -1.2 -1.2 -4 0 -12 Z\" fill=\"#F4B63F\"> <animate attributeName=\"opacity\" values=\"1;0.35;1\" dur=\"3.2s\" repeatCount=\"indefinite\"/> </path> </g> <g transform=\"translate(56 72) scale(0.55)\"> <path d=\"M0 -12 C1.2 -4 4 -1.2 12 0 C4 1.2 1.2 4 0 12 C-1.2 4 -4 1.2 -12 0 C-4 -1.2 -1.2 -4 0 -12 Z\" fill=\"#F4B63F\"> <animate attributeName=\"opacity\" values=\"0.4;1;0.4\" dur=\"3.2s\" repeatCount=\"indefinite\"/> </path> </g> <circle cx=\"98\" cy=\"110\" r=\"3.5\" fill=\"#3FB477\" opacity=\"0.5\"/> </svg>", "5": "<svg class=\"cat-art\" viewBox=\"0 0 320 240\" aria-hidden=\"true\"> <circle cx=\"160\" cy=\"118\" r=\"96\" fill=\"#F8E2A0\" opacity=\"0.5\"/> <ellipse cx=\"160\" cy=\"206\" rx=\"122\" ry=\"9\" fill=\"#7E5A00\" opacity=\"0.1\"/> <rect x=\"44\" y=\"52\" width=\"148\" height=\"112\" rx=\"10\" fill=\"#FFFFFF\" stroke=\"#7E5A00\" stroke-width=\"4\"/> <rect x=\"48\" y=\"56\" width=\"140\" height=\"19\" rx=\"6\" fill=\"#F8E2A0\"/> <path d=\"M46 76 H190\" stroke=\"#7E5A00\" stroke-width=\"3\"/> <path d=\"M92 77 V162 M140 77 V162 M46 104 H190 M46 132 H190\" stroke=\"#F8E2A0\" stroke-width=\"2.5\"/> <path d=\"M56 90 H80 M104 90 H128 M152 90 H176 M56 118 H76 M104 118 H124 M152 118 H170 M56 147 H82 M104 147 H120\" stroke=\"#E2AE1F\" stroke-width=\"4\" stroke-linecap=\"round\"/> <rect x=\"168\" y=\"132\" width=\"24\" height=\"52\" rx=\"5\" fill=\"#F8E2A0\" stroke=\"#7E5A00\" stroke-width=\"3.5\"/> <rect x=\"202\" y=\"108\" width=\"24\" height=\"76\" rx=\"5\" fill=\"#E2AE1F\" stroke=\"#7E5A00\" stroke-width=\"3.5\"/> <rect x=\"236\" y=\"80\" width=\"24\" height=\"104\" rx=\"5\" fill=\"#7E5A00\" stroke=\"#7E5A00\" stroke-width=\"3.5\"/> <path d=\"M152 186 H278\" stroke=\"#7E5A00\" stroke-width=\"4\" stroke-linecap=\"round\"/> <path d=\"M158 122 L196 92 L220 104 L268 52\" fill=\"none\" stroke=\"#0D7D71\" stroke-width=\"4.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/> <path d=\"M250 52 H269 V71\" fill=\"none\" stroke=\"#0D7D71\" stroke-width=\"4.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/> <g> <animateTransform attributeName=\"transform\" type=\"translate\" values=\"0 0;0 -6;0 0\" dur=\"3.6s\" repeatCount=\"indefinite\"/> <circle cx=\"74\" cy=\"190\" r=\"20\" fill=\"#E2AE1F\" stroke=\"#7E5A00\" stroke-width=\"3.5\"/> <circle cx=\"74\" cy=\"190\" r=\"13\" fill=\"none\" stroke=\"#F8E2A0\" stroke-width=\"2.5\"/> <path d=\"M80 183.5 C80 180 68 179 68 185 C68 191 80 189 80 195 C80 201 68 200 68 196.5 M74 177 V203\" fill=\"none\" stroke=\"#7E5A00\" stroke-width=\"3\" stroke-linecap=\"round\"/> </g> <g transform=\"translate(292 40)\"> <path d=\"M0 -12 C1.2 -4 4 -1.2 12 0 C4 1.2 1.2 4 0 12 C-1.2 4 -4 1.2 -12 0 C-4 -1.2 -1.2 -4 0 -12 Z\" fill=\"#F4B63F\"> <animate attributeName=\"opacity\" values=\"1;0.35;1\" dur=\"3.2s\" repeatCount=\"indefinite\"/> </path> </g> <circle cx=\"122\" cy=\"186\" r=\"3.5\" fill=\"#E2AE1F\" opacity=\"0.6\"/> <circle cx=\"30\" cy=\"110\" r=\"3\" fill=\"#7E5A00\" opacity=\"0.35\"/> </svg>", "6": "<svg class=\"cat-art\" viewBox=\"0 0 320 240\" aria-hidden=\"true\"> <circle cx=\"160\" cy=\"118\" r=\"96\" fill=\"#BFE2F5\" opacity=\"0.5\"/> <ellipse cx=\"160\" cy=\"206\" rx=\"122\" ry=\"9\" fill=\"#155F8A\" opacity=\"0.1\"/> <path d=\"M98 128 Q160 70 222 128\" fill=\"none\" stroke=\"#3CA2DA\" stroke-width=\"3\" stroke-dasharray=\"3 8\" stroke-linecap=\"round\"> <animate attributeName=\"stroke-dashoffset\" values=\"0;-22\" dur=\"1.6s\" repeatCount=\"indefinite\"/> </path> <circle cx=\"96\" cy=\"143\" r=\"16\" fill=\"#BFE2F5\" stroke=\"#155F8A\" stroke-width=\"3.5\"/> <path d=\"M66 188 C66 158 126 158 126 188 Z\" fill=\"#BFE2F5\" stroke=\"#155F8A\" stroke-width=\"3.5\" stroke-linejoin=\"round\"/> <circle cx=\"224\" cy=\"143\" r=\"16\" fill=\"#FFFFFF\" stroke=\"#155F8A\" stroke-width=\"3.5\"/> <path d=\"M194 188 C194 158 254 158 254 188 Z\" fill=\"#FFFFFF\" stroke=\"#155F8A\" stroke-width=\"3.5\" stroke-linejoin=\"round\"/> <circle cx=\"160\" cy=\"126\" r=\"22\" fill=\"#3CA2DA\" stroke=\"#155F8A\" stroke-width=\"4\"/> <path d=\"M118 190 C118 146 202 146 202 190 Z\" fill=\"#3CA2DA\" stroke=\"#155F8A\" stroke-width=\"4\" stroke-linejoin=\"round\"/> <path d=\"M50 40 H104 Q114 40 114 50 V66 Q114 76 104 76 H76 L62 87 L64 76 H50 Q40 76 40 66 V50 Q40 40 50 40 Z\" fill=\"#FFFFFF\" stroke=\"#155F8A\" stroke-width=\"3.5\" stroke-linejoin=\"round\"/> <circle cx=\"62\" cy=\"58\" r=\"4\" fill=\"#3CA2DA\"><animate attributeName=\"opacity\" values=\"1;0.25;1\" dur=\"1.4s\" repeatCount=\"indefinite\"/></circle> <circle cx=\"77\" cy=\"58\" r=\"4\" fill=\"#3CA2DA\"><animate attributeName=\"opacity\" values=\"1;0.25;1\" dur=\"1.4s\" begin=\"0.2s\" repeatCount=\"indefinite\"/></circle> <circle cx=\"92\" cy=\"58\" r=\"4\" fill=\"#3CA2DA\"><animate attributeName=\"opacity\" values=\"1;0.25;1\" dur=\"1.4s\" begin=\"0.4s\" repeatCount=\"indefinite\"/></circle> <path d=\"M216 34 H270 Q280 34 280 44 V64 Q280 74 270 74 H258 L260 85 L246 74 H216 Q206 74 206 64 V44 Q206 34 216 34 Z\" fill=\"#FFFFFF\" stroke=\"#155F8A\" stroke-width=\"3.5\" stroke-linejoin=\"round\"/> <circle cx=\"243\" cy=\"54\" r=\"12\" fill=\"#BFE2F5\"/> <path d=\"M243 54 V42 A12 12 0 0 1 254.3 58.1 Z\" fill=\"#3CA2DA\"/> <g transform=\"translate(160 40)\"> <path d=\"M0 -12 C1.2 -4 4 -1.2 12 0 C4 1.2 1.2 4 0 12 C-1.2 4 -4 1.2 -12 0 C-4 -1.2 -1.2 -4 0 -12 Z\" fill=\"#F4B63F\"> <animate attributeName=\"opacity\" values=\"1;0.35;1\" dur=\"3.2s\" repeatCount=\"indefinite\"/> </path> </g> <circle cx=\"34\" cy=\"150\" r=\"3.5\" fill=\"#3CA2DA\" opacity=\"0.5\"/> <circle cx=\"290\" cy=\"150\" r=\"3\" fill=\"#155F8A\" opacity=\"0.35\"/> </svg>", "7": "<svg class=\"cat-art\" viewBox=\"0 0 320 240\" aria-hidden=\"true\"> <circle cx=\"160\" cy=\"118\" r=\"96\" fill=\"#F4CDBE\" opacity=\"0.5\"/> <ellipse cx=\"160\" cy=\"206\" rx=\"122\" ry=\"9\" fill=\"#9A4127\" opacity=\"0.1\"/> <path d=\"M30 44 L60 28 L90 44 Z\" fill=\"#D2704F\" stroke=\"#9A4127\" stroke-width=\"3.5\" stroke-linejoin=\"round\"/> <rect x=\"34\" y=\"44\" width=\"52\" height=\"10\" rx=\"3\" fill=\"#F4CDBE\" stroke=\"#9A4127\" stroke-width=\"3.5\"/> <rect x=\"42\" y=\"54\" width=\"36\" height=\"62\" fill=\"#FFFFFF\" stroke=\"#9A4127\" stroke-width=\"3.5\"/> <path d=\"M52 60 V110 M60 60 V110 M68 60 V110\" stroke=\"#F4CDBE\" stroke-width=\"3\" stroke-linecap=\"round\"/> <rect x=\"34\" y=\"116\" width=\"52\" height=\"10\" rx=\"3\" fill=\"#F4CDBE\" stroke=\"#9A4127\" stroke-width=\"3.5\"/> <path d=\"M68 118 V182 C104 176 140 178 160 192 C180 178 216 176 252 182 V118\" fill=\"#D2704F\" stroke=\"#9A4127\" stroke-width=\"4\" stroke-linejoin=\"round\"/> <path d=\"M160 120 C140 106 104 104 76 110 V174 C104 170 140 172 160 184 Z\" fill=\"#FFFFFF\" stroke=\"#9A4127\" stroke-width=\"4\" stroke-linejoin=\"round\"/> <path d=\"M160 120 C180 106 216 104 244 110 V174 C216 170 180 172 160 184 Z\" fill=\"#FFFFFF\" stroke=\"#9A4127\" stroke-width=\"4\" stroke-linejoin=\"round\"/> <path d=\"M90 126 C110 122 130 123 146 129 M90 140 C110 136 130 137 146 143 M90 154 C108 150 124 151 140 156\" fill=\"none\" stroke=\"#F4CDBE\" stroke-width=\"3.5\" stroke-linecap=\"round\"/> <path d=\"M174 129 C190 123 206 122 222 125 M174 143 C188 138 200 137 212 138\" fill=\"none\" stroke=\"#F4CDBE\" stroke-width=\"3.5\" stroke-linecap=\"round\"/> <g> <animateTransform attributeName=\"transform\" type=\"rotate\" values=\"-2 206 150;3 206 150;-2 206 150\" dur=\"5s\" repeatCount=\"indefinite\"/> <path d=\"M238 30 C262 54 250 102 214 128 C206 100 212 60 238 30 Z\" fill=\"#F4CDBE\" stroke=\"#9A4127\" stroke-width=\"3.5\" stroke-linejoin=\"round\"/> <path d=\"M236 38 L204 156\" stroke=\"#9A4127\" stroke-width=\"3\" stroke-linecap=\"round\"/> <path d=\"M231 62 L245 58 M225 82 L241 80 M220 100 L235 100\" stroke=\"#D2704F\" stroke-width=\"2.5\" stroke-linecap=\"round\"/> </g> <g> <animateTransform attributeName=\"transform\" type=\"translate\" values=\"0 0;0 -6;0 0\" dur=\"4s\" repeatCount=\"indefinite\"/> <path d=\"M136 80 V52 L162 46 V74\" fill=\"none\" stroke=\"#9A4127\" stroke-width=\"3\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/> <path d=\"M136 52 L162 46\" stroke=\"#9A4127\" stroke-width=\"5\" stroke-linecap=\"round\"/> <ellipse cx=\"130\" cy=\"81\" rx=\"7\" ry=\"5.5\" fill=\"#D2704F\" stroke=\"#9A4127\" stroke-width=\"2.5\" transform=\"rotate(-20 130 81)\"/> <ellipse cx=\"156\" cy=\"75\" rx=\"7\" ry=\"5.5\" fill=\"#D2704F\" stroke=\"#9A4127\" stroke-width=\"2.5\" transform=\"rotate(-20 156 75)\"/> </g> <g transform=\"translate(286 92)\"> <path d=\"M0 -12 C1.2 -4 4 -1.2 12 0 C4 1.2 1.2 4 0 12 C-1.2 4 -4 1.2 -12 0 C-4 -1.2 -1.2 -4 0 -12 Z\" fill=\"#F4B63F\"> <animate attributeName=\"opacity\" values=\"1;0.35;1\" dur=\"3.2s\" repeatCount=\"indefinite\"/> </path> </g> <g transform=\"translate(110 36) scale(0.55)\"> <path d=\"M0 -12 C1.2 -4 4 -1.2 12 0 C4 1.2 1.2 4 0 12 C-1.2 4 -4 1.2 -12 0 C-4 -1.2 -1.2 -4 0 -12 Z\" fill=\"#F4B63F\"> <animate attributeName=\"opacity\" values=\"0.4;1;0.4\" dur=\"3.2s\" repeatCount=\"indefinite\"/> </path> </g> <circle cx=\"284\" cy=\"170\" r=\"3.5\" fill=\"#D2704F\" opacity=\"0.55\"/> </svg>", "8": "<svg class=\"cat-art\" viewBox=\"0 0 320 240\" aria-hidden=\"true\"> <circle cx=\"160\" cy=\"118\" r=\"96\" fill=\"#F9C6D2\" opacity=\"0.5\"/> <ellipse cx=\"160\" cy=\"206\" rx=\"122\" ry=\"9\" fill=\"#B32745\" opacity=\"0.1\"/> <path d=\"M222 150 C224 188 266 190 270 160 V146\" fill=\"none\" stroke=\"#B32745\" stroke-width=\"4\" stroke-linecap=\"round\"/> <circle cx=\"270\" cy=\"136\" r=\"10\" fill=\"#F9C6D2\" stroke=\"#B32745\" stroke-width=\"3.5\"/> <circle cx=\"270\" cy=\"136\" r=\"4\" fill=\"#B32745\"/> <path d=\"M160 190 C110 158 82 130 82 98 C82 72 100 56 124 56 C140 56 152 64 160 78 C168 64 180 56 196 56 C220 56 238 72 238 98 C238 130 210 158 160 190 Z\" fill=\"#E8587A\" stroke=\"#B32745\" stroke-width=\"4\" stroke-linejoin=\"round\"/> <path d=\"M102 90 C102 78 110 71 120 70\" fill=\"none\" stroke=\"#F9C6D2\" stroke-width=\"5\" stroke-linecap=\"round\"/> <path d=\"M92 120 H124 L134 100 L148 142 L160 108 L170 120 H228\" fill=\"none\" stroke=\"#FFFFFF\" stroke-width=\"5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-dasharray=\"210\" stroke-dashoffset=\"210\"> <animate attributeName=\"stroke-dashoffset\" values=\"210;0;0;-210\" keyTimes=\"0;0.45;0.7;1\" dur=\"2.8s\" repeatCount=\"indefinite\"/> </path> <g transform=\"translate(272 54)\"> <rect x=\"-22\" y=\"-22\" width=\"44\" height=\"44\" rx=\"12\" fill=\"#FFFFFF\" stroke=\"#B32745\" stroke-width=\"3.5\"/> <path d=\"M-5 -13 H5 V-5 H13 V5 H5 V13 H-5 V5 H-13 V-5 H-5 Z\" fill=\"#E8587A\" stroke=\"#B32745\" stroke-width=\"2.5\" stroke-linejoin=\"round\"/> </g> <g transform=\"rotate(-32 62 176)\"> <rect x=\"34\" y=\"165\" width=\"56\" height=\"22\" rx=\"11\" fill=\"#F9C6D2\" stroke=\"#B32745\" stroke-width=\"3.5\"/> <path d=\"M62 165 H79 A11 11 0 0 1 79 187 H62 Z\" fill=\"#E8587A\" stroke=\"#B32745\" stroke-width=\"3.5\" stroke-linejoin=\"round\"/> </g> <g transform=\"translate(50 62)\"> <path d=\"M0 -12 C1.2 -4 4 -1.2 12 0 C4 1.2 1.2 4 0 12 C-1.2 4 -4 1.2 -12 0 C-4 -1.2 -1.2 -4 0 -12 Z\" fill=\"#F4B63F\"> <animate attributeName=\"opacity\" values=\"1;0.35;1\" dur=\"3.2s\" repeatCount=\"indefinite\"/> </path> </g> <circle cx=\"296\" cy=\"96\" r=\"3.5\" fill=\"#E8587A\" opacity=\"0.5\"/> <circle cx=\"112\" cy=\"196\" r=\"3\" fill=\"#B32745\" opacity=\"0.35\"/> </svg>", "9": "<svg class=\"cat-art\" viewBox=\"0 0 320 240\" aria-hidden=\"true\"> <circle cx=\"160\" cy=\"118\" r=\"96\" fill=\"#EBD3B4\" opacity=\"0.55\"/> <ellipse cx=\"160\" cy=\"206\" rx=\"122\" ry=\"9\" fill=\"#6E4518\" opacity=\"0.1\"/> <path d=\"M132 194 H212 L202 178 H142 Z\" fill=\"#B98143\" stroke=\"#6E4518\" stroke-width=\"3.5\" stroke-linejoin=\"round\"/> <rect x=\"168\" y=\"62\" width=\"8\" height=\"118\" rx=\"3\" fill=\"#6E4518\"/> <g> <animateTransform attributeName=\"transform\" type=\"rotate\" values=\"-4 172 74;4 172 74;-4 172 74\" dur=\"6s\" repeatCount=\"indefinite\"/> <path d=\"M112 74 H232\" stroke=\"#6E4518\" stroke-width=\"6\" stroke-linecap=\"round\"/> <g> <animateTransform attributeName=\"transform\" type=\"rotate\" values=\"4 112 74;-4 112 74;4 112 74\" dur=\"6s\" repeatCount=\"indefinite\"/> <path d=\"M112 74 L94 124 M112 74 L130 124\" stroke=\"#6E4518\" stroke-width=\"2.5\" stroke-linecap=\"round\"/> <path d=\"M88 124 H136 C136 141 88 141 88 124 Z\" fill=\"#EBD3B4\" stroke=\"#6E4518\" stroke-width=\"3.5\" stroke-linejoin=\"round\"/> </g> <g> <animateTransform attributeName=\"transform\" type=\"rotate\" values=\"4 232 74;-4 232 74;4 232 74\" dur=\"6s\" repeatCount=\"indefinite\"/> <path d=\"M232 74 L214 124 M232 74 L250 124\" stroke=\"#6E4518\" stroke-width=\"2.5\" stroke-linecap=\"round\"/> <path d=\"M208 124 H256 C256 141 208 141 208 124 Z\" fill=\"#EBD3B4\" stroke=\"#6E4518\" stroke-width=\"3.5\" stroke-linejoin=\"round\"/> </g> </g> <circle cx=\"172\" cy=\"58\" r=\"8\" fill=\"#F4B63F\" stroke=\"#6E4518\" stroke-width=\"3\"/> <g transform=\"translate(54 170)\"> <path d=\"M-30 0 L0 -14 L30 0 L0 14 Z\" fill=\"#6E4518\" stroke=\"#6E4518\" stroke-width=\"3\" stroke-linejoin=\"round\"/> <path d=\"M-18 6 V20 C-18 28 18 28 18 20 V6\" fill=\"#B98143\" stroke=\"#6E4518\" stroke-width=\"3.5\" stroke-linejoin=\"round\"/> <path d=\"M0 0 L24 8 V24\" fill=\"none\" stroke=\"#F4B63F\" stroke-width=\"3\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/> <circle cx=\"24\" cy=\"27\" r=\"3.5\" fill=\"#F4B63F\"/> </g> <g transform=\"rotate(-30 266 168)\"> <rect x=\"248\" y=\"146\" width=\"36\" height=\"18\" rx=\"5\" fill=\"#B98143\" stroke=\"#6E4518\" stroke-width=\"3.5\"/> <rect x=\"262\" y=\"164\" width=\"8\" height=\"34\" rx=\"3\" fill=\"#EBD3B4\" stroke=\"#6E4518\" stroke-width=\"3\"/> </g> <g transform=\"translate(58 62)\"> <path d=\"M0 -12 C1.2 -4 4 -1.2 12 0 C4 1.2 1.2 4 0 12 C-1.2 4 -4 1.2 -12 0 C-4 -1.2 -1.2 -4 0 -12 Z\" fill=\"#F4B63F\"> <animate attributeName=\"opacity\" values=\"1;0.35;1\" dur=\"3.2s\" repeatCount=\"indefinite\"/> </path> </g> <g transform=\"translate(284 50) scale(0.55)\"> <path d=\"M0 -12 C1.2 -4 4 -1.2 12 0 C4 1.2 1.2 4 0 12 C-1.2 4 -4 1.2 -12 0 C-4 -1.2 -1.2 -4 0 -12 Z\" fill=\"#F4B63F\"> <animate attributeName=\"opacity\" values=\"0.4;1;0.4\" dur=\"3.2s\" repeatCount=\"indefinite\"/> </path> </g> <circle cx=\"112\" cy=\"190\" r=\"3\" fill=\"#B98143\" opacity=\"0.6\"/> </svg>", "10": "<svg class=\"cat-art\" viewBox=\"0 0 320 240\" aria-hidden=\"true\"> <circle cx=\"160\" cy=\"118\" r=\"96\" fill=\"#F2C6E4\" opacity=\"0.5\"/> <ellipse cx=\"160\" cy=\"206\" rx=\"122\" ry=\"9\" fill=\"#8E2672\" opacity=\"0.1\"/> <circle cx=\"124\" cy=\"126\" r=\"54\" fill=\"#F2C6E4\" stroke=\"#8E2672\" stroke-width=\"4\"/> <path d=\"M96 88 C106 82 118 88 114 98 C110 108 98 104 92 112 C86 104 88 94 96 88 Z\" fill=\"#D25AAE\"/> <path d=\"M136 136 C150 130 160 140 154 152 C148 162 136 160 132 150 C130 144 132 140 136 136 Z\" fill=\"#D25AAE\"/> <ellipse cx=\"124\" cy=\"126\" rx=\"22\" ry=\"54\" fill=\"none\" stroke=\"#8E2672\" stroke-width=\"3\"/> <path d=\"M70 126 H178 M79 99 H169 M79 153 H169\" stroke=\"#8E2672\" stroke-width=\"3\"/> <ellipse cx=\"124\" cy=\"126\" rx=\"78\" ry=\"22\" fill=\"none\" stroke=\"#D25AAE\" stroke-width=\"2.5\" stroke-dasharray=\"4 7\" transform=\"rotate(-18 124 126)\"/> <g transform=\"rotate(-18 124 126)\"> <circle r=\"6\" fill=\"#F4B63F\" stroke=\"#8E2672\" stroke-width=\"2.5\"> <animateMotion dur=\"7s\" repeatCount=\"indefinite\" path=\"M202 126 A78 22 0 1 1 46 126 A78 22 0 1 1 202 126\"/> </circle> </g> <g transform=\"rotate(-12 236 120)\"> <path d=\"M206 104 L262 78 V162 L206 136 Z\" fill=\"#D25AAE\" stroke=\"#8E2672\" stroke-width=\"4\" stroke-linejoin=\"round\"/> <rect x=\"190\" y=\"102\" width=\"18\" height=\"36\" rx=\"6\" fill=\"#FFFFFF\" stroke=\"#8E2672\" stroke-width=\"4\"/> <path d=\"M214 138 L222 166 H234 L228 142\" fill=\"#8E2672\" stroke=\"#8E2672\" stroke-width=\"3\" stroke-linejoin=\"round\"/> <path d=\"M274 100 Q284 120 274 140\" fill=\"none\" stroke=\"#D25AAE\" stroke-width=\"4\" stroke-linecap=\"round\"> <animate attributeName=\"opacity\" values=\"0.2;1;0.2\" dur=\"1.8s\" repeatCount=\"indefinite\"/> </path> <path d=\"M287 88 Q303 120 287 152\" fill=\"none\" stroke=\"#8E2672\" stroke-width=\"4\" stroke-linecap=\"round\" opacity=\"0.6\"> <animate attributeName=\"opacity\" values=\"0.1;0.7;0.1\" dur=\"1.8s\" begin=\"0.3s\" repeatCount=\"indefinite\"/> </path> </g> <path d=\"M44 38 H98 Q108 38 108 48 V62 Q108 72 98 72 H70 L58 83 L60 72 H44 Q34 72 34 62 V48 Q34 38 44 38 Z\" fill=\"#FFFFFF\" stroke=\"#8E2672\" stroke-width=\"3.5\" stroke-linejoin=\"round\"/> <path d=\"M46 50 H92 M46 60 H78\" stroke=\"#F2C6E4\" stroke-width=\"4\" stroke-linecap=\"round\"/> <g transform=\"translate(272 42)\"> <path d=\"M0 -12 C1.2 -4 4 -1.2 12 0 C4 1.2 1.2 4 0 12 C-1.2 4 -4 1.2 -12 0 C-4 -1.2 -1.2 -4 0 -12 Z\" fill=\"#F4B63F\"> <animate attributeName=\"opacity\" values=\"1;0.35;1\" dur=\"3.2s\" repeatCount=\"indefinite\"/> </path> </g> <circle cx=\"40\" cy=\"190\" r=\"3.5\" fill=\"#D25AAE\" opacity=\"0.5\"/> <circle cx=\"200\" cy=\"200\" r=\"3\" fill=\"#8E2672\" opacity=\"0.35\"/> </svg>", "11": "<svg class=\"cat-art\" viewBox=\"0 0 320 240\" aria-hidden=\"true\"> <circle cx=\"160\" cy=\"118\" r=\"96\" fill=\"#BCE8E1\" opacity=\"0.5\"/> <ellipse cx=\"160\" cy=\"206\" rx=\"122\" ry=\"9\" fill=\"#0A6157\" opacity=\"0.1\"/> <g transform=\"rotate(-8 64 120)\"> <rect x=\"34\" y=\"88\" width=\"56\" height=\"68\" rx=\"7\" fill=\"#FFFFFF\" stroke=\"#0A6157\" stroke-width=\"3.5\"/> <path d=\"M46 106 L51 111 L60 101 M46 128 L51 133 L60 123\" fill=\"none\" stroke=\"#19A394\" stroke-width=\"3\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/> <path d=\"M67 107 H80 M67 129 H78 M46 146 H72\" stroke=\"#BCE8E1\" stroke-width=\"3.5\" stroke-linecap=\"round\"/> </g> <rect x=\"84\" y=\"162\" width=\"152\" height=\"26\" rx=\"6\" fill=\"#19A394\" stroke=\"#0A6157\" stroke-width=\"4\"/> <path d=\"M100 163 V187 M220 163 V187\" stroke=\"#BCE8E1\" stroke-width=\"3\"/> <rect x=\"98\" y=\"136\" width=\"128\" height=\"26\" rx=\"6\" fill=\"#BCE8E1\" stroke=\"#0A6157\" stroke-width=\"4\"/> <path d=\"M114 149 H188\" stroke=\"#FFFFFF\" stroke-width=\"4\" stroke-linecap=\"round\"/> <rect x=\"90\" y=\"110\" width=\"140\" height=\"26\" rx=\"6\" fill=\"#FFFFFF\" stroke=\"#0A6157\" stroke-width=\"4\"/> <path d=\"M210 112 V134\" stroke=\"#F4B63F\" stroke-width=\"6\"/> <g stroke=\"#F4B63F\" stroke-width=\"4\" stroke-linecap=\"round\"> <animate attributeName=\"opacity\" values=\"1;0.3;1\" dur=\"2.6s\" repeatCount=\"indefinite\"/> <path d=\"M160 14 V22 M120 28 L126 34 M200 28 L194 34 M108 64 H116 M204 64 H212\"/> </g> <path d=\"M160 34 C142 34 130 48 130 64 C130 76 138 84 144 90 V98 H176 V90 C182 84 190 76 190 64 C190 48 178 34 160 34 Z\" fill=\"#FCE7B0\" stroke=\"#0A6157\" stroke-width=\"4\" stroke-linejoin=\"round\"/> <path d=\"M152 74 L157 82 L163 70 L168 82\" fill=\"none\" stroke=\"#E0A21B\" stroke-width=\"3\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/> <rect x=\"146\" y=\"98\" width=\"28\" height=\"11\" rx=\"4\" fill=\"#BCE8E1\" stroke=\"#0A6157\" stroke-width=\"3.5\"/> <g transform=\"rotate(24 262 130)\"> <rect x=\"254\" y=\"72\" width=\"18\" height=\"94\" rx=\"3\" fill=\"#F4B63F\" stroke=\"#0A6157\" stroke-width=\"3.5\"/> <path d=\"M263 74 V164\" stroke=\"#E0A21B\" stroke-width=\"2.5\"/> <path d=\"M254 166 L263 186 L272 166 Z\" fill=\"#FCE7B0\" stroke=\"#0A6157\" stroke-width=\"3.5\" stroke-linejoin=\"round\"/> <rect x=\"254\" y=\"58\" width=\"18\" height=\"15\" rx=\"3\" fill=\"#F2A7B5\" stroke=\"#0A6157\" stroke-width=\"3.5\"/> </g> <g transform=\"translate(278 36)\"> <path d=\"M0 -12 C1.2 -4 4 -1.2 12 0 C4 1.2 1.2 4 0 12 C-1.2 4 -4 1.2 -12 0 C-4 -1.2 -1.2 -4 0 -12 Z\" fill=\"#F4B63F\"> <animate attributeName=\"opacity\" values=\"1;0.35;1\" dur=\"3.2s\" repeatCount=\"indefinite\"/> </path> </g> <circle cx=\"44\" cy=\"190\" r=\"3.5\" fill=\"#19A394\" opacity=\"0.5\"/> <circle cx=\"112\" cy=\"70\" r=\"3\" fill=\"#0A6157\" opacity=\"0.35\"/> </svg>"};
/* ===== 70-core.js ===== */
/* ================= helpers ================= */
const $ = (s, r) => (r || document).querySelector(s);
const $$ = (s, r) => Array.from((r || document).querySelectorAll(s));
const esc = s => String(s == null ? '' : s).replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
const fmt = n => Number(n).toLocaleString('en-US');
const compact = n => n >= 1e6 ? (n / 1e6).toFixed(1).replace(/\.0$/, '') + 'M' : n >= 1000 ? (Math.round(n / 100) / 10).toString().replace(/\.0$/, '') + 'K' : String(n);
const plural = (n, w, ws) => `${fmt(n)} ${n === 1 ? w : (ws || w + 's')}`;
const norm = s => String(s || '').toLowerCase().normalize('NFD').replace(/[̀-ͯ]/g, '').replace(/[’'`]/g, '').replace(/[^a-z0-9\s]/g, ' ').replace(/\s+/g, ' ').trim();
const key = s => norm(s).replace(/\s/g, '');
const hsh = s => { let h = 2166136261; for (let i = 0; i < s.length; i++) { h ^= s.charCodeAt(i); h = Math.imul(h, 16777619); } return h >>> 0; };
function rng(seed) { let a = seed; return () => { a |= 0; a = a + 0x6D2B79F5 | 0; let t = Math.imul(a ^ a >>> 15, 1 | a); t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t; return ((t ^ t >>> 14) >>> 0) / 4294967296; }; }
const icon = (id, cls) => `<svg class="i${cls ? ' ' + cls : ''}" aria-hidden="true"><use href="#${id}"/></svg>`;
const reduceMotion = () => !!(window.matchMedia && matchMedia('(prefers-reduced-motion: reduce)').matches);
const isPhone = () => window.matchMedia('(max-width: 860px)').matches;
const slug = code => String(code).toLowerCase().replace(/[^a-z0-9]+/g, '');
const MARK = '<svg class="s-mark" viewBox="0 0 32 32" aria-hidden="true"><circle cx="16" cy="16" r="13" fill="none" stroke="currentColor" stroke-width="2.6"/><path d="M17.6 6.5 9.8 17.8h5.4l-1 7.7 8-11.6h-5.6z" fill="currentColor"/></svg>';
/* per-viewer conveniences only; every read and write is guarded */
const store = {
get(k, d) { try { const v = localStorage.getItem('sbc.' + k); return v == null ? d : JSON.parse(v); } catch (e) { return d; } },
set(k, v) { try { localStorage.setItem('sbc.' + k, JSON.stringify(v)); } catch (e) { /* storage unavailable */ } },
clear() { try { Object.keys(localStorage).filter(k => k.indexOf('sbc.') === 0).forEach(k => localStorage.removeItem(k)); } catch (e) { /* ignore */ } }
};
/* ================= dates (everything is relative to the viewer's today) ================= */
const TODAY = (() => { const d = new Date(); d.setHours(0, 0, 0, 0); return d; })();
const addDays = n => { const d = new Date(TODAY); d.setDate(d.getDate() + n); return d; };
const DOW = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
const MON = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
const dayName = n => n === 0 ? 'Today' : n === 1 ? 'Tomorrow' : DOW[addDays(n).getDay()];
const dateTxt = n => { const d = addDays(n); return `${DOW[d.getDay()]}, ${MON[d.getMonth()]} ${d.getDate()}`; };
const inDays = n => n === 0 ? 'today' : n === 1 ? 'tomorrow' : `in ${n} days`;
function greeting() { const h = new Date().getHours(); return h < 5 ? 'Up late' : h < 12 ? 'Good morning' : h < 18 ? 'Good afternoon' : 'Good evening'; }
/* ================= overlays: popovers, modals, sheets, toasts, tooltips ================= */
const OVL = () => $('#ovl');
/* overlays carry the current page's scope class, so a page's ported styles reach its sheets and dialogs */
const pageScopeCls = () => { const p = $('#page'); return p ? Array.from(p.classList).filter(c => /^pg-/.test(c)).join(' ') : ''; };
let POP = null;
function closePop() {
if (!POP) return;
const { el, anchor, onClose } = POP; POP = null;
el.remove();
if (anchor && anchor.setAttribute) anchor.setAttribute('aria-expanded', 'false');
if (onClose) onClose();
}
function openPop(anchor, html, opts) {
opts = opts || {};
const same = POP && POP.anchor === anchor;
closePop();
if (same && !opts.force) return null;
const el = document.createElement('div');
el.className = 'pop ' + (opts.cls || '');
el.setAttribute('role', opts.role || 'dialog');
if (opts.label) el.setAttribute('aria-label', opts.label);
el.innerHTML = html;
OVL().appendChild(el);
POP = { el, anchor, onClose: opts.onClose };
if (anchor && anchor.setAttribute) anchor.setAttribute('aria-expanded', 'true');
placePop(el, anchor, opts.align || 'end', opts.side || 'below');
const f = el.querySelector(opts.focus || 'button, input, select, [tabindex="0"]');
if (f && opts.focus !== false) f.focus({ preventScroll: true });
return el;
}
function placePop(el, anchor, align, side) {
const r = anchor.getBoundingClientRect(), w = el.offsetWidth, h = el.offsetHeight, vw = innerWidth, vh = innerHeight;
let left = align === 'start' ? r.left : align === 'center' ? r.left + r.width / 2 - w / 2 : r.right - w;
left = Math.max(12, Math.min(left, vw - w - 12));
let top = side === 'above' ? r.top - h - 8 : r.bottom + 8;
if (side !== 'above' && top + h > vh - 12) top = Math.max(12, r.top - h - 8);
if (top < 12) top = 12;
el.style.left = left + 'px'; el.style.top = top + 'px';
}
document.addEventListener('pointerdown', e => {
if (POP && !POP.el.contains(e.target) && !(POP.anchor && POP.anchor.contains && POP.anchor.contains(e.target))) closePop();
});
window.addEventListener('resize', () => closePop());
let MODAL = null;
function openModal(html, opts) {
opts = opts || {};
closeModal(true); closePop();
const scrim = document.createElement('div');
scrim.className = 'scrim ' + (opts.scope != null ? opts.scope : pageScopeCls());
scrim.innerHTML = `<div class="mcard ${opts.cls || ''}" role="dialog" aria-modal="true" aria-label="${esc(opts.label || 'Dialog')}">${html}</div>`;
OVL().appendChild(scrim);
MODAL = { scrim, opener: document.activeElement, onClose: opts.onClose };
scrim.addEventListener('mousedown', e => { if (e.target === scrim) closeModal(); });
const f = scrim.querySelector(opts.focus || 'input, button:not(.s-ib)'); if (f) f.focus();
return scrim.firstElementChild;
}
function closeModal(silent) {
if (!MODAL) return;
const { scrim, opener, onClose } = MODAL; MODAL = null;
scrim.remove();
if (!silent && opener && document.contains(opener)) opener.focus({ preventScroll: true });
if (onClose) onClose();
}
let SHEET = null;
function openSheet(title, body, opts) {
opts = opts || {};
closeSheet(true); closePop();
const wrap = document.createElement('div');
wrap.className = opts.scope != null ? opts.scope : pageScopeCls();
wrap.innerHTML = `<div class="sheet-scrim" data-act="sheet-close"></div><aside class="sheet" role="dialog" aria-modal="true" aria-label="${esc(title)}"><div class="sheet-h">${opts.lead || ''}<h2>${esc(title)}</h2><button type="button" class="s-ib" data-act="sheet-close" aria-label="Close">${icon('i-x')}</button></div><div class="sheet-b">${body}</div></aside>`;
OVL().appendChild(wrap);
SHEET = { wrap, opener: document.activeElement };
const f = wrap.querySelector('.sheet-b button, .sheet-b input, .sheet-h .s-ib'); if (f) f.focus();
return wrap.querySelector('.sheet');
}
function closeSheet(silent) {
if (!SHEET) return;
const { wrap, opener } = SHEET; SHEET = null; wrap.remove();
if (!silent && opener && document.contains(opener)) opener.focus({ preventScroll: true });
}
function toast(msg, action) {
let box = $('.toasts');
if (!box) { box = document.createElement('div'); box.className = 'toasts'; box.setAttribute('role', 'status'); box.setAttribute('aria-live', 'polite'); document.body.appendChild(box); }
box.innerHTML = '';
const t = document.createElement('div'); t.className = 'toast';
t.innerHTML = `<span>${esc(msg)}</span>` + (action ? `<button type="button">${esc(action.label)}</button>` : '');
if (action) t.querySelector('button').addEventListener('click', () => { t.remove(); action.run(); });
box.appendChild(t);
clearTimeout(toast.t); toast.t = setTimeout(() => t.remove(), action ? 5200 : 2800);
}
/* tooltips for the rail */
let TIP = null;
document.addEventListener('pointerover', e => {
const t = e.target.closest('[data-tip]');
if (!t || !t.closest('.nav.rail')) { if (TIP) { TIP.remove(); TIP = null; } return; }
if (TIP && TIP.dataset.for === t.dataset.tip) return;
if (TIP) TIP.remove();
TIP = document.createElement('div'); TIP.className = 'tip'; TIP.dataset.for = t.dataset.tip; TIP.textContent = t.dataset.tip;
document.body.appendChild(TIP);
const r = t.getBoundingClientRect(); TIP.style.left = (r.right + 8) + 'px'; TIP.style.top = (r.top + r.height / 2 - TIP.offsetHeight / 2) + 'px';
});
function trapTab(container, e) {
const f = $$('button, input, select, textarea, a[href], [tabindex]:not([tabindex="-1"])', container).filter(el => !el.disabled && el.offsetParent !== null);
if (!f.length) return; const first = f[0], last = f[f.length - 1];
if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); }
else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); }
}
document.addEventListener('keydown', e => {
if (e.key === 'Tab') { if (MODAL) trapTab(MODAL.scrim, e); else if (SHEET) trapTab(SHEET.wrap, e); return; }
if (e.key !== 'Escape') return;
if (POP) { const a = POP.anchor; closePop(); if (a && a.focus) a.focus(); return; }
if (MODAL) { closeModal(); return; }
if (SHEET) { closeSheet(); return; }
if (window.HOOT && HOOT.isOpen()) { HOOT.close(); return; }
if (window.DRAWER_OPEN) { closeDrawer(); return; }
if (document.getElementById('app').classList.contains('focus')) setFocusMode(false);
});
/* ================= theme ================= */
const THEMES = [
{ id: 'auto', name: 'Match device', group: 'Standard', desc: 'Light or dark, following your device' },
{ id: 'light', name: 'Light', group: 'Standard', desc: 'Clean teal on soft grey', sw: ['#ffffff', '#f6f8f8', '#1a8a7f', '#f6cc63'] },
{ id: 'dark', name: 'Dark', group: 'Standard', desc: 'The current dark mode', sw: ['#0e171a', '#0b1214', '#2db3a2', '#d99d36'] },
{ id: 'matcha', name: 'Matcha', group: 'Pro', desc: 'Calm · light', sw: ['#f7f8f1', '#eef1e6', '#4e7a39', '#d8b688'] },
{ id: 'cloud', name: 'Cloud Nine', group: 'Pro', desc: 'Dreamy · drifting clouds', motion: true, sw: ['#f7f6ff', '#ebe8fb', '#5b5bd6', '#ffbf9f'] },
{ id: 'midnight', name: 'Lo-fi Midnight', group: 'Pro', desc: 'Cozy · a breathing lamp', motion: true, sw: ['#110e18', '#15121e', '#f3b75f', '#c97591'] },
{ id: 'citrus', name: 'Citrus Rush', group: 'Pro', desc: 'Energetic · light', sw: ['#fff1d6', '#fff9f1', '#ff6b2c', '#7dc2ff'] },
{ id: 'arcade', name: 'Arcade', group: 'Pro', desc: 'Energetic · neon dark', sw: ['#090a17', '#0c0d1e', '#ff4f9a', '#3de6ef'] }
];
function applyTheme(id, animate) {
const root = document.documentElement;
if (animate && !reduceMotion()) { const app = $('#app'); app.classList.add('theming'); clearTimeout(applyTheme.t); applyTheme.t = setTimeout(() => app.classList.remove('theming'), 500); }
if (!id || id === 'auto') root.removeAttribute('data-sb'); else root.setAttribute('data-sb', id);
root.classList.toggle('still', !S.motion);
syncDark();
}
/* .dk on <html> mirrors "is the app dark right now" so ported dark-only rules follow the in-app theme */
function syncDark() { document.documentElement.classList.toggle('dk', isDarkNow()); }
if (window.matchMedia) { const mq = matchMedia('(prefers-color-scheme: dark)'); if (mq.addEventListener) mq.addEventListener('change', syncDark); }
try { new MutationObserver(syncDark).observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] }); } catch (e) { /* ignore */ }
function isDarkNow() {
const sb = document.documentElement.getAttribute('data-sb');
if (sb) return sb === 'dark' || sb === 'midnight' || sb === 'arcade';
const t = document.documentElement.getAttribute('data-theme'); if (t) return t === 'dark';
return !!(window.matchMedia && matchMedia('(prefers-color-scheme: dark)').matches);
}
/* ================= one state object for the whole prototype ================= */
/* Filled from the persona at boot (72-shell.js). Saved per viewer so a refresh keeps the demo where it was. */
const S = {};
const clone = v => JSON.parse(JSON.stringify(v));
/* ===== 71b-campus-data.js ===== */
/* Campus Hub fields from the institutions sheet, matched to the Documents hub schools */
const CAMPUS = {"um": {"d": "A private research university in Coral Gables with a significant focus on marine science, tropical medicine, and international business.", "w": "https://www.miami.edu", "e": 19000, "cp": "#F47321", "cs": "#005030", "m": "Hurricanes (Sebastian the Ibis)", "f": 1925, "t": "Private Nonprofit", "aa": "Miami"}, "fiu": {"d": "A large public research university in the Miami metropolitan area, recognized for its focus on international business and its proximity to the Everglades.", "w": "https://www.fiu.edu", "e": 56000, "cp": "#081E3F", "cs": "#C69214", "m": "Panthers (Roary)", "f": 1965, "t": "Public"}, "mdc": {"d": "A large public community college system in Miami-Dade County, Florida, serving one of the most diverse student populations in the United States.", "w": "https://www.mdc.edu", "e": 95000, "m": "Sharks", "f": 1959, "t": "Community College"}, "barry": {"d": "A private Catholic university in the Miami area founded by the Adrian Dominican Sisters, known for its diverse student body and health science programs.", "w": "https://www.barry.edu", "e": 7500, "m": "Buccaneers", "f": 1940, "t": "Private Nonprofit"}, "uf": {"d": "A public land-grant research university in Gainesville known for its significant contributions to agricultural research and its large student enrollment.", "w": "https://www.ufl.edu", "e": 61000, "cp": "#FA4616", "cs": "#0021A5", "m": "Gators (Albert and Alberta)", "f": 1853, "t": "Public"}, "ucf": {"d": "A large public research university in the Orlando metropolitan area that maintains one of the highest student enrollments in the United States.", "w": "https://www.ucf.edu", "e": 68000, "cp": "#FFC904", "cs": "#000000", "m": "Knights", "f": 1963, "t": "Public"}, "snhu": {"d": "A private nonprofit university in Manchester that has become one of the largest providers of online higher education in the United States.", "w": "https://www.snhu.edu", "e": 164000, "cp": "#003D7A", "cs": "#FFC72C", "m": "Penmen", "f": 1932, "t": "Private Nonprofit"}, "wgu": {"d": "A private nonprofit online university offering competency-based degrees, where progress depends on demonstrated mastery rather than time spent in a classroom.", "w": "https://www.wgu.edu", "e": 150000, "cp": "#003865", "cs": "#FFCD00", "m": "Night Owls", "f": 1997, "t": "Private Nonprofit"}, "asu": {"d": "Arizona State University is a public research university with its primary campus located in Tempe, Arizona.", "w": "https://www.asu.edu", "e": 145000, "cp": "#8C1D40", "cs": "#FFC627", "m": "Sun Devils (Sparky)", "f": 1885, "t": "Public"}, "uscla": {"d": "A private research university in Los Angeles with a long-standing reputation for its professional schools, particularly in film, communications, and business.", "w": "https://www.usc.edu", "e": 49500, "cp": "#990000", "cs": "#FFCC00", "m": "Trojans", "f": 1880, "t": "Private Nonprofit"}, "ucla": {"d": "Situated in the Westwood neighborhood of Los Angeles, this public research university is highly regarded for its medical center and competitive athletics program.", "w": "https://www.ucla.edu", "e": 46000, "cp": "#2D68C4", "cs": "#FFD100", "m": "Bruins (Joe and Josephine Bruin)", "f": 1919, "t": "Public"}, "uoft": {"d": "A public research university in Toronto that serves as Canada's most productive research institution and maintains a distinctive collegiate system similar to Oxbridge.", "w": "https://www.utoronto.ca", "e": 97000, "cp": "#00205B", "cs": "#FFFFFF", "m": "Varsity Blues", "f": 1827, "t": "Public"}, "tmu": {"d": "A public research university in downtown Toronto that emphasizes career-oriented education and urban integration, formerly known as Ryerson University.", "w": "https://www.torontomu.ca", "e": 48000, "cp": "#004C97", "cs": "#FFD200", "m": "Eggy the Ram", "f": 1948, "t": "Public"}, "york": {"d": "A large public research university in Toronto known for its diverse student body and its interdisciplinary approach to law, business, and the fine arts.", "w": "https://www.yorku.ca", "e": 55000, "cp": "#E31D2B", "cs": "#FFFFFF", "m": "Lions", "f": 1959, "t": "Public"}, "centennial": {"d": "A public college in Scarborough, Ontario, known for its diverse student body and extensive array of career-oriented diploma and degree programs.", "w": "https://www.centennialcollege.ca", "cp": "#00693E", "cs": "#FFC72C", "m": "Colts", "f": 1966, "t": "Community College"}, "ubc": {"d": "The University of British Columbia is a public research university with its main campus located in Vancouver, British Columbia.", "w": "https://www.ubc.ca", "e": 72000, "cp": "#002145", "cs": "#F7B731", "m": "Thunderbirds", "f": 1908, "t": "Public"}, "usyd": {"d": "Australia's oldest university, founded in 1850, is a major research institution known for its historic sandstone architecture and comprehensive academic offerings.", "w": "https://www.sydney.edu.au", "e": 74000, "cp": "#FFD700", "cs": "#00205B", "f": 1850, "t": "Public"}, "uts": {"d": "The University of Technology Sydney is a public research university located in Sydney, New South Wales.", "w": "https://www.uts.edu.au", "e": 45000, "cp": "#00A651", "cs": "#000000", "f": 1988, "t": "Public"}, "ust": {"d": "A historic Catholic research university in Manila founded in 1611, making it one of the oldest existing universities in Asia.", "w": "https://www.ust.edu.ph", "e": 42000, "cp": "#FFD700", "cs": "#FFFFFF", "m": "Growling Tigers", "f": 1611, "t": "Private Nonprofit"}, "dlsu": {"d": "A private Catholic research university in Manila founded by the Brothers of the Christian Schools, serving as the flagship institution of the De La Salle Philippines system.", "w": "https://www.dlsu.edu.ph", "e": 18500, "cp": "#007A33", "cs": "#FFFFFF", "m": "Green Archer", "f": 1911, "t": "Private Nonprofit"}, "swu": {"d": "Southwestern University is a private university located in Cebu City, Philippines, with a focus on medical and health science programs.", "w": "https://www.swu.edu.ph", "m": "Cobras", "f": 1946, "t": "Private Nonprofit"}};
/* University of Miami sample courses and documents (Campus Hub prototype) */
const UM_COURSES = [{"code": "BIL 150", "title": "General Biology", "docs": 24, "p1": true}, {"code": "CHM 113", "title": "Chemistry Laboratory I", "docs": 27, "p1": true}, {"code": "BIL 250", "title": "Genetics", "docs": 24, "p1": true}, {"code": "MGT 303", "title": "Operations Management", "docs": 20, "p1": true}, {"code": "CHM 206", "title": "Organic Chemistry Laboratory II", "docs": 20, "p1": true}, {"code": "NUR 315", "title": "Pathophysiology", "docs": 18, "p1": true}, {"code": "ECO 211", "title": "Economic Principles and Problems", "docs": 14, "p1": true}, {"code": "ACC 212", "title": "Managerial Accounting", "docs": 13, "p1": true}, {"code": "PSY 230", "title": "Psychology of Emotion", "docs": 11, "p1": true}, {"code": "BSL 212", "title": "Introduction to Business Law", "docs": 11, "p1": true}, {"code": "BMB 401", "title": "Biochemistry for the Biomedical Sciences", "docs": 11, "p1": true}, {"code": "MGT 304", "title": "Organizational Behavior", "docs": 10, "p1": true}, {"code": "CHM 205", "title": "Organic Chemistry Laboratory I", "docs": 10, "p1": true}, {"code": "BIL 255", "title": "Cellular and Molecular Biology", "docs": 9, "p1": true}, {"code": "PSY 250", "title": "Cognitive Psychology", "docs": 8, "p1": true}, {"code": "NUR 448", "title": "Psychiatric Mental Health Nursing", "docs": 8, "p1": true}, {"code": "MTH 107", "title": "Precalculus Mathematics I", "docs": 7, "p1": true}, {"code": "MAS 201", "title": "Introduction to Business Statistics", "docs": 7, "p1": true}, {"code": "ACC 530", "title": "International Financial Reporting Standards", "docs": 7, "p1": true}, {"code": "PSY 240", "title": "Abnormal Psychology", "docs": 7, "p1": true}, {"code": "EPS 291", "title": "Community and Character Development", "docs": 7, "p1": true}, {"code": "MKT 301", "title": "Marketing Foundations", "docs": 7, "p1": true}, {"code": "FIN 708", "title": "International Finance", "docs": 6, "p1": true}, {"code": "CHM 121", "title": "Principles of Chemistry", "docs": 6}, {"code": "MTH 161", "title": "Calculus I", "docs": 6}, {"code": "HIS 131", "title": "American History to 1865", "docs": 6}, {"code": "MUS 130", "title": "Music Appreciation", "docs": 6}, {"code": "REL 101", "title": "World Religions", "docs": 6}, {"code": "BIL 151", "title": "General Biology Laboratory", "docs": 5}, {"code": "PHY 102", "title": "College Physics II", "docs": 5}, {"code": "ENG 105", "title": "English Composition I", "docs": 5}, {"code": "ATM 103", "title": "Introduction to Meteorology", "docs": 5}, {"code": "COM 102", "title": "Public Speaking", "docs": 5}, {"code": "MTH 108", "title": "Precalculus Mathematics II", "docs": 5}, {"code": "PSY 110", "title": "Introduction to Psychology", "docs": 5}, {"code": "MTH 162", "title": "Calculus II", "docs": 5}, {"code": "ECO 303", "title": "Intermediate Macroeconomics", "docs": 4}, {"code": "CHM 151", "title": "Chemistry for Engineers", "docs": 4}, {"code": "ANT 101", "title": "Introduction to Anthropology", "docs": 4}, {"code": "MTH 309", "title": "Discrete Mathematics", "docs": 4}, {"code": "PHI 101", "title": "Introduction to Philosophy", "docs": 4}, {"code": "CHM 321", "title": "Physical Chemistry I", "docs": 4}, {"code": "PSY 335", "title": "Health Psychology", "docs": 4}, {"code": "MTH 151", "title": "Calculus I for Engineers", "docs": 4}, {"code": "PSY 290", "title": "Social Psychology", "docs": 4}, {"code": "HIS 101", "title": "Western Civilization I", "docs": 4}, {"code": "PSY 296", "title": "Statistics for Psychology", "docs": 4}, {"code": "PHY 101", "title": "College Physics I", "docs": 4}, {"code": "BIL 160", "title": "Evolution and Biodiversity", "docs": 3}, {"code": "MUS 101", "title": "Music Theory I", "docs": 3}, {"code": "BIL 335", "title": "Animal Physiology", "docs": 3}, {"code": "CHM 322", "title": "Physical Chemistry II", "docs": 3}, {"code": "BIL 368", "title": "Immunology", "docs": 3}, {"code": "PHI 110", "title": "Logic and Critical Thinking", "docs": 3}, {"code": "SOC 101", "title": "Introduction to Sociology", "docs": 3}, {"code": "SPA 101", "title": "Elementary Spanish I", "docs": 3}, {"code": "PSY 280", "title": "Developmental Psychology", "docs": 3}, {"code": "ECO 341", "title": "Money and Banking", "docs": 3}, {"code": "NUR 460", "title": "Nursing Leadership", "docs": 3}, {"code": "HIS 132", "title": "American History since 1865", "docs": 3}, {"code": "EPS 201", "title": "Lifespan Human Development", "docs": 3}, {"code": "CSC 421", "title": "Operating Systems", "docs": 3}, {"code": "ENG 106", "title": "Writing About Literature", "docs": 3}, {"code": "SPA 102", "title": "Elementary Spanish II", "docs": 3}, {"code": "ECE 202", "title": "Electric Circuits II", "docs": 3}, {"code": "MSC 111", "title": "Introduction to Marine Science", "docs": 3}, {"code": "BUS 200", "title": "Business Communication", "docs": 3}, {"code": "PSY 211", "title": "Research Methods in Psychology", "docs": 3}, {"code": "MTH 224", "title": "Introduction to Differential Equations", "docs": 2}, {"code": "FIN 405", "title": "Derivatives and Risk Management", "docs": 2}, {"code": "PHY 205", "title": "University Physics I", "docs": 2}, {"code": "MAS 311", "title": "Quantitative Methods for Management", "docs": 2}, {"code": "FIN 330", "title": "Corporate Finance", "docs": 2}, {"code": "BIL 161", "title": "Evolution and Biodiversity Laboratory", "docs": 2}, {"code": "BME 310", "title": "Biomechanics", "docs": 2}, {"code": "CAE 310", "title": "Mechanics of Materials", "docs": 2}, {"code": "NUR 340", "title": "Maternal-Newborn Nursing", "docs": 2}, {"code": "ANT 301", "title": "Archaeology of the Americas", "docs": 2}, {"code": "MKT 410", "title": "Brand Management", "docs": 2}, {"code": "MSC 112", "title": "Marine Science Laboratory", "docs": 2}, {"code": "ACC 311", "title": "Intermediate Accounting I", "docs": 2}, {"code": "BSL 312", "title": "Corporate and Commercial Law", "docs": 2}, {"code": "CHM 221", "title": "Quantitative Analysis", "docs": 2}, {"code": "BMB 402", "title": "Biochemistry II", "docs": 2}, {"code": "CAE 330", "title": "Fluid Mechanics", "docs": 2}, {"code": "COM 101", "title": "Introduction to Communication", "docs": 1}, {"code": "GEG 120", "title": "Physical Geography", "docs": 1}, {"code": "ECE 118", "title": "Introduction to Programming", "docs": 1}, {"code": "ECO 320", "title": "Econometrics", "docs": 1}, {"code": "FIN 302", "title": "Financial Management", "docs": 1}, {"code": "PHI 115", "title": "Ethics", "docs": 1}, {"code": "MTH 310", "title": "Probability and Statistics", "docs": 1}, {"code": "ECE 335", "title": "Digital Logic Design", "docs": 1}, {"code": "ACC 411", "title": "Auditing", "docs": 1}, {"code": "NUR 345", "title": "Pediatric Nursing", "docs": 1}, {"code": "CSC 322", "title": "Systems Programming", "docs": 1}, {"code": "ECO 302", "title": "Intermediate Microeconomics", "docs": 1}, {"code": "CSC 120", "title": "Computer Programming I", "docs": 1}, {"code": "SPA 301", "title": "Advanced Spanish Conversation", "docs": 1}, {"code": "MTH 210", "title": "Introduction to Linear Algebra", "docs": 1}, {"code": "CSC 431", "title": "Artificial Intelligence", "docs": 1}, {"code": "POL 212", "title": "Comparative Politics", "docs": 1}, {"code": "THA 101", "title": "Introduction to Theatre", "docs": 1}, {"code": "NUR 316", "title": "Adult Health Nursing I", "docs": 1}, {"code": "MTH 211", "title": "Calculus III", "docs": 1}, {"code": "ADV 201", "title": "Introduction to Advertising", "docs": 1}, {"code": "ECO 212", "title": "Economic Principles and Problems II", "docs": 1}, {"code": "POL 213", "title": "International Relations", "docs": 1}, {"code": "SOC 310", "title": "Race and Ethnicity", "docs": 1}, {"code": "EPS 340", "title": "Educational Psychology", "docs": 1}, {"code": "HIS 345", "title": "History of Florida", "docs": 1}, {"code": "NUR 300", "title": "Health Assessment", "docs": 1}, {"code": "PHY 315", "title": "Modern Physics", "docs": 1}, {"code": "POL 211", "title": "American Government", "docs": 1}, {"code": "ACC 315", "title": "Cost Accounting", "docs": 1}, {"code": "ACC 211", "title": "Financial Accounting", "docs": 1}, {"code": "ENG 260", "title": "Creative Writing", "docs": 1}, {"code": "CHM 202", "title": "Organic Chemistry II", "docs": 1}, {"code": "MAS 202", "title": "Business Statistics II", "docs": 1}, {"code": "MUS 245", "title": "History of Jazz", "docs": 1}, {"code": "MKT 401", "title": "Digital Marketing", "docs": 1}, {"code": "ARH 101", "title": "Art History: Prehistoric to Medieval", "docs": 1}, {"code": "SPA 211", "title": "Intermediate Spanish", "docs": 1}, {"code": "CHM 201", "title": "Organic Chemistry I", "docs": 1}, {"code": "HIS 220", "title": "History of Latin America", "docs": 1}, {"code": "BIL 330", "title": "Microbiology", "docs": 1}, {"code": "BUS 150", "title": "Business and Society", "docs": 1}, {"code": "NUR 410", "title": "Community Health Nursing", "docs": 1}, {"code": "NUR 312", "title": "Pharmacology for Nursing", "docs": 1}, {"code": "ECO 350", "title": "International Economics", "docs": 1}, {"code": "PHI 330", "title": "Philosophy of Mind", "docs": 1}, {"code": "ENG 211", "title": "The Novel", "docs": 1}, {"code": "FIN 320", "title": "Investments", "docs": 1}, {"code": "ATM 351", "title": "Climate Dynamics", "docs": 1}, {"code": "JOU 310", "title": "Data Journalism", "docs": 1}, {"code": "POL 360", "title": "Constitutional Law", "docs": 1}, {"code": "MGT 315", "title": "Project Management", "docs": 1}, {"code": "KIN 305", "title": "Exercise Physiology", "docs": 1}, {"code": "MGT 410", "title": "Entrepreneurship", "docs": 1}, {"code": "MSC 230", "title": "Coral Reef Ecology", "docs": 1}, {"code": "CAE 210", "title": "Statics", "docs": 1}, {"code": "PHY 206", "title": "University Physics II", "docs": 1}, {"code": "ACC 312", "title": "Intermediate Accounting II", "docs": 1}, {"code": "BIL 360", "title": "Neurobiology", "docs": 1}, {"code": "BIL 268", "title": "Ecology", "docs": 1}, {"code": "NUR 317", "title": "Adult Health Nursing II", "docs": 1}, {"code": "MKT 371", "title": "Consumer Behavior", "docs": 1}, {"code": "ACC 421", "title": "Federal Income Taxation", "docs": 1}, {"code": "KIN 310", "title": "Biomechanics of Human Movement", "docs": 1}, {"code": "PSY 310", "title": "Biological Psychology", "docs": 1}, {"code": "BME 375", "title": "Biomaterials", "docs": 1}, {"code": "BIL 375", "title": "Developmental Biology", "docs": 1}, {"code": "ATM 305", "title": "Hurricanes and Tropical Weather", "docs": 1}, {"code": "ANT 203", "title": "Biological Anthropology", "docs": 1}, {"code": "KIN 201", "title": "Human Anatomy for Kinesiology", "docs": 1}, {"code": "MGT 401", "title": "Strategic Management", "docs": 1}, {"code": "PHY 221", "title": "Physics Laboratory I", "docs": 1}, {"code": "CSC 427", "title": "Theory of Computing", "docs": 1}, {"code": "ADV 302", "title": "Copywriting and Creative Strategy", "docs": 1}, {"code": "SOC 220", "title": "Social Problems", "docs": 1}, {"code": "CSC 317", "title": "Data Structures and Algorithms", "docs": 1}, {"code": "MGT 403", "title": "Human Resource Management", "docs": 1}, {"code": "BME 211", "title": "Introduction to Biomedical Engineering", "docs": 1}, {"code": "ENG 210", "title": "Shakespeare", "docs": 1}, {"code": "ADV 405", "title": "Media Planning", "docs": 1}, {"code": "ECE 201", "title": "Electric Circuits I", "docs": 1}, {"code": "COM 312", "title": "Media Writing", "docs": 1}, {"code": "MSC 313", "title": "Biological Oceanography", "docs": 1}, {"code": "CSC 220", "title": "Computer Programming II", "docs": 1}, {"code": "JOU 201", "title": "Reporting and Writing I", "docs": 1}, {"code": "IEN 311", "title": "Engineering Economy", "docs": 1}, {"code": "ECE 316", "title": "Signals and Systems", "docs": 1}, {"code": "MSC 320", "title": "Marine Mammals", "docs": 1}, {"code": "ANT 201", "title": "Cultural Anthropology", "docs": 1}, {"code": "MKT 380", "title": "Marketing Research", "docs": 1}];
const CH_TYPES = { guide: 'Study guide', lab: 'Lab report', exam: 'Exam prep', notes: 'Class notes', practice: 'Practice set', cheat: 'Cheat sheet' };
const UM_POPULAR = [
{ title: 'Exam 2 Study Guide: Cell Division, Meiosis & Mendelian Genetics', code: 'BIL 150', type: 'guide', pages: 14, views: 2310, saves: 418 },
{ title: 'Titration & Standardization Lab Report', code: 'CHM 113', type: 'lab', pages: 9, views: 1874, saves: 302 },
{ title: 'Managerial Accounting Final Review: CVP, Budgets & Variances', code: 'ACC 212', type: 'exam', pages: 22, views: 1650, saves: 296 },
{ title: 'Pathophysiology Concept Maps: Cardiac & Renal', code: 'NUR 315', type: 'notes', pages: 18, views: 1422, saves: 275 },
{ title: 'Genetics Practice Problems with Worked Steps', code: 'BIL 250', type: 'practice', pages: 11, views: 1309, saves: 231 },
{ title: 'Operations Management Formula Sheet', code: 'MGT 303', type: 'cheat', pages: 2, views: 1188, saves: 260 },
{ title: 'Psychology of Emotion Lecture Notes, Weeks 1–6', code: 'PSY 230', type: 'notes', pages: 31, views: 964, saves: 150 },
{ title: 'Supply, Demand & Elasticity Cheat Sheet', code: 'ECO 211', type: 'cheat', pages: 3, views: 902, saves: 177 },
];
const UM_FRESH = [
{ title: 'Grignard Reaction Pre-lab Write-up', code: 'CHM 206', type: 'lab', pages: 6, ago: '2h ago' },
{ title: 'Metabolism Pathways Summary: Glycolysis to the ETC', code: 'BMB 401', type: 'guide', pages: 12, ago: 'Yesterday' },
{ title: 'Business Law Midterm Outline', code: 'BSL 212', type: 'exam', pages: 15, ago: '2 days ago' },
{ title: 'Psychiatric Nursing Medication Cards', code: 'NUR 448', type: 'notes', pages: 8, ago: '3 days ago' },
{ title: 'Unit Circle & Trig Identities One-Pager', code: 'MTH 107', type: 'cheat', pages: 1, ago: '4 days ago' },
{ title: 'Memory Models: Atkinson–Shiffrin to Working Memory', code: 'PSY 250', type: 'notes', pages: 10, ago: '5 days ago' },
];
/* the University of Miami prefixes → the 11 course categories (Course Hub art and glyphs) */
const UM_PREFIX_CAT = { BIL: 4, BMB: 4, MSC: 4, ATM: 2, CHM: 2, PHY: 2, MTH: 2, MAS: 2, EPS: 2, ECO: 5, ACC: 5, FIN: 5, MGT: 5, MKT: 5, BSL: 5, BUS: 5, ADV: 10, PSY: 6, SOC: 6, ANT: 6, GEG: 6, POL: 9, HIS: 7, ENG: 7, PHI: 7, SPA: 7, MUS: 7, ARH: 7, THA: 7, REL: 7, NUR: 8, KIN: 8, CSC: 1, ECE: 3, BME: 3, CAE: 3, IEN: 3, COM: 10, JOU: 10 };
const UM_UNSORTED = 701;
/* ===== 71a-dochub-data.js ===== */
/* ================= schools, courses and documents (from Documents hub round 2) ================= */
/* ================= data =================
The six Cebu schools marked live:true use the counts from the live page.
Everything else (other schools, course codes, document counts) is sample data. */
const LOCS = {
cebu: { label: 'Cebu City', full: 'Cebu City, Philippines', lat: 10.3157, lng: 123.8854, unit: 'km', radii: [2, 4, 6, 10], def: 2, country: 'Philippines', region: 'Central Visayas', city: 'Cebu City' },
manila: { label: 'Manila', full: 'Manila, Philippines', lat: 14.5995, lng: 120.9842, unit: 'km', radii: [3, 6, 10, 15], def: 3, country: 'Philippines', region: 'Metro Manila', city: 'Manila' },
miami: { label: 'Miami', full: 'Miami, Florida', lat: 25.7617, lng: -80.1918, unit: 'mi', radii: [5, 10, 15, 25], def: 2, country: 'United States', region: 'Florida', city: 'Miami' },
toronto: { label: 'Toronto', full: 'Toronto, Ontario', lat: 43.6532, lng: -79.3832, unit: 'km', radii: [5, 10, 15, 25], def: 3, country: 'Canada', region: 'Ontario', city: 'Toronto' },
sydney: { label: 'Sydney', full: 'Sydney, New South Wales', lat: -33.8688, lng: 151.2093, unit: 'km', radii: [3, 6, 10, 20], def: 3, country: 'Australia', region: 'New South Wales', city: 'Sydney' }
};
const SCHOOLS = [
{ id: 'usc', acr: 'USC', name: 'University of San Carlos – Main Campus', short: 'University of San Carlos', country: 'Philippines', region: 'Central Visayas', city: 'Cebu City', lat: 10.2998, lng: 123.8990, type: 'University', color: '#0B6E3A', docs: 35, courses: 28, live: true, domains: ['usc.edu.ph'], mascot: 'Warriors', aliases: [{ t: 'Carolinian', why: 'Students are Carolinians' }, { t: 'San Carlos' }], codes: ['ACCT 201', 'LAW 101', 'PHYS 1235L', 'ECON 102', 'CHEM 111', 'EDUC 101'] },
{ id: 'uc', acr: 'UC', name: 'University of Cebu – Main Campus', short: 'University of Cebu', country: 'Philippines', region: 'Central Visayas', city: 'Cebu City', lat: 10.2975, lng: 123.8977, type: 'University', color: '#1D4E9E', docs: 72, courses: 43, live: true, domains: ['uc.edu.ph'], mascot: 'Webmasters', aliases: [{ t: 'UC Main' }], codes: ['ACC 111', 'BSA 101', 'ME 101', 'CRIM 101', 'NURS 102'] },
{ id: 'swu', acr: 'SWU', name: 'Southwestern University, Cebu City', short: 'Southwestern University', country: 'Philippines', region: 'Central Visayas', city: 'Cebu City', lat: 10.3018, lng: 123.8920, type: 'University', color: '#1E7B4F', docs: 242, courses: 95, live: true, domains: ['swu.phinma.edu.ph'], mascot: 'Cobras', aliases: [{ t: 'SWU PHINMA' }, { t: 'Southwestern' }], codes: ['ACCT 2301', 'LAW 235', 'ECON 101', 'NURS 101', 'PHAR 201', 'MED 101'] },
{ id: 'cdu', acr: 'CDU', name: "Cebu Doctors' University", short: "Cebu Doctors' University", country: 'Philippines', region: 'Central Visayas', city: 'Mandaue City', lat: 10.3270, lng: 123.9230, type: 'University', color: '#0E7C86', docs: 21, courses: 20, live: true, domains: ['cdu.edu.ph'], aliases: [{ t: 'Cebu Doctors' }], codes: ['ENGLISH 2212', 'AVCL 4', 'ANAT 101', 'NURS 121', 'PHYS 101'] },
{ id: 'ctu', acr: 'CTU', name: 'Cebu Technological University – Main Campus', short: 'Cebu Technological University', country: 'Philippines', region: 'Central Visayas', city: 'Cebu City', lat: 10.2965, lng: 123.9065, type: 'University', color: '#7A1F2B', docs: 30, courses: 27, live: true, domains: ['ctu.edu.ph'], aliases: [{ t: 'Cebu State College of Science and Technology', why: 'Former name' }, { t: 'CSCST', why: 'Former name' }], codes: ['CAS 358', 'BIT-CT 101', 'EE 201', 'EDUC 102'] },
{ id: 'citu', acr: 'CIT-U', name: 'Cebu Institute of Technology – University', short: 'Cebu Institute of Technology', country: 'Philippines', region: 'Central Visayas', city: 'Cebu City', lat: 10.2946, lng: 123.8811, type: 'University', color: '#8C1D2C', docs: 25, courses: 20, live: true, domains: ['cit.edu'], mascot: 'Wildcats', aliases: [{ t: 'CIT', why: 'Short name' }], codes: ['MGMT 465', 'ACCTG 134', 'ACCTG 132', 'CPE 101', 'CE 201'] },
{ id: 'usjr', acr: 'USJ-R', name: 'University of San Jose–Recoletos', short: 'University of San Jose–Recoletos', country: 'Philippines', region: 'Central Visayas', city: 'Cebu City', lat: 10.2943, lng: 123.8975, type: 'University', color: '#0F6B3E', docs: 58, courses: 31, domains: ['usjr.edu.ph'], mascot: 'Jaguars', aliases: [{ t: 'USJR' }, { t: 'Recoletos' }], codes: ['ACCT 101', 'CS 111', 'LAW 201', 'PSYCH 101'] },
{ id: 'uv', acr: 'UV', name: 'University of the Visayas – Main Campus', short: 'University of the Visayas', country: 'Philippines', region: 'Central Visayas', city: 'Cebu City', lat: 10.2987, lng: 123.9003, type: 'University', color: '#127A3E', docs: 44, courses: 26, domains: ['uv.edu.ph'], mascot: 'Green Lancers', aliases: [], codes: ['CRIM 101', 'NURS 101', 'ACCT 101', 'ENG 101'] },
{ id: 'cnu', acr: 'CNU', name: 'Cebu Normal University', short: 'Cebu Normal University', country: 'Philippines', region: 'Central Visayas', city: 'Cebu City', lat: 10.3021, lng: 123.8966, type: 'University', color: '#1F3E8A', docs: 27, courses: 18, domains: ['cnu.edu.ph'], aliases: [{ t: 'Cebu Normal' }], codes: ['EDUC 101', 'PSYCH 101', 'MATH 101', 'NURS 101'] },
{ id: 'upcebu', acr: 'UP Cebu', name: 'University of the Philippines Cebu', short: 'UP Cebu', country: 'Philippines', region: 'Central Visayas', city: 'Cebu City', lat: 10.3225, lng: 123.8987, type: 'University', color: '#7B1113', docs: 63, courses: 34, domains: ['up.edu.ph'], mascot: 'Fighting Maroons', aliases: [{ t: 'UP', why: 'University of the Philippines' }, { t: 'UPC' }], codes: ['MATH 21', 'CMSC 11', 'ECON 11', 'COMM 10'] },
{ id: 'ust', acr: 'UST', name: 'University of Santo Tomas', short: 'University of Santo Tomas', country: 'Philippines', region: 'Metro Manila', city: 'Manila', lat: 14.6096, lng: 120.9894, type: 'University', color: '#D9A400', docs: 3120, courses: 640, domains: ['ust.edu.ph'], mascot: 'Growling Tigers', aliases: [{ t: 'Thomasian', why: 'Students are Thomasians' }, { t: 'Santo Tomas' }], codes: ['ACCT 101', 'NURS 101', 'MED 101', 'LAW 101', 'PSYCH 101'] },
{ id: 'upd', acr: 'UPD', name: 'University of the Philippines Diliman', short: 'UP Diliman', country: 'Philippines', region: 'Metro Manila', city: 'Quezon City', lat: 14.6538, lng: 121.0685, type: 'University', color: '#7B1113', docs: 4210, courses: 820, domains: ['up.edu.ph'], mascot: 'Fighting Maroons', aliases: [{ t: 'UP', why: 'University of the Philippines' }, { t: 'UP Diliman' }], codes: ['MATH 21', 'CMSC 11', 'ECON 11', 'KAS 1', 'PHYS 71'] },
{ id: 'admu', acr: 'ADMU', name: 'Ateneo de Manila University', short: 'Ateneo de Manila', country: 'Philippines', region: 'Metro Manila', city: 'Quezon City', lat: 14.6394, lng: 121.0780, type: 'University', color: '#0A3D7A', docs: 2380, courses: 510, domains: ['ateneo.edu', 'student.ateneo.edu'], mascot: 'Blue Eagles', aliases: [{ t: 'Ateneo' }], codes: ['MATH 10', 'EC 102', 'PH 101', 'CSCI 21'] },
{ id: 'dlsu', acr: 'DLSU', name: 'De La Salle University', short: 'De La Salle University', country: 'Philippines', region: 'Metro Manila', city: 'Manila', lat: 14.5648, lng: 120.9932, type: 'University', color: '#0B6E3A', docs: 2690, courses: 560, domains: ['dlsu.edu.ph'], mascot: 'Green Archers', aliases: [{ t: 'La Salle' }], codes: ['ACCTBA1', 'CCPROG1', 'GEMATMW', 'ECON 101'] },
{ id: 'feu', acr: 'FEU', name: 'Far Eastern University', short: 'Far Eastern University', country: 'Philippines', region: 'Metro Manila', city: 'Manila', lat: 14.6042, lng: 120.9867, type: 'University', color: '#0F5C2E', docs: 1460, courses: 380, domains: ['feu.edu.ph'], mascot: 'Tamaraws', aliases: [], codes: ['ACCT 101', 'NURS 101', 'ARCH 101', 'PSYCH 101'] },
{ id: 'um', acr: 'UM', name: 'University of Miami', short: 'University of Miami', country: 'United States', region: 'Florida', city: 'Coral Gables', lat: 25.7215, lng: -80.2793, type: 'University', color: '#F47321', docs: 1300, courses: 175, domains: ['miami.edu'], mascot: 'Hurricanes', aliases: [{ t: 'The U' }, { t: 'U Miami' }, { t: 'UMiami' }], codes: ['BIL 150', 'CHM 113', 'ECO 211', 'MTH 161', 'PSY 110'] },
{ id: 'fiu', acr: 'FIU', name: 'Florida International University', short: 'Florida International', country: 'United States', region: 'Florida', city: 'Miami', lat: 25.7562, lng: -80.3755, type: 'University', color: '#081E3F', docs: 26900, courses: 2600, domains: ['fiu.edu'], mascot: 'Panthers', aliases: [], codes: ['ACG 2021', 'PSY 2012', 'MAC 1105', 'ENC 1101', 'CHM 1045'] },
{ id: 'mdc', acr: 'MDC', name: 'Miami Dade College', short: 'Miami Dade College', country: 'United States', region: 'Florida', city: 'Miami', lat: 25.7778, lng: -80.1903, type: 'College', color: '#005EB8', docs: 9800, courses: 1300, domains: ['mdc.edu', 'mymdc.net'], mascot: 'Sharks', aliases: [{ t: 'Miami Dade' }], codes: ['ENC 1101', 'MAC 1105', 'BSC 2085', 'PSY 2012'] },
{ id: 'barry', acr: 'BU', name: 'Barry University', short: 'Barry University', country: 'United States', region: 'Florida', city: 'Miami Shores', lat: 25.8770, lng: -80.1937, type: 'University', color: '#A6192E', docs: 2100, courses: 420, domains: ['barry.edu', 'mymail.barry.edu'], mascot: 'Buccaneers', aliases: [{ t: 'Barry' }], codes: ['NUR 301', 'PSY 281', 'BIO 220'] },
{ id: 'stu', acr: 'STU', name: 'St. Thomas University', short: 'St. Thomas University', country: 'United States', region: 'Florida', city: 'Miami Gardens', lat: 25.9206, lng: -80.2534, type: 'University', color: '#00843D', docs: 1350, courses: 300, domains: ['stu.edu'], mascot: 'Bobcats', aliases: [{ t: 'Saint Thomas University' }], codes: ['LAW 501', 'BUS 201', 'PSY 101'] },
{ id: 'uf', acr: 'UF', name: 'University of Florida', short: 'University of Florida', country: 'United States', region: 'Florida', city: 'Gainesville', lat: 29.6436, lng: -82.3549, type: 'University', color: '#0021A5', docs: 41200, courses: 3900, domains: ['ufl.edu'], mascot: 'Gators', aliases: [{ t: 'Florida' }], codes: ['CHM 2045', 'MAC 2311', 'ECO 2013', 'PSY 2012'] },
{ id: 'ucf', acr: 'UCF', name: 'University of Central Florida', short: 'Central Florida', country: 'United States', region: 'Florida', city: 'Orlando', lat: 28.6024, lng: -81.2001, type: 'University', color: '#A88412', docs: 38600, courses: 3500, domains: ['ucf.edu'], mascot: 'Knights', aliases: [], codes: ['ACG 2021', 'COP 3502', 'PSY 2012', 'MAC 2311'] },
{ id: 'snhu', acr: 'SNHU', name: 'Southern New Hampshire University', short: 'Southern New Hampshire', country: 'United States', region: 'New Hampshire', city: 'Manchester', lat: 43.0390, lng: -71.4520, type: 'University', online: true, color: '#0A3370', docs: 61800, courses: 2400, domains: ['snhu.edu'], mascot: 'Penmen', aliases: [{ t: 'Southern New Hampshire' }], codes: ['ENG 123', 'MAT 240', 'IT 140', 'PSY 108', 'ACC 201'] },
{ id: 'wgu', acr: 'WGU', name: 'Western Governors University', short: 'Western Governors', country: 'United States', region: 'Utah', city: 'Salt Lake City', lat: 40.6846, lng: -111.8890, type: 'University', online: true, color: '#003057', docs: 52300, courses: 900, domains: ['wgu.edu'], mascot: 'Night Owls', aliases: [{ t: 'Western Governors' }], codes: ['C722', 'C215', 'D080', 'C207'] },
{ id: 'asu', acr: 'ASU', name: 'Arizona State University', short: 'Arizona State', country: 'United States', region: 'Arizona', city: 'Tempe', lat: 33.4242, lng: -111.9281, type: 'University', online: true, color: '#8C1D40', docs: 47800, courses: 4100, domains: ['asu.edu'], mascot: 'Sun Devils', aliases: [{ t: 'Arizona State' }], codes: ['MAT 265', 'PSY 101', 'ACC 231', 'ENG 101'] },
{ id: 'uscla', acr: 'USC', name: 'University of Southern California', short: 'Southern California', country: 'United States', region: 'California', city: 'Los Angeles', lat: 34.0224, lng: -118.2851, type: 'University', color: '#990000', docs: 21600, courses: 2800, domains: ['usc.edu'], mascot: 'Trojans', aliases: [{ t: 'Southern Cal' }, { t: 'SC' }], codes: ['WRIT 150', 'MATH 125', 'ECON 203'] },
{ id: 'ucla', acr: 'UCLA', name: 'University of California, Los Angeles', short: 'UCLA', country: 'United States', region: 'California', city: 'Los Angeles', lat: 34.0689, lng: -118.4452, type: 'University', color: '#2774AE', docs: 29400, courses: 3300, domains: ['ucla.edu', 'g.ucla.edu'], mascot: 'Bruins', aliases: [], codes: ['MATH 31A', 'CHEM 14A', 'PSYCH 10'] },
{ id: 'uoft', acr: 'U of T', name: 'University of Toronto', short: 'University of Toronto', country: 'Canada', region: 'Ontario', city: 'Toronto', lat: 43.6629, lng: -79.3957, type: 'University', color: '#1E3765', docs: 33100, courses: 3600, domains: ['utoronto.ca', 'mail.utoronto.ca'], mascot: 'Varsity Blues', aliases: [{ t: 'UofT' }, { t: 'UT' }], codes: ['MAT 135', 'CSC 108', 'ECO 101', 'PSY 100'] },
{ id: 'tmu', acr: 'TMU', name: 'Toronto Metropolitan University', short: 'Toronto Metropolitan', country: 'Canada', region: 'Ontario', city: 'Toronto', lat: 43.6577, lng: -79.3788, type: 'University', color: '#004C9B', docs: 14800, courses: 1900, domains: ['torontomu.ca'], mascot: 'Bold', aliases: [{ t: 'Ryerson', why: 'Formerly Ryerson University' }, { t: 'Ryerson University', why: 'Former name' }], codes: ['CPS 109', 'ACC 100', 'ECN 104'] },
{ id: 'york', acr: 'YorkU', name: 'York University', short: 'York University', country: 'Canada', region: 'Ontario', city: 'Toronto', lat: 43.7735, lng: -79.5019, type: 'University', color: '#E31837', docs: 19700, courses: 2500, domains: ['yorku.ca', 'my.yorku.ca'], mascot: 'Lions', aliases: [{ t: 'York' }], codes: ['ADMS 1010', 'MATH 1013', 'PSYC 1010'] },
{ id: 'centennial', acr: 'CC', name: 'Centennial College', short: 'Centennial College', country: 'Canada', region: 'Ontario', city: 'Toronto', lat: 43.7845, lng: -79.2275, type: 'College', color: '#1B365D', docs: 7400, courses: 1100, domains: ['centennialcollege.ca', 'my.centennialcollege.ca'], mascot: 'Colts', aliases: [{ t: 'Centennial' }], codes: ['COMP 100', 'ACCT 123', 'COMM 170'] },
{ id: 'ubc', acr: 'UBC', name: 'University of British Columbia', short: 'British Columbia', country: 'Canada', region: 'British Columbia', city: 'Vancouver', lat: 49.2606, lng: -123.2460, type: 'University', color: '#002145', docs: 28700, courses: 3400, domains: ['ubc.ca', 'student.ubc.ca'], mascot: 'Thunderbirds', aliases: [], codes: ['CPSC 110', 'ECON 101', 'PSYC 101', 'MATH 100'] },
{ id: 'unsw', acr: 'UNSW', name: 'UNSW Sydney', short: 'UNSW Sydney', country: 'Australia', region: 'New South Wales', city: 'Sydney', lat: -33.9173, lng: 151.2313, type: 'University', color: '#D9A900', docs: 24800, courses: 2900, domains: ['unsw.edu.au', 'student.unsw.edu.au', 'ad.unsw.edu.au'], aliases: [{ t: 'University of New South Wales', why: 'Full name' }], codes: ['MATH1131', 'COMP1511', 'ACCT1501', 'ECON1101'] },
{ id: 'usyd', acr: 'USYD', name: 'The University of Sydney', short: 'University of Sydney', country: 'Australia', region: 'New South Wales', city: 'Sydney', lat: -33.8888, lng: 151.1873, type: 'University', color: '#E64626', docs: 26100, courses: 3100, domains: ['sydney.edu.au', 'uni.sydney.edu.au'], aliases: [{ t: 'Sydney Uni' }, { t: 'University of Sydney' }], codes: ['MATH1021', 'ECON1001', 'PSYC1001'] },
{ id: 'uts', acr: 'UTS', name: 'University of Technology Sydney', short: 'UTS', country: 'Australia', region: 'New South Wales', city: 'Sydney', lat: -33.8832, lng: 151.2005, type: 'University', color: '#0F4BEB', docs: 15300, courses: 2100, domains: ['uts.edu.au', 'student.uts.edu.au'], aliases: [], codes: ['MATH 33230', 'ACCT 22107', 'PROG 31251'] },
{ id: 'mq', acr: 'MQ', name: 'Macquarie University', short: 'Macquarie University', country: 'Australia', region: 'New South Wales', city: 'Sydney', lat: -33.7738, lng: 151.1126, type: 'University', color: '#A6192E', docs: 12900, courses: 1800, domains: ['mq.edu.au', 'students.mq.edu.au'], aliases: [{ t: 'Macquarie' }], codes: ['ACCG1000', 'STAT1170', 'PSYU1101'] }
];
const BY_ID = {};
SCHOOLS.forEach(s => {
BY_ID[s.id] = s;
s.mini = s.acr.length > 4 ? (s.acr.replace(/[^A-Z]/g, '').slice(0, 3) || s.acr.slice(0, 3)) : s.acr;
s.k = { acr: key(s.acr), name: norm(s.name), short: norm(s.short), words: norm(s.name + ' ' + s.short).split(' '), alias: s.aliases.map(a => norm(a.t)) };
});
/* the University of Miami counts follow the Campus Hub sheet: course documents plus the unsorted ones */
BY_ID.um.docs = UM_COURSES.reduce((n, c) => n + c.docs, 0) + UM_UNSORTED; BY_ID.um.courses = UM_COURSES.length;
const CC = { 'Philippines': 'PH', 'United States': 'US', 'Canada': 'CA', 'Australia': 'AU' };
const CATS = [
{ id: 1, name: 'Computer Science & IT', tone: 'blue' },
{ id: 2, name: 'Math & Physical Sciences', tone: 'lav' },
{ id: 3, name: 'Engineering', tone: 'gold' },
{ id: 4, name: 'Biological Sciences', tone: 'mint' },
{ id: 5, name: 'Business & Economics', tone: 'blue' },
{ id: 6, name: 'Social Sciences', tone: 'lav' },
{ id: 7, name: 'Humanities & Arts', tone: 'gold' },
{ id: 8, name: 'Health & Nursing', tone: 'mint' },
{ id: 9, name: 'Education, Politics & Law', tone: 'blue' },
{ id: 10, name: 'Communication & Interdisciplinary', tone: 'lav' }
];
const CAT = id => CATS.find(c => c.id === id) || CATS[9];
const PFX = {
ACCT: [5, 'Accounting'], ACC: [5, 'Accounting'], ACG: [5, 'Financial Accounting'], ACCTG: [5, 'Accounting'], ACCG: [5, 'Accounting Foundations'], ACCTBA: [5, 'Basic Accounting'],
ADMS: [5, 'Intro to Administrative Studies'], BSA: [5, 'Accountancy'], BUS: [5, 'Business Fundamentals'], ECO: [5, 'Microeconomics'], ECON: [5, 'Economics'], EC: [5, 'Economics'], ECN: [5, 'Microeconomics'], MGMT: [5, 'Management'], C: [5, 'Business'], D: [5, 'Business'],
LAW: [9, 'Law'], EDUC: [9, 'Foundations of Education'], KAS: [7, 'Philippine History'],
ENG: [7, 'English Composition'], ENGLISH: [7, 'English'], ENC: [7, 'English Composition I'], WRIT: [7, 'Writing and Critical Inquiry'], PH: [7, 'Philosophy'], ARCH: [3, 'Architectural Design'], CAS: [10, 'Arts & Sciences'], COMM: [10, 'Communication'],
MATH: [2, 'Mathematics'], MAT: [2, 'Mathematics'], MAC: [2, 'College Algebra'], MTH: [2, 'Calculus'], PHYS: [2, 'Physics'], CHEM: [2, 'General Chemistry'], CHM: [2, 'General Chemistry'], STAT: [2, 'Statistics'], GEMATMW: [2, 'Mathematics in the Modern World'],
BIL: [4, 'General Biology'], BIO: [4, 'Biology'], BSC: [4, 'Anatomy & Physiology'],
PSY: [6, 'Psychology'], PSYC: [6, 'Psychology'], PSYCH: [6, 'Psychology'], PSYU: [6, 'Psychology'], CRIM: [6, 'Criminology'],
CS: [1, 'Computer Programming'], CSC: [1, 'Intro to Programming'], CSCI: [1, 'Data Structures'], CMSC: [1, 'Intro to Computer Science'], COMP: [1, 'Programming Fundamentals'], COP: [1, 'Computer Science I'], CPS: [1, 'Intro to Programming'], CPSC: [1, 'Program Design'], CCPROG: [1, 'Programming Fundamentals'], IT: [1, 'Intro to Scripting'], PROG: [1, 'Data Structures'],
CPE: [3, 'Computer Engineering'], CE: [3, 'Statics of Rigid Bodies'], EE: [3, 'Electrical Circuits'], ME: [3, 'Mechanical Engineering'], BIT: [3, 'Industrial Technology'],
NUR: [8, 'Nursing'], NURS: [8, 'Nursing'], ANAT: [8, 'Human Anatomy'], AVCL: [8, 'Allied Health'], MED: [8, 'Medical Foundations'], PHAR: [8, 'Pharmacology']
};
const EXACT = {
'ENG 123': 'English Composition II', 'MATH1131': 'Mathematics 1A', 'C722': 'Project Management', 'C215': 'Operations Management', 'D080': 'Managing in a Global Business Environment', 'C207': 'Data-Driven Decision Making',
'COMP1511': 'Programming Fundamentals', 'ACCT 2301': 'Principles of Financial Accounting', 'ACCT 201': 'Partnership & Corporate Accounting', 'LAW 235': 'Obligations & Contracts', 'LAW 101': 'Introduction to Law',
'PHYS 1235L': 'Physics Laboratory', 'ENC 1101': 'English Composition I', 'PSY 2012': 'General Psychology', 'ACG 2021': 'Principles of Financial Accounting', 'MAT 240': 'Applied Statistics', 'ENGLISH 2212': 'Purposive Communication', 'MGMT 465': 'Strategic Management'
};
const DOC_OVERRIDE = { 'snhu|ENG 123': 6689, 'unsw|MATH1131': 1837, 'wgu|C722': 2410, 'fiu|PSY 2012': 1512, 'unsw|COMP1511': 1388, 'ubc|CPSC 110': 1204 };
const TYPE_TONE = { 'Notes': 'mint', 'Past exam': 'gold', 'Study guide': 'blue', 'Practice': 'lav' };
const DOC_T = [
['Study guide', c => `${c.code} midterm reviewer`],
['Notes', c => `${c.title} lecture notes, weeks 1–6`],
['Past exam', c => `${c.code} final exam, previous term`],
['Practice', c => `${c.code} problem set with solutions`],
['Notes', c => `${c.title} chapter summaries`],
['Study guide', c => `${c.code} formulas and key terms`]
];
const TRENDING = [
{ title: 'Notes for Project Management 101', type: 'Notes', code: 'C722', sid: 'wgu', rate: 96, votes: 48, pages: 14 },
{ title: 'Bonding Practice Problems for Module Four', type: 'Practice', code: 'CHEM 1151', sid: null, rate: 92, votes: 25, pages: 6 },
{ title: 'HESI Leadership and Management Exam V1 & V2 (2023–2024)', type: 'Past exam', code: 'NUR 2211C', sid: null, rate: 89, votes: 71, pages: 32 },
{ title: 'ENG 123 Project One: rhetorical analysis outline', type: 'Study guide', code: 'ENG 123', sid: 'snhu', rate: 94, votes: 112, pages: 5 }
];
const EXAMPLES = [
{ q: 'USC', note: 'Acronym · two schools share it', icon: 'i-abc' },
{ q: 'Ryerson', note: 'Old name · now Toronto Metropolitan', icon: 'i-history' },
{ q: 'ACCT 201', note: 'Course code', icon: 'i-hash' },
{ q: 'you@usc.edu.ph', note: 'Your school email', icon: 'i-mail' },
{ q: 'Hurricanes', note: 'Mascot', icon: 'i-paw' }
];
const KINDS = [
{ v: 'University', label: 'University', note: 'Bachelor’s and graduate programs', icon: 'i-building', tone: 'blue' },
{ v: 'College', label: 'College', note: 'Community and technical colleges', icon: 'i-cap', tone: 'mint' },
{ v: 'Online', label: 'Online school', note: 'Study fully online', icon: 'i-laptop', tone: 'lav' },
{ v: 'High school', label: 'High school', note: 'Senior high and secondary', icon: 'i-pencil', tone: 'gold' }
];
const AGO = ['8 min ago', '35 min ago', '2 hours ago', '4 hours ago', 'Yesterday', 'Yesterday'];
const CAPS = {
search: '<b>Module: smart search.</b> One box understands school names, nicknames, old names, acronyms, mascots, course codes and school email addresses.',
hoot: '<b>Module: Mr. Hoot’s 3-tap match.</b> A guided welcome confirms where you study, then turns into your campus pass on every visit after.',
atlas: '<b>Module: campus atlas.</b> Browse country → region → city → school, or jump through every school A–Z. Arrow keys move between columns.',
radar: '<b>Module: around you.</b> New direction. A map of the closest schools, plus a course-code lookup for students who know their syllabus.'
};
const VIEWS = ['search', 'hoot', 'atlas', 'radar'];
/* ================= derived data ================= */
function courseMeta(code) {
const m = code.match(/^[A-Za-z]+/); const p = m ? m[0].toUpperCase() : '';
const base = PFX[p] || [10, 'General Studies'];
return { cat: base[0], title: EXACT[code] || base[1] };
}
function coursesOf(s) {
if (s._c) return s._c;
if (s.id === 'um') {
s._c = UM_COURSES.map(c => ({ code: c.code, title: c.title, cat: UM_PREFIX_CAT[c.code.split(' ')[0]] || courseMeta(c.code).cat, docs: c.docs, s, key: s.id + '|' + c.code }));
return s._c;
}
const per = s.docs / s.courses;
s._c = s.codes.map(code => {
const meta = courseMeta(code); const h = hsh(s.id + code);
const docs = DOC_OVERRIDE[s.id + '|' + code] || Math.max(1, Math.round(per * (0.8 + (h % 160) / 100)));
return { code, title: meta.title, cat: meta.cat, docs, s, key: s.id + '|' + code };
});
return s._c;
}
const ALL_COURSES = SCHOOLS.flatMap(coursesOf);
function docsOf(c) {
/* the course's real list (71d-links.js), in the Documents hub's shape */
if (c._d) return c._d;
c._d = courseDocs(c.s.id, c.code).map(hubDoc);
return c._d;
}
let _di = null;
function docIndex() {
if (_di) return _di;
_di = TRENDING.map(d => Object.assign(d, { k: norm(d.title + ' ' + d.code) }));
ALL_COURSES.forEach(c => docsOf(c).slice(0, 10).forEach(d => { d.k = norm(d.title + ' ' + c.code); _di.push(d); }));
return _di;
}
const docCode = d => (d.c ? d.c.code : d.code);
const docSchool = d => (d.c ? d.c.s : (d.sid ? BY_ID[d.sid] : null));
const docWhere = d => { const s = docSchool(d); return docCode(d) + (s ? ' · ' + s.short : ''); };
const TREE = {};
SCHOOLS.forEach(s => { TREE[s.country] = TREE[s.country] || {}; TREE[s.country][s.region] = TREE[s.country][s.region] || {}; (TREE[s.country][s.region][s.city] = TREE[s.country][s.region][s.city] || []).push(s.id); });
const schoolsIn = (country, region, city) => SCHOOLS.filter(s => s.country === country && (!region || s.region === region) && (!city || s.city === city));
const sumDocs = list => list.reduce((n, s) => n + s.docs, 0);
/* ================= geography ================= */
const rad = d => d * Math.PI / 180;
function km(a1, o1, a2, o2) { const dA = rad(a2 - a1), dO = rad(o2 - o1); const x = Math.sin(dA / 2) ** 2 + Math.cos(rad(a1)) * Math.cos(rad(a2)) * Math.sin(dO / 2) ** 2; return 6371 * 2 * Math.atan2(Math.sqrt(x), Math.sqrt(1 - x)); }
const LOC = () => LOCS[S.loc] || LOCS.cebu;
function dist(s) { const L = LOC(); const k = km(L.lat, L.lng, s.lat, s.lng); return L.unit === 'mi' ? k * 0.621371 : k; }
const nearMax = () => { const L = LOC(); return L.radii[L.radii.length - 1]; };
function distTxt(s) { const L = LOC(), d = dist(s); if (d > nearMax() * 3) return ''; return (d < 10 ? d.toFixed(1) : Math.round(d)) + ' ' + L.unit; }
function nearList(filter) { return SCHOOLS.filter(s => dist(s) <= nearMax() && (!filter || filter(s))).sort((a, b) => dist(a) - dist(b)); }
function bearing(L, s) { const p1 = rad(L.lat), p2 = rad(s.lat), dl = rad(s.lng - L.lng); return Math.atan2(Math.sin(dl) * Math.cos(p2), Math.cos(p1) * Math.sin(p2) - Math.sin(p1) * Math.cos(p2) * Math.cos(dl)); }
const placeTxt = s => s.country === 'Philippines' ? `${s.city}, Philippines` : `${s.city}, ${s.region}`;
function topCourses(scope, cat) { let list = scope === 'near' ? nearList().flatMap(coursesOf) : ALL_COURSES; if (cat) list = list.filter(c => c.cat === cat); return list.slice().sort((a, b) => b.docs - a.docs); }
function gridFor(n) { if (n <= 2) return 'grid2'; if (n % 3 === 0) return 'grid3'; if (n % 4 === 0) return 'grid4'; if (n % 2 === 0) return 'grid2'; return 'grid3'; }
/* ================= smart search engine ================= */
function lev(a, b) {
if (a === b) return 0; const m = a.length, n = b.length; if (!m) return n; if (!n) return m;
let prev = Array.from({ length: n + 1 }, (_, i) => i);
for (let i = 1; i <= m; i++) { const cur = [i]; for (let j = 1; j <= n; j++) cur[j] = Math.min(prev[j] + 1, cur[j - 1] + 1, prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1)); prev = cur; }
return prev[n];
}
const STOP = new Set(['of', 'the', 'and', 'at', 'de', 'la', 'university', 'college', 'campus', 'main', 'school']);
function scoreSchool(s, q, toks) {
let best = null; const hit = (sc, why) => { if (!best || sc > best.sc) best = { sc, why }; };
const qk = q.replace(/\s/g, '');
if (qk === s.k.acr) hit(100, 'Acronym');
s.aliases.forEach((a, i) => { const ak = s.k.alias[i]; const why = a.why || `Also called “${a.t}”`; if (ak === q || ak.replace(/\s/g, '') === qk) hit(96, why); else if (q.length >= 3 && ak.startsWith(q)) hit(76, why); });
if (s.k.name.startsWith(q) || s.k.short.startsWith(q)) hit(90, null);
else if (toks.length && toks.every(t => s.k.words.some(w => w.startsWith(t)))) hit(toks.some(t => !STOP.has(t)) ? 82 : 30, null);
else if (q.length >= 3 && s.k.name.includes(q)) hit(64, null);
if (s.mascot && q.length >= 3 && norm(s.mascot).startsWith(q)) hit(72, 'Mascot: ' + s.mascot);
if (q.length >= 3 && norm(s.city).startsWith(q)) hit(58, 'In ' + s.city);
if (!best && q.length >= 4) {
const content = toks.filter(t => t.length >= 4 && !STOP.has(t));
const pool = s.k.words.concat(s.k.alias.join(' ').split(' '));
if (content.length && content.every(t => pool.some(w => w.length >= 3 && (w.startsWith(t) || lev(t, w.slice(0, t.length)) <= (t.length >= 6 ? 2 : 1))))) hit(40, 'Close match');
}
if (best) { if (dist(s) <= nearMax()) best.sc += 6; if (S.mine === s.id) best.sc += 4; }
return best;
}
function search(raw) {
const q = norm(raw), out = { q, email: [], schools: [], courses: [], docs: [], dom: null, code: null };
if (!q) return out;
const t = raw.trim().toLowerCase();
let dom = null;
if (t.includes('@')) dom = t.split('@').pop().trim();
else if (/^[a-z0-9-]+(\.[a-z0-9-]+)+$/.test(t) && /\.(edu|ac|ca|au|ph|net|org)/.test(t)) dom = t;
if (dom !== null) {
out.dom = dom;
if (dom.includes('.')) SCHOOLS.forEach(s => { const m = s.domains.find(d => dom === d || dom.endsWith('.' + d)); if (m) out.email.push({ s, m }); });
out.email.sort((a, b) => dist(a.s) - dist(b.s));
return out;
}
const toks = q.split(' ').filter(Boolean);
out.schools = SCHOOLS.map(s => ({ s, r: scoreSchool(s, q, toks) })).filter(x => x.r).sort((a, b) => b.r.sc - a.r.sc || dist(a.s) - dist(b.s)).slice(0, 6);
const qk = q.replace(/\s/g, '');
if (/^[a-z]{1,8}\d{1,6}[a-z]?$/.test(qk) || (/^[a-z]{2,8}$/.test(qk) && PFX[qk.toUpperCase()] && qk.length > 1)) {
out.code = raw.trim().toUpperCase();
out.courses = ALL_COURSES.filter(c => key(c.code).startsWith(qk)).sort((a, b) => { const na = dist(a.s) <= nearMax() ? 1 : 0, nb = dist(b.s) <= nearMax() ? 1 : 0; return (nb - na) || (na ? dist(a.s) - dist(b.s) : b.docs - a.docs); }).slice(0, 5);
}
if (q.length >= 3) {
out.docs = docIndex().filter(d => toks.every(tk => d.k.includes(tk))).sort((a, b) => { const sa = docSchool(a), sb = docSchool(b); const na = sa && dist(sa) <= nearMax() ? 1 : 0, nb = sb && dist(sb) <= nearMax() ? 1 : 0; return nb - na; }).slice(0, 3);
}
return out;
}
function hl(text, q) {
let toks = q.split(' ').filter(Boolean); if (!toks.length) return esc(text);
if (toks.some(t => !STOP.has(t))) toks = toks.filter(t => !STOP.has(t) || t.length > 3);
const low = text.toLowerCase(); const ranges = [];
toks.forEach(t => { const re = new RegExp('(^|[^a-z0-9])(' + t.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + ')', 'g'); let m; while ((m = re.exec(low))) { const st = m.index + m[1].length; ranges.push([st, st + t.length]); if (re.lastIndex === m.index) re.lastIndex++; } });
if (!ranges.length) return esc(text);
ranges.sort((a, b) => a[0] - b[0]); const merged = [];
ranges.forEach(r => { const l = merged[merged.length - 1]; if (l && r[0] <= l[1]) l[1] = Math.max(l[1], r[1]); else merged.push(r.slice()); });
let out = '', p = 0; merged.forEach(([a, b]) => { out += esc(text.slice(p, a)) + '<mark>' + esc(text.slice(a, b)) + '</mark>'; p = b; });
return out + esc(text.slice(p));
}
/* ===== 71c-mydata.js ===== */
/* ================= the student, her room and her documents (sample data) ================= */
const PERSONA = { first: 'Maya', full: 'Maya Santos', initials: 'MS', email: 'maya.santos@miami.edu', plan: 'Free' };
const SHELF_COLORS = { teal: '#14b8a6', sky: '#0ea5e9', violet: '#8b5cf6', amber: '#f59e0b', rose: '#f43f5e', emerald: '#10b981', slate: '#64748b' };
/* Every document the prototype can open. kind = how its first page is drawn on the document page. */
const DOCS = {};
function addDoc(d) { d.id = d.id || slug(d.title).slice(0, 40); DOCS[d.id] = d; return d; }
[
{ id: 'mitotic', title: 'Worksheet on Mitotic Index and Phase Indices', code: 'BIL 150', type: 'Worksheet', pages: 3, pro: true, up: 9, down: 1, kind: 'ws', views: 1248, added: 'Sep 14, 2026', topics: ['Mitotic index', 'Phase index', 'Cell cycle', 'Mitosis', 'Onion root tip', 'Data tables'], summary: 'A BIL 150 lab worksheet. You calculate the mitotic index for 12 onion root tip control samples, work out phase indices for each stage of mitosis, then compare treatment against control.' },
{ id: 'mitosis-stats', title: 'Statistical Analysis of Mitosis Worksheet', code: 'BIL 150', type: 'Worksheet', pages: 3, pro: true, up: 15, down: 1, kind: 'ws' },
{ id: 'module2', title: 'Module 2 Study Guide - Chemistry of Life', code: 'BIL 150', type: 'Study guide', pages: 11, pro: true, up: 22, down: 3, kind: 'defs' },
{ id: 'evolution', title: 'Evolution and Cell Structures - A Study Guide', code: 'BIL 150', type: 'Study guide', pages: 28, pro: true, up: 45, down: 2, kind: 'bullets' },
{ id: 'core-bio', title: 'Core Concepts in Biology - Evolution, Cells, Genetics', code: 'BIL 150', type: 'Practice test', pages: 21, pro: true, up: 30, down: 3, kind: 'mcq' },
{ id: 'quiz-corr', title: 'Original Quiz and Corrections', code: 'BIL 150', type: 'Quiz', pages: 4, pro: true, up: 7, down: 2, kind: 'hand' },
{ id: 'bil-exam1', title: 'BIL150 Exam 1', code: 'BIL 150', type: 'Exam', pages: 3, pro: true, up: 11, down: 2, kind: 'hand2' },
{ id: 'cell-cycle', title: 'Cell Cycle and Mitosis Lecture Notes', code: 'BIL 150', type: 'Notes', pages: 9, pro: false, up: 22, down: 2, kind: 'notes' },
{ id: 'onion-lab', title: 'Onion Root Tip Mitosis Lab Report', code: 'BIL 151', type: 'Lab report', pages: 6, pro: true, up: 16, down: 2, kind: 'report' },
{ id: 'exam2-guide', title: 'Exam 2 Study Guide: Cell Division, Meiosis & Mendelian Genetics', code: 'BIL 150', type: 'Study guide', pages: 14, pro: true, up: 38, down: 3, kind: 'bullets' },
{ id: 'cell-transport', title: 'Cell Transport Study Guide', code: 'BIL 150', type: 'Study guide', pages: 14, pro: false, up: 41, down: 4, kind: 'defs' },
{ id: 'titration', title: 'Titration & Standardization Lab Report', code: 'CHM 113', type: 'Lab report', pages: 9, pro: true, up: 19, down: 2, kind: 'report' },
{ id: 'stoich', title: 'Stoichiometry Practice Set', code: 'CHM 113', type: 'Practice set', pages: 6, pro: false, up: 27, down: 3, kind: 'mcq' },
{ id: 'limiting', title: 'Limiting Reagents Worksheet', code: 'CHM 113', type: 'Worksheet', pages: 4, pro: true, up: 12, down: 1, kind: 'ws' },
{ id: 'unit3-review', title: 'Unit 3 Review Slides', code: 'CHM 113', type: 'Notes', pages: 18, pro: true, up: 9, down: 1, kind: 'notes' },
{ id: 'conditioning', title: 'Classical Conditioning Notes', code: 'PSY 110', type: 'Notes', pages: 7, pro: false, up: 24, down: 2, kind: 'notes' },
{ id: 'research-quiz', title: 'Research Methods Quiz', code: 'PSY 110', type: 'Quiz', pages: 3, pro: true, up: 13, down: 2, kind: 'mcq' },
{ id: 'memory-lec', title: 'Memory and Learning Lecture', code: 'PSY 110', type: 'Notes', pages: 12, pro: true, up: 18, down: 1, kind: 'notes' },
{ id: 'stats-sheet', title: 'Stats Formula Sheet', code: 'PSY 110', type: 'Cheat sheet', pages: 2, pro: false, up: 31, down: 2, kind: 'defs' },
{ id: 'ethics', title: 'Ethics Review', code: 'PSY 110', type: 'Study guide', pages: 5, pro: true, up: 8, down: 1, kind: 'bullets' }
].forEach(addDoc);
const docById = id => DOCS[id];
const docUp = d => Math.round(d.up / Math.max(1, d.up + d.down) * 100);
/* shelves: three of Maya's own, plus the editable example a brand-new student starts with */
const SHELVES = [
{ id: 'bil', name: 'BIL 150 · Midterm', color: 'teal', course: 'BIL 150', docs: ['mitotic', 'cell-transport', 'module2', 'evolution', 'exam2-guide'], exam: { title: 'BIL 150 Midterm', days: 3, time: '9:00 AM' }, tpl: 'Exam countdown' },
{ id: 'chm', name: 'CHM 113 · Unit 3', color: 'amber', course: 'CHM 113', docs: ['stoich', 'limiting', 'titration'], exam: { title: 'Unit 3 quiz', days: 6, time: '' }, tpl: 'Student planner' },
{ id: 'fin', name: 'Finals Week', color: 'rose', course: 'PSY 110', docs: ['conditioning', 'research-quiz', 'memory-lec', 'stats-sheet', 'ethics'], exam: { title: 'PSY 110 Final', days: 51, time: '' }, tpl: 'Task board' }
];
const EXAMPLE_SHELF = { id: 'start', name: 'Getting started', color: 'rose', example: true, docs: [], exam: null, tpl: 'Example' };
const SHELF_TASKS = {
bil: [
{ id: 'b1', t: 'Read ch. 3–4: Cell transport', st: 'todo', day: 0, doc: 'cell-transport', pri: 'high' },
{ id: 'b2', t: 'Flashcards: membrane terms (18 left)', st: 'todo', day: 1, doc: null },
{ id: 'b3', t: 'Lab 2 write-up: osmosis in potato cells', st: 'todo', day: 3, doc: 'mitotic', pri: 'high' },
{ id: 'b4', t: 'Re-watch lecture 4 (enzyme kinetics)', st: 'todo', day: 4, doc: 'module2' },
{ id: 'b5', t: 'Practice quiz: osmosis vs diffusion', st: 'doing', day: 0, doc: 'cell-transport' },
{ id: 'b6', t: 'One-page summary sheet, ch. 5–6', st: 'doing', day: 2, doc: 'exam2-guide' },
{ id: 'b7', t: 'Set up the shelf from “Exam countdown”', st: 'done', day: -8, doc: null },
{ id: 'b8', t: 'Skim the Module 2 study guide', st: 'done', day: -6, doc: 'module2' },
{ id: 'b9', t: 'Ask Hoot: Km vs Vmax, explain simply', st: 'done', day: -5, doc: null }
],
chm: [
{ id: 'c1', t: 'Redox practice set, problems 1–12', st: 'todo', day: 1, doc: 'stoich' },
{ id: 'c2', t: 'Review stoichiometry notes', st: 'todo', day: 2, doc: 'stoich' },
{ id: 'c3', t: 'Lab report 3: titration curves', st: 'doing', day: 4, doc: 'titration', pri: 'high' },
{ id: 'c4', t: 'Limiting reagents worksheet', st: 'todo', day: 5, doc: 'limiting' },
{ id: 'c5', t: 'Read the unit 3 review slides', st: 'done', day: -2, doc: 'unit3-review' }
],
fin: [
{ id: 'f1', t: 'Outline PSY 110 ch. 1–4', st: 'todo', day: 5, doc: 'conditioning' },
{ id: 'f2', t: 'Make a stats formula card', st: 'todo', day: 9, doc: 'stats-sheet' },
{ id: 'f3', t: 'Research methods quiz, twice', st: 'todo', day: 12, doc: 'research-quiz' },
{ id: 'f4', t: 'Memory and learning: one-page summary', st: 'doing', day: 14, doc: 'memory-lec' }
],
start: [
{ id: 's1', t: 'Rename this shelf to one of your courses', st: 'todo', day: 0, doc: null },
{ id: 's2', t: 'Drop a document here to file it', st: 'todo', day: 1, doc: null },
{ id: 's3', t: 'Delete this shelf when you’re done', st: 'todo', day: 2, doc: null }
]
};
const DEFAULT_NOTES = [
{ id: 'n1', text: 'Midterm Thu: ch. 3–6 + lab 2' },
{ id: 'n2', text: 'Ask about osmosis vs diffusion' },
{ id: 'n3', text: 'Lab partner: Sam, Thu 2pm' }
];
const AI_COURSES = [
{ id: 'cellbio', name: 'Cell Biology Foundations', from: 'BIL 150 · Midterm', pct: 42, next: 'Passive transport' },
{ id: 'stoichboot', name: 'Stoichiometry Bootcamp', from: 'CHM 113 · Unit 3', pct: 15, next: 'Mole ratios' }
];
const NOTIFS = [
{ ic: 'i-doc', tone: 'tb', t: '<b>3 new documents in BIL 150</b>', s: 'Including an Exam 2 study guide · 2h ago', go: 'course-um.bil150', unread: true },
{ ic: 'i-clock', tone: 'pin', t: '<b>BIL 150 Midterm in 3 days</b>', s: '4 tasks left on your shelf · Today', go: 'shelf-bil', unread: true },
{ ic: 'i-remix', tone: 'tc', t: '<b>Priya remixed your Mitosis Sprint</b>', s: 'Now a 10-minute boss battle · Yesterday', go: 'study', unread: false },
{ ic: 'i-star', tone: 'ta', t: '<b>Your notes got 12 thumbs up</b>', s: 'Cell Transport Study Guide · 2 days ago', go: 'doc-cell-transport', unread: false }
];
/* ================= Course Hub data (from the Calm hub) ================= */
const CH_CATS = {
1: { name: 'Computer Science & IT', deep: '#3340A8', mid: '#5B6BEA', light: '#C9CFFB', tint: '#EEF0FF' },
2: { name: 'Math & Physical Sciences', deep: '#5B36B0', mid: '#8C66E0', light: '#DCCFFA', tint: '#F3EEFF' },
3: { name: 'Engineering', deep: '#A3480B', mid: '#F08A3C', light: '#FDD5B5', tint: '#FFF2E7' },
4: { name: 'Biological Sciences', deep: '#17703F', mid: '#3FB477', light: '#BFE9CF', tint: '#EAF7EF' },
5: { name: 'Business & Economics', deep: '#7E5A00', mid: '#E2AE1F', light: '#F8E2A0', tint: '#FEF7E1' },
6: { name: 'Social Sciences', deep: '#155F8A', mid: '#3CA2DA', light: '#BFE2F5', tint: '#EAF6FC' },
7: { name: 'Humanities & Arts', deep: '#9A4127', mid: '#D2704F', light: '#F4CDBE', tint: '#FCEFEA' },
8: { name: 'Health & Nursing', deep: '#B32745', mid: '#E8587A', light: '#F9C6D2', tint: '#FDEDF1' },
9: { name: 'Education, Politics & Law', deep: '#6E4518', mid: '#B98143', light: '#EBD3B4', tint: '#F8F0E6' },
10: { name: 'Communication', deep: '#8E2672', mid: '#D25AAE', light: '#F2C6E4', tint: '#FCEEF8' },
11: { name: 'Study essentials', deep: '#0A6157', mid: '#19A394', light: '#BCE8E1', tint: '#E8F6F3' }
};
const CH_DOC_TYPES = {
guide: { label: 'Study guide', plural: 'Study guides', kind: 'guide' },
exam: { label: 'Exam & quiz', plural: 'Exams & quizzes', kind: 'exam' },
notes: { label: 'Notes', plural: 'Notes', kind: 'notes' },
worksheet: { label: 'Worksheet', plural: 'Worksheets', kind: 'sheet' }
};
const CH_TYPE_ORDER = ['guide', 'exam', 'notes', 'worksheet'];
/* [title, type, pages, free, thumb, doc id when the document page exists] */
const BIL_DOCS = [
['Statistical Analysis of Mitosis Worksheet', 'worksheet', 3, 0, 'sheet', 'mitosis-stats'],
['Worksheet on Mitotic Index and Phase Indices', 'worksheet', 3, 0, 'sheet', 'mitotic'],
['Module 2 Study Guide - Chemistry of Life', 'guide', 11, 0, 'guide', 'module2'],
['Evolution and Cell Structures - A Study Guide', 'guide', 28, 0, 'guide', 'evolution'],
['Core Concepts in Biology - Evolution, Cells, Genetics', 'guide', 21, 0, 'guide', 'core-bio'],
['Original Quiz and Corrections', 'exam', 4, 0, 'scan', 'quiz-corr'],
['BIL150 Exam 1', 'exam', 3, 0, 'scan', 'bil-exam1'],
['Fall 2021 Quiz 1', 'exam', 2, 0, 'exam'],
['Fall 2021 Quiz 2', 'exam', 2, 0, 'exam'],
['ATP Production in Cells', 'notes', 0, 0, 'notes'],
['Final Review Questions for BIL 150', 'exam', 0, 0, 'exam'],
['Exam Two Notes for General Biology', 'notes', 0, 0, 'notes'],
['Exam One Notes for Biology', 'notes', 0, 0, 'notes'],
['Physiology of Neurons', 'notes', 0, 0, 'notes'],
['Organelles in Cells', 'notes', 0, 0, 'notes'],
['Exam Four Notes for General Biology', 'notes', 0, 0, 'notes'],
["Dr. Mallory's Test One Notes", 'notes', 0, 0, 'notes'],
['Cell Transport Study Guide', 'guide', 14, 1, 'guide', 'cell-transport'],
['Development of Vertebrates', 'notes', 0, 0, 'notes'],
['Extra Module on Photosynthesis', 'notes', 0, 1, 'notes'],
['Cell Cycle and Mitosis Lecture Notes', 'notes', 9, 1, 'notes', 'cell-cycle'],
['Cell Metabolism Basics', 'notes', 0, 1, 'notes'],
['General Biology Test Two Class Notes', 'notes', 0, 1, 'notes'],
['Exam 2 Study Guide: Cell Division, Meiosis & Mendelian Genetics', 'guide', 14, 0, 'guide', 'exam2-guide']
];
const GENERIC_DOCS = [
['Midterm Study Guide', 'guide', 12, 0, 'guide'],
['Lecture Notes: Weeks 1 to 4', 'notes', 18, 1, 'notes'],
['Practice Exam 1', 'exam', 6, 0, 'exam'],
['Final Exam Review', 'guide', 0, 0, 'guide'],
['Homework 3 Solutions', 'worksheet', 4, 0, 'sheet'],
['Chapter 2 Summary', 'notes', 0, 1, 'notes'],
['Quiz 2 with Answers', 'exam', 2, 0, 'scan'],
['Key Terms Glossary', 'guide', 5, 1, 'guide'],
['Lecture Notes: Weeks 5 to 8', 'notes', 15, 0, 'notes'],
['Practice Exam 2', 'exam', 0, 0, 'exam'],
['Problem Set 4', 'worksheet', 3, 0, 'sheet']
];
const COURSE_INFO = {
'BIL 150': { subject: 'Biology', desc: 'An introduction to the fundamental principles of biology, including cell structure, genetics, evolution, and the flow of energy through living systems.', tags: ['cell biology', 'mitosis', 'evolution', 'genetics', 'cellular respiration', 'photosynthesis'] },
'CHM 113': { subject: 'Chemistry', desc: 'Hands-on laboratory work alongside general chemistry: measurement, titration, stoichiometry and safe lab practice.', tags: ['titration', 'stoichiometry', 'lab safety', 'measurement'] },
'PSY 110': { subject: 'Psychology', desc: 'A survey of the science of behavior and mental processes, from the brain to learning, memory and social behavior.', tags: ['learning', 'memory', 'research methods', 'conditioning'] },
'BIL 250': { subject: 'Biology', desc: 'How traits are inherited and expressed, from Mendel to molecular genetics.', tags: ['Mendelian genetics', 'DNA', 'gene expression'] },
'ECO 211': { subject: 'Economics', desc: null, tags: [] }
};
const catOfCode = code => { const p = (String(code).match(/^[A-Za-z]+/) || [''])[0].toUpperCase(); return UM_PREFIX_CAT[p] || (PFX[p] ? PFX[p][0] : 11); };
/* ===== 71d-links.js ===== */
/* ================= links between the areas ================= */
/* One token per page, and one document list per course, so search, Documents, Campus Hub, the course page,
shelves and the document page all point at the same documents. */
const tokSchool = sid => 'school-' + sid;
const tokCourse = (sid, code) => `course-${sid}.${slug(code)}`;
const tokDoc = id => 'doc-' + id;
const tokShelf = id => 'shelf-' + id;
function courseBy(sid, codeOrSlug) {
const s = BY_ID[sid]; if (!s) return null;
const k = slug(codeOrSlug);
return coursesOf(s).find(c => slug(c.code) === k) || null;
}
const HUB_LABEL = { guide: 'Study guide', exam: 'Exam', notes: 'Notes', worksheet: 'Worksheet' };
const HUB_TO_DH = { guide: 'Study guide', exam: 'Past exam', notes: 'Notes', worksheet: 'Practice' };
const THUMB_OF = { guide: 'guide', exam: 'exam', notes: 'notes', worksheet: 'sheet' };
const PAPER_OF = { guide: 'bullets', exam: 'mcq', notes: 'notes', worksheet: 'ws' };
function hubType(label) {
const l = String(label || '').toLowerCase();
if (/work|lab|practice set|problem/.test(l)) return 'worksheet';
if (/exam|quiz|test/.test(l)) return 'exam';
if (/note|lecture|slide/.test(l)) return 'notes';
return 'guide';
}
/* generated rows for courses without a hand-made list: varied, numbered, and they still group into an exam prep path */
const GEN_T = [
['guide', (c, n) => `${c.code} Midterm Study Guide`, 12],
['notes', (c, n) => `${c.title}: Lecture Notes, Weeks ${n * 4 - 3} to ${n * 4}`, 18, 1],
['exam', (c, n) => `${c.code} Practice Exam ${n}`, 6],
['guide', (c, n) => n === 1 ? 'Final Exam Review' : `Final Exam Review, Part ${n}`, 9],
['worksheet', (c, n) => `Homework ${n + 2} Solutions`, 4],
['notes', (c, n) => `Chapter ${n + 1} Summary`, 5, 1],
['exam', (c, n) => `Quiz ${n + 1} with Answers`, 2],
['guide', (c, n) => n === 1 ? `${c.title} Key Terms Glossary` : `Key Terms, Unit ${n}`, 5, 1],
['notes', (c, n) => `Exam ${Math.min(n + 1, 4)} Notes`, 15],
['worksheet', (c, n) => `Problem Set ${n + 3}`, 3],
['notes', (c, n) => `Week ${n + 2} Discussion Notes`, 7, 1],
['guide', (c, n) => `${c.code} Formula and Concept Sheet${n > 1 ? ' ' + n : ''}`, 2]
];
const CDOCS = {};
function registerDoc(r, s, c, i) {
const ex = r.id && DOCS[r.id];
if (ex) {
Object.assign(ex, { sid: s.id, hub: r.type || hubType(ex.type), thumb: r.thumb || THUMB_OF[r.type || hubType(ex.type)] });
if (!ex.pages && r.pages) ex.pages = r.pages;
return ex;
}
const h = hsh(s.id + c.code + r.t);
const id = r.id || `${s.id}-${slug(c.code)}-${i + 1}`;
if (DOCS[id]) return DOCS[id];
const up = 6 + h % 40, down = 1 + (h >>> 5) % 4;
return addDoc({ id, title: r.t, code: c.code, sid: s.id, type: HUB_LABEL[r.type], hub: r.type, pages: r.pages || 2 + h % 18, pro: !r.free, up, down, kind: PAPER_OF[r.type], thumb: r.thumb || THUMB_OF[r.type], views: 120 + h % 1800, gen: true });
}
function courseDocs(sid, code) {
const key = sid + '|' + code;
if (CDOCS[key]) return CDOCS[key];
const s = BY_ID[sid];
const c = coursesOf(s).find(x => x.code === code) || { code, title: courseMeta(code).title, docs: 6 };
const total = c.docs;
let rows;
if (key === 'um|BIL 150') rows = BIL_DOCS.map(r => ({ t: r[0], type: r[1], pages: r[2], free: !!r[3], thumb: r[4], id: r[5] }));
else {
rows = Object.values(DOCS).filter(d => !d.gen && d.code === code && (d.sid || 'um') === sid).map(d => ({ id: d.id, t: d.title, type: hubType(d.type), pages: d.pages, free: !d.pro }));
const n = Math.min(total, 36);
for (let i = 0; rows.length < n; i++) {
const t = GEN_T[i % GEN_T.length], round = Math.floor(i / GEN_T.length) + 1;
rows.push({ t: t[1](c, round), type: t[0], pages: round === 1 ? t[2] : 0, free: !!t[3] && round === 1 });
}
}
const list = rows.map((r, i) => registerDoc(r, s, c, i));
list.total = Math.max(total, list.length);
CDOCS[key] = list;
return list;
}
const docRate = d => Math.round(d.up / Math.max(1, d.up + d.down) * 100);
/* the Documents hub cards and search speak in their own document shape: give them the shared documents */
function hubDoc(d) {
const c = courseBy(d.sid || 'um', d.code) || { code: d.code, title: '', s: BY_ID[d.sid || 'um'] };
return { id: d.id, type: HUB_TO_DH[d.hub || hubType(d.type)], title: d.title, pages: d.pages, rate: docRate(d), votes: d.up + d.down, c, k: norm(d.title + ' ' + d.code) };
}
/* a document that only the Documents hub knew about (trending list): give it a page */
function ensureDoc(d) {
if (d.id && DOCS[d.id]) return DOCS[d.id];
const id = 'x-' + slug(d.title).slice(0, 36);
if (DOCS[id]) return DOCS[id];
const type = d.type === 'Past exam' ? 'exam' : d.type === 'Practice' ? 'worksheet' : d.type === 'Notes' ? 'notes' : 'guide';
const up = Math.max(1, Math.round((d.votes || 20) * (d.rate || 90) / 100));
return addDoc({ id, title: d.title, code: d.code, sid: d.sid || null, type: HUB_LABEL[type], hub: type, pages: d.pages || 6, pro: true, up, down: Math.max(1, (d.votes || 20) - up), kind: PAPER_OF[type], thumb: THUMB_OF[type], views: 400 + hsh(id) % 4000, gen: true });
}
/* Maya's own documents carry no school id and belong to Miami; a shared document from an unknown school has sid null */
const docSid = d => d.sid === undefined ? 'um' : d.sid;
const schoolOfDoc = d => docSid(d) ? BY_ID[docSid(d)] || null : null;
const courseOfDoc = d => d.code && docSid(d) ? courseBy(docSid(d), d.code) : null;
/* the Campus Hub's popular and fresh UM documents become real documents in their courses */
const CH_LABEL = { guide: 'Study guide', lab: 'Lab report', exam: 'Exam', notes: 'Notes', practice: 'Practice set', cheat: 'Cheat sheet' };
UM_POPULAR.concat(UM_FRESH).forEach(p => {
if (Object.values(DOCS).some(d => d.title === p.title)) return;
const h = hsh(p.title), up = 8 + h % 60;
addDoc({ id: slug(p.title).slice(0, 40), title: p.title, code: p.code, sid: 'um', type: CH_LABEL[p.type] || 'Study guide', pages: p.pages, pro: true, up, down: 1 + h % 5, views: p.views || 200 + h % 900, kind: PAPER_OF[hubType(CH_LABEL[p.type])] });
});
const docByTitle = t => Object.values(DOCS).find(d => d.title === t) || null;
/* ===== 72-shell.js ===== */
/* ================= personas and saved state ================= */
const PERSIST = ['persona', 'theme', 'motion', 'mine', 'loc', 'recents', 'recent', 'pins', 'sbOpen', 'navMin', 'treeOpen', 'tree', 'shelves', 'tasks', 'notes', 'starred', 'saved', 'viewed', 'votes', 'unlocked', 'unlocksUsed', 'onboarded', 'tips', 'shelfView', 'made', 'plan', 'courseNotes', 'remixOf'];
function personaState(p) {
const ret = p !== 'new';
return {
persona: ret ? 'returning' : 'new',
mine: ret ? 'um' : null, loc: 'miami', recents: ret ? ['um', 'fiu'] : [],
recent: ret ? [{ t: 'doc', id: 'stoich' }, { t: 'shelf', id: 'chm' }, { t: 'course', id: 'um|BIL 150' }] : [],
pins: ret ? [{ t: 'shelf', id: 'bil' }, { t: 'doc', id: 'cell-transport' }, { t: 'ai', id: 'cellbio' }] : [],
shelves: ret ? clone(SHELVES) : [clone(EXAMPLE_SHELF)],
tasks: ret ? clone({ bil: SHELF_TASKS.bil, chm: SHELF_TASKS.chm, fin: SHELF_TASKS.fin }) : { start: clone(SHELF_TASKS.start) },
notes: ret ? clone(DEFAULT_NOTES) : [],
starred: ret ? ['um|BIL 150', 'um|CHM 113', 'um|PSY 110'] : [],
saved: ret ? ['mitotic', 'cell-transport', 'module2', 'evolution', 'exam2-guide', 'stoich', 'limiting', 'titration', 'unit3-review', 'conditioning', 'research-quiz', 'memory-lec', 'stats-sheet', 'ethics', 'cell-cycle', 'core-bio'] : [],
viewed: ret ? ['cell-transport', 'module2'] : [],
votes: {}, unlocked: [], unlocksUsed: 2,
onboarded: ret, tips: true,
tree: { shelves: true, bil: true }, treeOpen: true, sbOpen: false, navMin: false,
shelfView: {}, made: ret ? clone(MADE_DEFAULT) : [], plan: 'free', courseNotes: [], remixOf: null
};
}
function loadState() {
const saved = store.get('state', null);
Object.assign(S, personaState(saved && saved.persona), { theme: 'auto', motion: true });
if (saved && typeof saved === 'object') PERSIST.forEach(k => { if (saved[k] !== undefined) S[k] = saved[k]; });
if (!LOCS[S.loc]) S.loc = 'miami';
if (S.mine && !BY_ID[S.mine]) S.mine = null;
if (!Array.isArray(S.shelves) || !S.shelves.length) { S.shelves = clone(SHELVES); S.tasks = clone({ bil: SHELF_TASKS.bil, chm: SHELF_TASKS.chm, fin: SHELF_TASKS.fin }); }
['recents', 'recent', 'pins', 'notes', 'starred', 'saved', 'viewed', 'unlocked', 'made', 'courseNotes'].forEach(k => { if (!Array.isArray(S[k])) S[k] = []; });
if (!S.tasks || typeof S.tasks !== 'object') S.tasks = {};
S.shelves.forEach(s => { if (!S.tasks[s.id]) S.tasks[s.id] = []; });
['tree', 'votes', 'shelfView'].forEach(k => { if (!S[k] || typeof S[k] !== 'object') S[k] = {}; });
}
let saveT = 0;
function save() { clearTimeout(saveT); saveT = setTimeout(() => { const o = {}; PERSIST.forEach(k => { o[k] = S[k]; }); store.set('state', o); }, 150); }
function setPersona(p) {
const keep = { theme: S.theme, motion: S.motion, loc: S.loc };
Object.keys(S).forEach(k => delete S[k]);
Object.assign(S, personaState(p), keep);
save();
}
/* ================= small lookups ================= */
const shelfById = id => S.shelves.find(s => s.id === id) || null;
const tasksOf = id => S.tasks[id] || (S.tasks[id] = []);
const openTasks = id => tasksOf(id).filter(t => t.st !== 'done').length;
const shelfColor = s => SHELF_COLORS[s.color] || s.color || SHELF_COLORS.teal;
const starKey = (sid, code) => sid + '|' + code;
const isStarred = (sid, code) => S.starred.includes(starKey(sid, code));
const plan = () => S.plan === 'pro' ? 'Pro' : 'Free';
const UNLOCKS = 5;
const unlocksLeft = () => Math.max(0, UNLOCKS - S.unlocksUsed);
const resetDay = () => { const d = new Date(TODAY); d.setMonth(d.getMonth() + 1, 1); return `${MON[d.getMonth()]} ${d.getDate()}`; };
function toggleIn(arr, v) { const i = arr.indexOf(v); if (i >= 0) arr.splice(i, 1); else arr.push(v); return i < 0; }
const dotEl = c => `<span class="sdot" style="background:${c}"></span>`;
const link = t => `href="#${t}"`;
/* everything the sidebar, search and panels can point at */
function refInfo(ref) {
if (!ref) return null;
if (ref.t === 'shelf') { const s = shelfById(ref.id); if (!s) return null; const n = openTasks(s.id); return { title: s.name, meta: `Shelf · ${plural(n, 'task')} open`, lead: `<span class="nav-dot" style="background:${shelfColor(s)}"></span>`, tok: tokShelf(s.id) }; }
if (ref.t === 'doc') { const d = DOCS[ref.id] || lazyDoc(ref.id); if (!d) return null; return { title: d.title, meta: `Document · ${d.code || 'Shared'}`, lead: icon('i-file'), tok: tokDoc(d.id) }; }
if (ref.t === 'course') { const [sid, code] = ref.id.split('|'); const c = courseBy(sid, code); if (!c) return null; return { title: `${c.code} · ${c.title}`, meta: `Course · ${BY_ID[sid].short}`, lead: catGlyph(catOfCourse(c), 28), tok: tokCourse(sid, c.code), raw: true }; }
if (ref.t === 'school') { const s = BY_ID[ref.id]; if (!s) return null; return { title: s.short, meta: S.mine === s.id ? 'Your school' : 'Campus Hub', lead: schoolMono(s, 28), tok: tokSchool(s.id), raw: true }; }
if (ref.t === 'ai') { const a = AI_COURSES.find(x => x.id === ref.id); if (!a) return null; return { title: a.name, meta: `AI course · ${a.pct}%`, lead: icon('i-cap'), tok: 'study-tools', bar: a.pct }; }
if (ref.t === 'game') { const g = studyItem(ref.id); if (!g) return null; return { title: g.name, meta: g.kind === 'tool' ? 'Study tool' : 'Study game', lead: icon(g.kind === 'tool' ? 'i-wrench' : 'i-gamepad'), tok: 'game-' + g.id }; }
return null;
}
function lazyDoc(id) {
if (DOCS[id]) return DOCS[id];
const m = String(id).match(/^([a-z]+)-([a-z0-9]+)-\d+$/);
if (m && BY_ID[m[1]]) { const c = courseBy(m[1], m[2]); if (c) courseDocs(m[1], c.code); }
if (!DOCS[id] && /^x-/.test(id)) TRENDING.forEach(ensureDoc);
return DOCS[id] || null;
}
function addRecentFor(r) {
let ref = null;
if (r.page === 'shelf') ref = { t: 'shelf', id: r.id };
else if (r.page === 'doc') ref = { t: 'doc', id: r.id };
else if (r.page === 'course') ref = { t: 'course', id: starKey(r.sid, r.code) };
else if (r.page === 'campus') ref = { t: 'school', id: r.id };
else if (r.page === 'study' && r.tab === 'detail') ref = { t: 'game', id: r.id };
if (!ref) return;
S.recent = [ref].concat(S.recent.filter(x => !(x.t === ref.t && x.id === ref.id))).slice(0, 8);
if (ref.t === 'school') S.recents = [ref.id].concat(S.recents.filter(x => x !== ref.id)).slice(0, 6);
if (ref.t === 'course') { const sid = r.sid; S.recents = [sid].concat(S.recents.filter(x => x !== sid)).slice(0, 6); }
}
/* shared visual atoms used on several pages */
function schoolMono(s, size) {
const p = schoolPal(s);
const t = monoText(s);
return `<span class="mono" data-len="${t.length}" style="--s:${size || 32}px;--m-deep:${p.deep};--m-bright:${p.bright === p.deep ? 'transparent' : p.bright};--m-on:${p.onDeep}" aria-hidden="true">${esc(t)}</span>`;
}
function monoText(s) {
const a = s.acr || '';
if (a && a.length <= 4 && /^[A-Za-z&.\-]+$/.test(a)) return a;
if (a && a.replace(/[^A-Z]/g, '').length >= 2 && a.replace(/[^A-Z]/g, '').length <= 4) return a.replace(/[^A-Z]/g, '');
const w = s.name.replace(/[()]/g, ' ').split(/[\s\-,–]+/).filter(x => x && !/^(of|the|at|and|for|in|de|la|du|del|&)$/i.test(x));
return w.slice(0, 3).map(x => x[0]).join('').toUpperCase();
}
const catOfCourse = c => c.cat || catOfCode(c.code);
/* ================= router ================= */
/* Tokens are plain words so a link to any view can be shared: #room, #shelf-bil, #documents, #school-um,
#course-um.bil150, #doc-mitotic, #study, #game-lyso, #preferences */
const PAGES = {};
let R = { page: 'room' }, TOKEN = 'room';
function parseToken(t) {
t = String(t || '').replace(/^#/, '');
let m;
if (t === '' || t === 'room') return { page: 'room' };
if ((m = t.match(/^shelf-([\w-]+)$/))) return shelfById(m[1]) ? { page: 'shelf', id: m[1] } : null;
if ((m = t.match(/^documents(?:-(atlas|radar))?$/))) return { page: 'docs', view: m[1] || 'search' };
if ((m = t.match(/^school-([a-z]+)$/))) return BY_ID[m[1]] ? { page: 'campus', id: m[1] } : null;
if ((m = t.match(/^course-([a-z]+)\.([a-z0-9]+)$/))) { const c = courseBy(m[1], m[2]); return c ? { page: 'course', sid: m[1], code: c.code } : null; }
if ((m = t.match(/^doc-([\w.-]+)$/))) return lazyDoc(m[1]) ? { page: 'doc', id: m[1] } : null;
if ((m = t.match(/^study(?:-(tools|mine|create))?$/))) return { page: 'study', tab: m[1] || 'games' };
if ((m = t.match(/^game-([\w-]+)$/))) return studyItem(m[1]) ? { page: 'study', tab: 'detail', id: m[1] } : null;
if (t === 'preferences') return { page: 'prefs' };
return null;
}
const shellOf = r => r.page === 'room' ? 'room' : r.page === 'shelf' ? 'shelf' : 'full';
const navOf = r => ({ room: 'room', shelf: 'room', docs: 'documents', campus: 'documents', course: 'documents', doc: 'documents', study: 'study' })[r.page] || '';
function go(token, opts) {
opts = opts || {};
const r = parseToken(token);
if (!r) { toast('That page isn’t part of this prototype.'); return; }
const prevShell = shellOf(R);
TOKEN = token; R = r;
// Entering My Room or a shelf collapses the main sidebar to its rail (shelves epic); other pages keep it as left.
if (shellOf(r) !== 'full' && prevShell === 'full') S.sbOpen = false;
closePop(); closeSheet(true); closeModal(true); closeDrawer(true);
addRecentFor(r);
try { history[opts.replace ? 'replaceState' : 'pushState'](null, '', '#' + token); } catch (e) { /* history unavailable */ }
render();
if (!opts.keepScroll) { const p = $('#page'); if (p) p.scrollTop = 0; }
if (opts.focus !== false) { const h = $('#page h1'); if (h) { h.setAttribute('tabindex', '-1'); h.focus({ preventScroll: true }); } }
save();
}
function onHistory() {
const t = (location.hash || '').slice(1) || 'room';
const r = parseToken(t); if (!r || t === TOKEN) return;
TOKEN = t; R = r; closePop(); closeSheet(true); closeModal(true); closeDrawer(true); render();
}
window.addEventListener('popstate', onHistory);
window.addEventListener('hashchange', onHistory);
function rerender(keepScroll) {
const p = $('#page'), y = p ? p.scrollTop : 0;
const a = document.activeElement, sig = a && a.id ? '#' + a.id : null;
render();
if (keepScroll !== false && p) $('#page').scrollTop = y;
if (sig) { const n = $(sig); if (n) n.focus({ preventScroll: true }); }
save();
}
/* ================= render ================= */
function render() {
const P = PAGES[R.page] || PAGES.room;
const shell = shellOf(R), app = $('#app');
app.dataset.shell = shell; app.dataset.page = R.page;
renderNav(shell);
renderPanel(shell);
renderTop();
if (R.page !== 'doc' && app.classList.contains('focus')) setFocusMode(false);
const page = $('#page');
page.style.cssText = '';
page.className = 'page' + (P.cls ? ' ' + P.cls(R) : '');
page.innerHTML = P.render(R);
if (P.mount) P.mount(R, page);
if (window.HOOT) HOOT.route(R);
layoutVars();
if (!S.onboarded && R.page === 'room' && !render.tourShown && window.TOUR) { render.tourShown = true; setTimeout(() => TOUR.start(), 350); }
}
function layoutVars() {
const nav = $('#nav'), panel = $('#rpanel');
const w = (nav && nav.offsetParent !== null ? nav.offsetWidth : 0) + (panel && !panel.hidden && panel.offsetParent !== null ? panel.offsetWidth : 0);
document.documentElement.style.setProperty('--main-left', w + 'px');
}
window.addEventListener('resize', () => { layoutVars(); if (!isPhone() && DRAWER_OPEN) closeDrawer(true); });
/* ================= main sidebar: full, or the Canva-style rail ================= */
function navMode(shell) { return shell === 'full' ? (S.navMin ? 'rail' : 'full') : (S.sbOpen ? 'full' : 'rail'); }
function navItems(active, rail) {
return [['room', 'i-home', 'My Room', 'Room'], ['documents', 'i-search', 'Documents', 'Docs'], ['study', 'i-gamepad', 'Study Room', 'Study']]
.map(([t, ic, l, sh]) => `<a class="nav-item" href="#${t}" ${t === active ? 'aria-current="page"' : ''} ${rail ? `data-tip="${l}"` : ''}>${icon(ic)}<span class="long">${l}</span><span class="short">${sh}</span></a>`).join('');
}
function navRow(ref, rail) {
const x = refInfo(ref); if (!x) return '';
const cur = TOKEN === x.tok;
return `<a class="nav-row" href="#${x.tok}" ${cur ? 'aria-current="page"' : ''} ${rail ? `data-tip="${esc(x.title)}"` : `title="${esc(x.title)}"`}><span class="ri${x.raw ? ' raw' : ''}">${x.lead}</span><span class="rt"><b>${esc(x.title)}</b><small>${esc(x.meta)}</small>${x.bar ? `<span class="s-bar"><i style="width:${x.bar}%"></i></span>` : ''}</span></a>`;
}
function pinnedRefs() {
const out = [];
if (S.mine) out.push({ t: 'school', id: S.mine });
S.pins.forEach(p => { if (refInfo(p)) out.push(p); });
return out;
}
function recentRefs(n) {
const pins = pinnedRefs();
return S.recent.filter(r => !pins.some(p => p.t === r.t && p.id === r.id) && refInfo(r)).slice(0, n || 4);
}
function navFoot(rail) {
const left = unlocksLeft(), pct = Math.round(S.unlocksUsed / UNLOCKS * 100);
return `<div class="nav-foot">
${S.plan === 'pro' ? '' : `<button class="plan" data-act="pro" type="button"><span class="pt"><span>Free plan</span><b>${S.unlocksUsed} of ${UNLOCKS} unlocks</b></span><span class="s-bar"><i style="width:${pct}%;background:var(--pin-btn)"></i></span><span class="pc">${icon('i-sparkle', 'sm')}Upgrade plan</span></button>
<button class="s-ib nav-up" data-act="pro" type="button" data-tip="Upgrade plan" aria-label="Upgrade plan">${icon('i-sparkle')}</button>`}
<button class="acct" data-act="account" type="button" ${rail ? 'data-tip="Your account"' : ''} aria-label="Your account"><span class="s-av">${PERSONA.initials}</span><span class="rt"><b>${esc(PERSONA.first)} <span class="s-pro" style="${S.plan === 'pro' ? '' : 'background:var(--card-2);color:var(--muted);border-color:var(--border)'}">${S.plan === 'pro' ? 'PRO' : 'FREE'}</span></b><small>${esc(PERSONA.email)}</small></span>${icon('i-up', 'sm')}</button>
</div>`;
}
function navInner(mode, active) {
const rail = mode === 'rail';
const pins = pinnedRefs(), rec = recentRefs(rail ? 2 : 4);
return `<div class="nav-brand"><a href="#room" class="nav-home" aria-label="StudyBoost, My Room">${MARK}</a><span class="nav-word">Study<i>Boost</i></span><button type="button" class="s-ib sm nav-x" data-act="nav-toggle" aria-label="Collapse sidebar" title="Collapse sidebar">${icon('i-panel')}</button></div>
<button type="button" class="nav-exp" data-act="nav-toggle" aria-label="Expand sidebar" data-tip="Expand sidebar">${icon('i-right', 'sm')}</button>
<div class="nav-scroll">
<nav class="nav-main" aria-label="Main">${navItems(active, rail)}</nav>
${pins.length ? `<div class="nav-grp"><div class="nav-h"><span class="s-k">Pinned</span></div>${pins.map(p => navRow(p, rail)).join('')}</div>` : ''}
${rec.length ? `<div class="nav-grp"><div class="nav-h"><span class="s-k">Recent</span></div>${rec.map(p => navRow(p, rail)).join('')}</div>` : ''}
</div>
${navFoot(rail)}`;
}
function renderNav(shell) {
const nav = $('#nav'), mode = navMode(shell);
nav.className = 'nav' + (mode === 'rail' ? ' rail' : '');
nav.innerHTML = navInner(mode, navOf(R));
}
/* ================= room panel and shelf panel ================= */
function renderPanel(shell) {
const p = $('#rpanel');
if (shell === 'full') { p.hidden = true; p.innerHTML = ''; return; }
p.hidden = false;
if (shell === 'room' && !S.treeOpen) { p.className = 'rpanel shut'; p.innerHTML = `<div class="rp-strip"><button type="button" class="rp-round" data-act="tree-toggle" aria-label="Open the room panel" title="Open the room panel">${icon('i-right')}</button><span>Your room</span></div>`; return; }
p.className = 'rpanel';
p.innerHTML = shell === 'room' ? roomPanelInner(true) : shelfPanelInner(shelfById(R.id), true);
}
const PANEL_Q = { q: '' };
function treeLeaf(lead, label, attrs, extra) { return `<li><div class="it">${'<span class="twsp"></span>'}${lead}${attrs.href ? `<a class="t" href="${attrs.href}">${esc(label)}</a>` : `<button type="button" class="t" ${attrs.act || ''}>${esc(label)}</button>`}${extra || ''}</div></li>`; }
function treeNode(key, lead, label, kids, count, href, open) {
const isOpen = open != null ? open : !!S.tree[key];
return `<li class="${isOpen ? 'open' : ''}"><div class="it"><button type="button" class="tw-b" data-act="tree" data-key="${key}" aria-expanded="${isOpen}" aria-label="${isOpen ? 'Collapse' : 'Expand'} ${esc(label)}">${icon('i-right', 'twi')}</button>${lead}${href ? `<a class="t" href="${href}">${esc(label)}</a>` : `<button type="button" class="t" data-act="tree" data-key="${key}">${esc(label)}</button>`}${count != null ? `<span class="c">${count}</span>` : ''}</div><ul>${kids.join('')}</ul></li>`;
}
function roomTree() {
const q = norm(PANEL_Q.q), hit = t => !q || norm(t).includes(q);
const shelves = S.shelves.map(s => {
const docs = s.docs.map(id => DOCS[id]).filter(Boolean);
const shown = q ? docs.filter(d => hit(d.title)) : docs.slice(0, 3);
if (q && !hit(s.name) && !shown.length) return '';
const kids = shown.map(d => treeLeaf(icon('i-file', 'ic'), d.title, { href: '#' + tokDoc(d.id) }));
if (!q && docs.length > 3) kids.push(treeLeaf(icon('i-more', 'ic'), `${docs.length - 3} more`, { href: '#' + tokShelf(s.id) }));
if (!docs.length) kids.push(`<li><span class="it it-empty"><span class="twsp"></span>No documents yet</span></li>`);
return treeNode(s.id, dotEl(shelfColor(s)), s.name, kids, `${openTasks(s.id)} open`, '#' + tokShelf(s.id), q ? true : null);
}).filter(Boolean);
const ai = AI_COURSES.filter(a => hit(a.name)).map(a => treeLeaf(icon('i-cap', 'ic'), a.name, { act: `data-act="ai-course" data-id="${a.id}"` }));
const notes = S.notes.filter(n => hit(n.text)).map(n => treeLeaf(icon('i-pin', 'ic'), n.text, { act: 'data-act="notes-sheet"' }));
const empty = t => `<li><span class="it it-empty"><span class="twsp"></span>${t}</span></li>`;
return treeNode('shelves', icon('i-shelf', 'ic'), 'Shelves', shelves.length ? shelves : [empty(q ? 'No shelf matches' : 'No shelves yet')], null, null, q ? true : null)
+ treeNode('ai', icon('i-sparkle', 'ic'), 'AI Courses', ai.length ? ai : [empty('Nothing here yet')], AI_COURSES.length, null, q && ai.length ? true : null)
+ treeNode('notes', icon('i-pin', 'ic'), 'Notes', notes.length ? notes : [empty(q ? 'No note matches' : 'Jot one on the pinboard')], S.notes.length, null, q && notes.length ? true : null);
}
function roomPanelInner(withCollapse) {
const n = S.shelves.length, lim = S.plan === 'pro' ? 20 : 3;
return `<div class="rp-in">
<div class="rp-ws"><span class="t">${esc(PERSONA.first)}’s room<small>${plan()} plan · ${n} of ${lim} shelves</small></span>${withCollapse ? `<button type="button" class="rp-round" data-act="tree-toggle" aria-label="Collapse the room panel" title="Collapse the room panel">${icon('i-left')}</button>` : ''}</div>
<label class="rp-search">${icon('i-search', 'sm')}<span class="sr">Search your room</span><input type="search" data-role="room-q" placeholder="Search your room" value="${esc(PANEL_Q.q)}" autocomplete="off"></label>
<button type="button" class="rp-new" data-act="new-shelf">${icon('i-plus', 'sm')}New shelf</button>
<div class="rp-sec"><div class="rp-sech"><span>Workspace</span></div><ul class="tree" data-role="room-tree">${roomTree()}</ul></div>
<div class="rp-sec"><button type="button" class="it" data-act="hoot-open">${icon('i-message', 'ic')}<span class="t">Ask Hoot about your room</span></button></div>
${S.plan === 'pro' ? '' : `<div class="rp-foot"><div class="meter"><span>Shelves on Free</span><b>${n} / 3</b></div><span class="s-bar"><i style="width:${Math.min(100, n / 3 * 100)}%;background:var(--pin-btn)"></i></span><button type="button" class="s-link" data-act="pro">Upgrade for 20 shelves</button></div>`}
</div>`;
}
document.addEventListener('input', e => {
if (e.target.dataset && e.target.dataset.role === 'room-q') { PANEL_Q.q = e.target.value; const t = e.target.closest('.rp-in').querySelector('[data-role="room-tree"]'); if (t) t.innerHTML = roomTree(); }
});
function weekCounts(id) {
const tasks = tasksOf(id).filter(t => t.st !== 'done');
const start = -((TODAY.getDay() + 6) % 7); // Monday of this week, relative to today
return Array.from({ length: 7 }, (_, i) => ({ d: start + i, n: tasks.filter(t => t.day === start + i).length, label: 'MTWTFSS'[i] }));
}
function shelfPanelInner(s, desktop) {
if (!s) return '';
const view = S.shelfView[s.id] || 'board';
const ts = tasksOf(s.id), cnt = st => ts.filter(t => t.st === st).length;
const docs = s.docs.map(id => DOCS[id]).filter(Boolean);
const wk = weekCounts(s.id), due = wk.reduce((n, d) => n + (d.d >= 0 ? d.n : 0), 0), today = ts.filter(t => t.st !== 'done' && t.day === 0).length;
const sec = (h, body, right) => `<div class="rp-sec"><div class="rp-sech"><span>${h}</span>${right || ''}</div>${body}</div>`;
const vi = [['board', 'i-kanban', 'Board'], ['table', 'i-table', 'Table'], ['list', 'i-list', 'List'], ['docs', 'i-file', 'Documents']];
return `<div class="rp-in">
<a class="backlink" href="#room">${icon('i-back', 'sm')}My Room</a>
<div class="shelfhead"><span class="sq" style="background:${shelfColor(s)}">${icon('i-shelf')}</span><span class="t"><b>${esc(s.name)}</b><small>${plural(docs.length, 'document')} · ${plural(ts.length, 'task')} · ${esc(s.tpl || 'Blank shelf')}</small></span></div>
${sec('Views', vi.map(([k, ic, l]) => `<button type="button" class="it${view === k ? ' on' : ''}" data-act="shelf-view" data-view="${k}">${icon(ic, 'ic')}<span class="t">${l}</span>${k === 'docs' ? `<span class="c">${docs.length}</span>` : ''}</button>`).join(''))}
${sec('Tasks', [['todo', 'To do', 'var(--faint)'], ['doing', 'Doing', 'var(--warn)'], ['done', 'Done', 'var(--success)']].map(([k, l, c]) => `<button type="button" class="it" data-act="shelf-view" data-view="board"><span class="sdot round" style="background:${c}"></span><span class="t">${l}</span><span class="c">${cnt(k)}</span></button>`).join('') + `<button type="button" class="it" data-act="task-add">${icon('i-plus', 'ic')}<span class="t">Add task</span></button>`, `<span>${openTasks(s.id)} open</span>`)}
${sec('Documents', docs.slice(0, 4).map(d => `<a class="it" href="#${tokDoc(d.id)}">${icon('i-file', 'ic')}<span class="t">${esc(d.title)}</span></a>`).join('') + (docs.length > 4 ? `<button type="button" class="it" data-act="shelf-view" data-view="docs">${icon('i-more', 'ic')}<span class="t">${docs.length - 4} more</span></button>` : '') + (docs.length ? '' : `<button type="button" class="it" data-act="add-docs">${icon('i-plus', 'ic')}<span class="t">Add documents</span></button>`))}
${sec('Study', `<button type="button" class="it" data-act="hoot-open" data-ctx="shelf">${icon('i-message', 'ic')}<span class="t">Ask Hoot on this shelf</span></button><button type="button" class="it" data-act="shelf-game">${icon('i-gamepad', 'ic')}<span class="t">Make a game from it</span></button><button type="button" class="it" data-act="ai-course-make">${icon('i-wand', 'ic')}<span class="t">Turn into an AI course</span></button>`)}
${s.exam ? `<div class="wid exam" data-wid><button type="button" class="widh" data-act="widget"><span>${icon('i-calendar')}Next deadline · this shelf</span>${icon('i-down', 'sm tw2')}</button>
<div class="widb"><div class="exrow"><div class="cd">${s.exam.days}<small>${s.exam.days === 1 ? 'day' : 'days'}</small></div><div><b>${esc(s.exam.title)}</b><div class="rp-sub">${dateTxt(s.exam.days)}${s.exam.time ? ' · ' + s.exam.time : ''}</div></div></div></div></div>` : ''}
<div class="wid" data-wid><button type="button" class="widh" data-act="widget"><span>${icon('i-clock')}This week · this shelf</span>${icon('i-down', 'sm tw2')}</button>
<div class="widb"><div class="week">${wk.map(d => `<div class="wday${d.d === 0 ? ' today' : ''}${d.n && d.d !== 0 ? ' due' : ''}"><i>${d.n || ''}</i>${d.label}</div>`).join('')}</div>
<div class="rp-sub"><b>${plural(due, 'task')} due</b> this week on this shelf · ${today} today</div></div></div>
<div class="rp-foot"><div class="meter"><span>Documents on this shelf</span><b>${docs.length} / 20</b></div><span class="s-bar"><i style="width:${docs.length * 5}%"></i></span></div>
</div>`;
}
/* ================= top bar ================= */
const SX = { seg: 'all', scoped: true, active: -1, items: [] };
const SX_SEGS = [['all', 'All'], ['schools', 'Schools'], ['courses', 'Courses'], ['docs', 'Documents'], ['study', 'Games and tools']];
function pageScope() {
if (!SX.scoped) return null;
if (R.page === 'campus') return { t: 'school', sid: R.id };
if (R.page === 'course') return { t: 'course', sid: R.sid, code: R.code };
if (R.page === 'doc') { const d = DOCS[R.id]; if (!d) return null; const s = schoolOfDoc(d), c = courseOfDoc(d); if (c) return { t: 'course', sid: s.id, code: c.code }; if (s) return { t: 'school', sid: s.id }; }
return null;
}
function scopeHtml() {
const sc = pageScope();
if (sc) {
const s = BY_ID[sc.sid];
const label = sc.t === 'school' ? s.short : sc.code;
return `<span class="sx-tok" title="Searching ${esc(sc.t === 'school' ? s.name : sc.code + ' at ' + s.short)}">${sc.t === 'school' ? schoolMono(s, 22) : catGlyph(catOfCode(sc.code), 22)}<span>${esc(label)}</span><button type="button" data-act="sx-unscope" aria-label="Search all of StudyBoost instead">${icon('i-x', 'xs')}</button></span>`;
}
const seg = SX_SEGS.find(x => x[0] === SX.seg);
return `<button type="button" class="sx-seg" data-act="sx-seg" aria-haspopup="menu" aria-expanded="false" aria-label="Search in: ${seg[1]}"><span>${seg[1]}</span>${icon('i-down', 'xs')}</button>`;
}
function sxPlaceholder() {
const sc = pageScope();
if (sc && sc.t === 'school') return `Search ${BY_ID[sc.sid].short} courses and documents`;
if (sc && sc.t === 'course') return `Search ${sc.code} documents`;
return { all: 'Search schools, courses, documents and games', schools: 'School name, nickname, acronym or email', courses: 'Course code or title, e.g. BIL 150', docs: 'Search 2M+ study guides and notes', study: 'Search games and study tools' }[SX.seg];
}
function renderTop() {
const left = unlocksLeft(), pct = Math.round(left / UNLOCKS * 100);
const q = $('#sx-q') ? $('#sx-q').value : '';
$('#top').innerHTML = `
<button type="button" class="s-ib menu-btn" data-act="drawer-open" aria-label="Open menu">${icon('i-menu')}</button>
<a class="top-brand" href="#room" aria-label="StudyBoost, My Room">${MARK}</a>
<form class="sx" id="sx" role="search" autocomplete="off">
<div class="sx-scope">${scopeHtml()}</div>
<label for="sx-q" class="sr">Search StudyBoost</label>
<input id="sx-q" type="text" placeholder="${esc(sxPlaceholder())}" value="${esc(q)}" role="combobox" aria-autocomplete="list" aria-expanded="false" aria-controls="sx-panel" spellcheck="false">
<button type="button" class="sx-x" data-act="sx-clear" aria-label="Clear search" ${q ? '' : 'hidden'}>${icon('i-x', 'sm')}</button>
<button type="submit" class="sx-go" aria-label="Search">${icon('i-search')}</button>
<div class="sx-panel" id="sx-panel" role="listbox" aria-label="Suggestions" hidden></div>
</form>
<div class="top-acts">
<button type="button" class="new-btn" data-act="new-menu" aria-haspopup="menu" aria-expanded="false">${icon('i-plus')}<span class="nb-t">New</span></button>
${S.plan === 'pro' ? '' : `<button type="button" class="unlock" data-act="unlocks" aria-haspopup="dialog" aria-expanded="false" aria-label="${left} free unlocks left this month"><span class="s-ring" style="--p:${pct}"></span><span class="ul-t"><b>${left}</b> free ${left === 1 ? 'unlock' : 'unlocks'} left</span></button>`}
<button type="button" class="s-ib theme-btn" data-act="theme-menu" aria-haspopup="dialog" aria-expanded="false" aria-label="Theme" title="Theme">${icon('i-palette')}</button>
<button type="button" class="s-ib" data-act="notifs" aria-haspopup="dialog" aria-expanded="false" aria-label="Notifications" title="Notifications">${icon('i-bell')}${NOTIFS.some(n => n.unread) ? '<span class="badge-dot"></span>' : ''}</button>
<button type="button" class="s-av" data-act="account" aria-haspopup="menu" aria-expanded="false" aria-label="Your account">${PERSONA.initials}</button>
</div>`;
wireSearch();
}
/* ---------- global smart search (the Documents hub engine, now in the top bar) ---------- */
function sxBuild(raw) {
const items = [], q = norm(raw), sc = pageScope(), seg = sc ? 'all' : SX.seg;
const opt = (it, inner) => { it.i = items.length; items.push(it); return `<div class="opt" role="option" id="sx-o${it.i}" data-i="${it.i}" aria-selected="false">${inner}</div>`; };
const group = (label, inner) => inner ? `<div class="og" role="group" aria-label="${esc(label)}"><div class="og-h" aria-hidden="true">${esc(label)}</div>${inner}</div>` : '';
const sOpt = (s, why, cls, hq) => opt({ run: () => go(tokSchool(s.id)) }, `${schoolMono(s, 32)}<span class="ot"><b>${hq ? hl(s.name, hq) : esc(s.name)}</b><span>${esc(placeTxt(s))}${distTxt(s) ? ' · ' + distTxt(s) : ''}</span></span>${why ? `<span class="why${cls ? ' ' + cls : ''}">${esc(why)}</span>` : ''}`);
const cOpt = (c, hq, why) => opt({ run: () => go(tokCourse(c.s.id, c.code)) }, `${catGlyph(catOfCourse(c), 32)}<span class="ot"><b>${hq ? hl(c.code + ' · ' + c.title, hq) : esc(c.code + ' · ' + c.title)}</b><span>${esc(c.s.short)} · ${plural(c.docs, 'document')}</span></span>${why ? `<span class="why${why === 'Your course' ? ' mine' : ' near'}">${why}</span>` : ''}`);
const dOpt = (d, hq) => opt({ run: () => go(tokDoc(d.id)) }, `${miniThumb(d.hub || hubType(d.type))}<span class="ot"><b>${hq ? hl(d.title, hq) : esc(d.title)}</b><span>${esc(d.type)} · ${esc(d.code || '')}${schoolOfDoc(d) ? ' · ' + esc(schoolOfDoc(d).short) : ''}</span></span>${S.saved.includes(d.id) ? '<span class="why mine">Saved</span>' : ''}`);
const toks = q.split(' ').filter(Boolean);
const docHit = d => toks.every(t => norm(d.title + ' ' + d.code + ' ' + d.type).includes(t));
let html = '';
if (sc) {
const s = BY_ID[sc.sid];
if (sc.t === 'school') {
const cs = coursesOf(s).filter(c => !q || norm(c.code + ' ' + c.title).includes(q) || key(c.code).startsWith(q.replace(/\s/g, '')));
html += group(q ? `Courses at ${s.short}` : `Popular at ${s.short}`, cs.slice().sort((a, b) => b.docs - a.docs).slice(0, 6).map(c => cOpt(c, q, isStarred(s.id, c.code) ? 'Your course' : '')).join(''));
if (q.length >= 2) {
const ds = coursesOf(s).flatMap(c => courseDocs(s.id, c.code).slice(0, 12)).filter(docHit).slice(0, 4);
html += group(`Documents at ${s.short}`, ds.map(d => dOpt(d, q)).join(''));
}
} else {
const ds = courseDocs(s.id, sc.code).filter(d => !q || docHit(d)).slice(0, 8);
html += group(q ? `Documents in ${sc.code}` : `In ${sc.code}`, ds.map(d => dOpt(d, q)).join(''));
}
if (!items.length) html += `<p class="og-empty">Nothing in ${esc(sc.t === 'school' ? s.short : sc.code)} matches “${esc(raw.trim())}”.</p>`;
html += `<div class="og og-foot">${opt({ run: () => { SX.scoped = false; renderTop(); const i = $('#sx-q'); i.value = raw; i.focus(); sxOpen(); } }, `<span class="oi">${icon('i-globe')}</span><span class="ot"><b>${raw.trim() ? `Search all of StudyBoost for “${esc(raw.trim())}”` : 'Search all of StudyBoost instead'}</b></span>`)}</div>`;
return { html, items };
}
if (!q) {
const ids = []; if (S.mine) ids.push(S.mine); S.recents.forEach(id => { if (!ids.includes(id) && BY_ID[id]) ids.push(id); });
if (seg === 'all' || seg === 'schools') html += group('Jump back in', ids.slice(0, 3).map(id => sOpt(BY_ID[id], id === S.mine ? 'Your school' : 'Viewed', id === S.mine ? 'mine' : '')).join(''));
if (seg === 'all' || seg === 'courses') html += group('Your courses', S.starred.slice(0, 3).map(k => { const [sid, code] = k.split('|'); const c = courseBy(sid, code); return c ? cOpt(c, '', 'Your course') : ''; }).join(''));
if (seg === 'all' || seg === 'schools') html += group('Closest to ' + LOC().label, nearList().filter(s => !ids.includes(s.id)).slice(0, 3).map(s => sOpt(s, 'Nearby', 'near')).join(''));
if (seg === 'study') html += group('Popular in the Study Room', studyCatalog().slice(0, 5).map(g => opt({ run: () => go('game-' + g.id) }, `<span class="oi">${icon(g.kind === 'tool' ? 'i-wrench' : 'i-gamepad')}</span><span class="ot"><b>${esc(g.name)}</b><span>${esc(g.by ? 'By ' + g.byName + ' · ' + g.course : g.cat)}</span></span>`)).join(''));
if (seg === 'all' || seg === 'docs') html += group('Try searching', EXAMPLES.map(ex => opt({ keep: true, run: () => { $('#sx-q').value = ex.q; } }, `<span class="oi">${icon(ex.icon)}</span><span class="ot"><b>${esc(ex.q)}</b><span>${esc(ex.note)}</span></span>`)).join(''));
return { html, items };
}
const Rz = search(raw);
if (Rz.dom !== null) {
html += Rz.email.length ? group('Matched your school email', Rz.email.map(x => sOpt(x.s, '@' + x.m, 'near')).join('')) : `<p class="og-empty">${Rz.dom.includes('.') ? `No school on StudyBoost uses <b>${esc(Rz.dom)}</b> yet.` : 'Keep typing your school email…'}</p>`;
if (Rz.email.length > 1) html += `<p class="og-note">${Rz.email.length} campuses share this email domain. Pick yours.</p>`;
} else {
if (seg === 'all' || seg === 'schools') {
html += group('Schools', Rz.schools.slice(0, seg === 'schools' ? 6 : 4).map(x => sOpt(x.s, x.r.why || (dist(x.s) <= nearMax() ? 'Nearby' : ''), x.r.why ? '' : 'near', Rz.q)).join(''));
const same = Rz.schools.filter(x => x.r.why === 'Acronym');
if (same.length > 1) html += `<p class="og-note">${same.length} schools go by ${esc(same[0].s.acr)}. The one closest to you is first.</p>`;
}
if (seg === 'all' || seg === 'courses') {
let cs = Rz.courses;
if (!cs.length && q.length >= 3) cs = ALL_COURSES.filter(c => norm(c.title).includes(q)).sort((a, b) => (dist(a.s) <= nearMax() ? 0 : 1) - (dist(b.s) <= nearMax() ? 0 : 1) || b.docs - a.docs).slice(0, 4);
html += group(`Courses${Rz.code ? ' matching ' + Rz.code : ''}`, cs.slice(0, seg === 'courses' ? 8 : 4).map(c => cOpt(c, '', isStarred(c.s.id, c.code) ? 'Your course' : dist(c.s) <= nearMax() ? 'Nearby' : '')).join(''));
}
if ((seg === 'all' || seg === 'docs') && q.length >= 3) {
const mine = Object.values(DOCS).filter(d => !d.gen && docHit(d));
const more = docIndex().filter(d => toks.every(tk => d.k.includes(tk)) && !mine.some(m => m.id === d.id)).slice(0, 6).map(d => d.id ? DOCS[d.id] : ensureDoc(d)).filter(Boolean);
html += group('Documents', mine.concat(more).slice(0, seg === 'docs' ? 8 : 4).map(d => dOpt(d, q)).join(''));
}
if ((seg === 'all' || seg === 'study') && q.length >= 3) {
const gs = studyCatalog().filter(g => norm(g.name + ' ' + (g.wish || '') + ' ' + (g.course || '')).includes(q)).slice(0, 3);
html += group('Study Room', gs.map(g => opt({ run: () => go('game-' + g.id) }, `<span class="oi">${icon(g.kind === 'tool' ? 'i-wrench' : 'i-gamepad')}</span><span class="ot"><b>${hl(g.name, q)}</b><span>${esc(g.byName ? 'By ' + g.byName + ' · ' + g.course : g.cat)}</span></span>`)).join(''));
const sh = S.shelves.filter(s => norm(s.name).includes(q)).slice(0, 2);
html += group('In your room', sh.map(s => opt({ run: () => go(tokShelf(s.id)) }, `<span class="oi">${dotEl(shelfColor(s))}</span><span class="ot"><b>${hl(s.name, q)}</b><span>Shelf · ${plural(openTasks(s.id), 'task')} open</span></span>`)).join(''));
}
if (!items.length) html += `<p class="og-empty">Nothing matches “${esc(raw.trim())}” yet.</p>`;
}
let foot = '';
if (Rz.dom === null) foot += opt({ run: () => toast(`Prototype: this would search all 2M+ study guides for “${raw.trim()}”.`) }, `<span class="oi">${icon('i-search')}</span><span class="ot"><b>Search all documents for “${esc(raw.trim())}”</b></span>`);
foot += opt({ run: () => openRequest(Rz.dom !== null ? '' : raw.trim()) }, `<span class="oi">${icon('i-plus')}</span><span class="ot"><b>Can’t find your school? Request it</b><span>We’ll add it and let you know.</span></span>`);
return { html: html + `<div class="og og-foot">${foot}</div>`, items };
}
function sxPaint(scroll) {
const panel = $('#sx-panel'), input = $('#sx-q'); if (!panel) return;
$$('.opt', panel).forEach(el => el.setAttribute('aria-selected', +el.dataset.i === SX.active ? 'true' : 'false'));
const el = SX.active >= 0 ? $('#sx-o' + SX.active) : null;
if (el) { input.setAttribute('aria-activedescendant', el.id); if (scroll) el.scrollIntoView({ block: 'nearest' }); } else input.removeAttribute('aria-activedescendant');
}
function sxRender() {
const panel = $('#sx-panel'), input = $('#sx-q'); if (!panel) return;
const r = sxBuild(input.value); SX.items = r.items; panel.innerHTML = r.html;
SX.active = input.value.trim() && SX.items.length ? 0 : -1; sxPaint(false);
$('.sx-x').hidden = !input.value;
}
function sxOpen() { const p = $('#sx-panel'); if (!p) return; sxRender(); p.hidden = false; $('#sx-q').setAttribute('aria-expanded', 'true'); }
function sxClose() { const p = $('#sx-panel'); if (!p || p.hidden) return; p.hidden = true; const i = $('#sx-q'); i.setAttribute('aria-expanded', 'false'); i.removeAttribute('aria-activedescendant'); }
function sxChoose(it) { if (!it) return; if (it.keep) { it.run(); $('#sx-q').focus(); sxRender(); return; } sxClose(); const i = $('#sx-q'); i.blur(); it.run(); }
function wireSearch() {
const input = $('#sx-q'), panel = $('#sx-panel'), form = $('#sx');
input.addEventListener('focus', sxOpen);
input.addEventListener('input', () => { if (panel.hidden) sxOpen(); else sxRender(); });
input.addEventListener('keydown', e => {
if (e.key === 'ArrowDown') { e.preventDefault(); if (panel.hidden) { sxOpen(); return; } SX.active = Math.min(SX.items.length - 1, SX.active + 1); sxPaint(true); }
else if (e.key === 'ArrowUp') { e.preventDefault(); SX.active = Math.max(0, SX.active - 1); sxPaint(true); }
else if (e.key === 'Escape' && !panel.hidden) { e.preventDefault(); e.stopPropagation(); sxClose(); }
});
form.addEventListener('submit', e => { e.preventDefault(); if (panel.hidden) { sxOpen(); return; } if (input.value.trim()) sxChoose(SX.items[SX.active >= 0 ? SX.active : 0]); else input.focus(); });
panel.addEventListener('mousedown', e => e.preventDefault());
panel.addEventListener('click', e => { const o = e.target.closest('.opt'); if (o) sxChoose(SX.items[+o.dataset.i]); });
panel.addEventListener('mousemove', e => { const o = e.target.closest('.opt'); if (o && +o.dataset.i !== SX.active) { SX.active = +o.dataset.i; sxPaint(false); } });
input.addEventListener('blur', () => setTimeout(() => { if (document.activeElement !== input) sxClose(); }, 150));
}
document.addEventListener('keydown', e => {
if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k') { e.preventDefault(); const i = $('#sx-q'); if (i) i.focus(); }
else if (e.key === '/' && !e.target.closest('input, textarea, select, [contenteditable]')) { const i = $('#sx-q'); if (i) { e.preventDefault(); i.focus(); } }
});
/* ---------- top bar menus ---------- */
function newMenu(anchor) {
const at = S.plan !== 'pro';
const it = (act, ic, t, m, extra, tone) => `<button type="button" class="mi" role="menuitem" data-act="${act}" ${extra || ''}><span class="mi-ic${tone ? ' ' + tone : ''}">${icon(ic)}</span><span class="mt">${t}<small>${m}</small></span></button>`;
openPop(anchor, `<div class="menu" role="menu" aria-label="New">
${it('upload', 'i-upload', 'Upload a document', 'Notes, guides, past exams')}
${it('new-shelf', 'i-shelf', 'New shelf', 'From a template, with example tasks')}
${it('hoot-open', 'i-message', 'Ask Hoot', 'Questions about any document')}
${it('go', 'i-gamepad', 'Make a game or tool', 'Describe it in one sentence', 'data-to="study-create"')}
${it('new-note', 'i-pin', 'New note', 'Pinned to your room')}
<hr>
<button type="button" class="mi" role="menuitem" data-act="pro" data-feature="AI Courses"><span class="mi-ic gold">${icon('i-cap')}</span><span class="mt">AI Course ${at ? `<span class="s-pro" style="margin-left:4px">${icon('i-lock', 'xs')} PRO</span>` : ''}<small>A full course from your syllabus</small></span></button>
</div>`, { cls: 'new-pop', role: 'menu', label: 'New' });
}
function unlocksPop(anchor) {
const left = unlocksLeft(), used = S.unlocksUsed;
openPop(anchor, `<div class="pro">
<div class="pop-h" style="padding:0 0 10px"><h3>Your free month</h3><span class="s-k">Resets ${resetDay()}</span></div>
<div class="pro-meter"><span class="s-ring" style="--p:${Math.round(left / UNLOCKS * 100)}"></span><span><b>${left} of ${UNLOCKS} unlocks left</b><small>Document unlocks · ${used} used this month</small></span></div>
<ul class="pro-list"><li>${icon('i-check')}Unlimited document unlocks</li><li>${icon('i-check')}Unlimited Ask Hoot and AI Courses</li><li>${icon('i-check')}Study games without ads, and every premium theme</li></ul>
<div style="display:flex;gap:8px"><button type="button" class="s-btn primary" data-act="pro" style="flex:1">${icon('i-sparkle', 'sm')}See Pro plans</button><button type="button" class="s-btn quiet" data-act="pop-close">Not now</button></div>
</div>`, { label: 'Free unlocks', focus: '.s-btn' });
}
function proPop(anchor, feature) {
const p = S.proPlan || 'year';
const html = `<div class="pro">
<div class="pop-h" style="padding:0 0 8px"><h3>${feature ? esc(feature) + ' are part of Pro' : 'StudyBoost Pro'}</h3><button type="button" class="s-ib sm" data-act="pop-close" aria-label="Close">${icon('i-x')}</button></div>
<p style="font-size:13.5px;color:var(--muted)">${feature === 'AI Courses' ? 'Upload a syllabus and get lessons, practice quizzes and a study plan for the whole term. Try Pro free for 7 days.' : 'Every premium document, unlimited Ask Hoot, AI Courses and the premium themes.'}</p>
<div class="plans" role="radiogroup" aria-label="Billing" style="margin-top:12px">
<button type="button" role="radio" aria-checked="${p === 'month'}" aria-pressed="${p === 'month'}" data-act="pro-plan" data-v="month"><b>Monthly</b><strong>$9.99</strong><span> / month</span></button>
<button type="button" role="radio" aria-checked="${p === 'year'}" aria-pressed="${p === 'year'}" data-act="pro-plan" data-v="year"><b>Yearly <span class="s-pro">SAVE 40%</span></b><strong>$5.99</strong><span> / month</span></button>
</div>
<ul class="pro-list"><li>${icon('i-check')}Unlimited document unlocks</li><li>${icon('i-check')}AI Courses, flashcards and quizzes</li><li>${icon('i-check')}1,500 AI credits a month</li><li>${icon('i-check')}Matcha, Cloud Nine, Lo-fi Midnight, Citrus Rush and Arcade</li></ul>
<button type="button" class="s-btn primary wide lg" data-act="pro-go">${icon('i-sparkle', 'sm')}Try Pro free for 7 days</button>
<p style="font-size:12px;color:var(--faint);margin-top:8px;text-align:center">Prototype: no payment is taken. Trying Pro switches this demo to a Pro account.</p>
</div>`;
if (anchor && document.contains(anchor) && !isPhone()) openPop(anchor, html, { label: 'StudyBoost Pro', force: true, focus: '[aria-checked="true"]' });
else openModal(html.replace('class="pro"', 'class="pro" style="width:auto"'), { label: 'StudyBoost Pro', cls: 'mcard-pro' });
}
function themeSwatch(t) {
if (!t.sw) return `<span class="tp-sw tp-auto" aria-hidden="true"><i style="background:#ffffff"></i><i style="background:#0e171a"></i></span>`;
return `<span class="tp-sw" aria-hidden="true"><i style="background:${t.sw[1]}"></i><span class="m" style="background:${t.sw[0]}"><b style="background:${t.sw[2]}"></b><s style="background:${t.sw[3]}"></s></span></span>`;
}
function themeGrid() {
return `<div class="tp-grid" role="radiogroup" aria-label="Theme">${THEMES.map(t => `<button type="button" class="tp-opt" role="radio" aria-checked="${S.theme === t.id}" data-act="theme" data-v="${t.id}">${themeSwatch(t)}<span class="tp-name"><span>${esc(t.name)}<small>${esc(t.desc)}</small></span>${t.group === 'Pro' && S.plan !== 'pro' ? '<span class="s-pro">PRO</span>' : ''}</span></button>`).join('')}</div>`;
}
function themePop(anchor) {
openPop(anchor, `<div class="tp"><div class="pop-h"><h3>Theme</h3><span class="s-k">${S.plan === 'pro' ? 'Pro' : 'Pro themes: free to preview'}</span></div>${themeGrid()}
<div class="tp-foot"><button type="button" role="switch" aria-checked="${S.motion}" data-act="motion"><span class="s-switch"><i></i></span>Ambient motion</button><span>Cloud Nine and Lo-fi Midnight</span></div></div>`, { label: 'Theme', focus: '[aria-checked="true"]' });
}
function notifPop(anchor) {
const tone = t => ({ tb: 'background:var(--tb-bg);color:var(--tb-ink)', tc: 'background:var(--tc-bg);color:var(--tc-ink)', ta: 'background:var(--ta-bg);color:var(--ta-ink)', pin: 'background:var(--pin-bg);color:var(--pin-ink)' })[t];
openPop(anchor, `<div class="nt"><div class="pop-h"><h3>Notifications</h3><button type="button" class="s-link" data-act="notifs-read" style="font-size:13px">Mark all read</button></div>
${NOTIFS.map((n, i) => `<a class="nt-row${n.unread ? ' unread' : ''}" href="#${n.go}" data-act="notif" data-i="${i}"><span class="ni" style="${tone(n.tone)}">${icon(n.ic)}</span><span class="nx">${n.t}<small>${n.s}</small></span></a>`).join('')}</div>`, { label: 'Notifications' });
}
function accountMenu(anchor) {
openPop(anchor, `<div class="menu" role="menu" aria-label="Your account">
<div class="acct-h"><span class="s-av">${PERSONA.initials}</span><span><b>${esc(PERSONA.full)}</b><small>${plan()} plan${S.plan === 'pro' ? '' : ` · ${unlocksLeft()} unlocks left`}</small></span></div>
${S.plan === 'pro' ? '' : `<button type="button" class="mi" role="menuitem" data-act="pro"><span class="mi-ic gold">${icon('i-sparkle')}</span><span class="mt">Unlock Pro features</span></button>`}
<button type="button" class="mi" role="menuitem" data-act="toast" data-msg="Prototype: your profile would open here.">${icon('i-user')}<span class="mt">Profile</span></button>
<a class="mi" role="menuitem" href="#preferences">${icon('i-gear')}<span class="mt">Preferences</span></a>
<button type="button" class="mi" role="menuitem" data-act="tour">${icon('i-help')}<span class="mt">Help and tutorials<small>Replay the welcome tour</small></span></button>
<hr>
<button type="button" class="mi" role="menuitem" data-act="toast" data-msg="Prototype: signing out is turned off.">${icon('i-out')}<span class="mt">Sign out</span></button>
</div>`, { cls: 'acct-pop', role: 'menu', label: 'Your account' });
}
function segMenu(anchor) {
openPop(anchor, `<div class="menu" role="menu" aria-label="Search in">${SX_SEGS.map(([k, l]) => `<button type="button" class="mi" role="menuitemradio" aria-checked="${SX.seg === k}" data-act="sx-seg-pick" data-v="${k}">${SX.seg === k ? icon('i-check') : '<span class="i"></span>'}<span class="mt">${l}</span></button>`).join('')}</div>`, { align: 'start', role: 'menu', label: 'Search in' });
}
/* ================= request your school (Documents hub) ================= */
function openRequest(name) {
const card = openModal(`<div class="mcard-h"><div><h2>Request your school</h2><p>We add new schools every week. Tell us where you study and we’ll let you know when it’s live.</p></div><button type="button" class="s-ib" data-act="modal-close" aria-label="Close">${icon('i-x')}</button></div>
<form class="mcard-b" id="req-form">
<label class="fld"><span>School name</span><input class="s-input" id="req-school" required value="${esc(name || '')}"></label>
<label class="fld"><span>City and country</span><input class="s-input" id="req-city" required value="${esc(LOC().full)}"></label>
<label class="fld"><span>Email for updates (optional)</span><input class="s-input" id="req-mail" type="email" placeholder="you@school.edu"></label>
<button type="submit" class="s-btn primary lg">Send request</button>
</form>`, { label: 'Request your school', focus: name ? '#req-city' : '#req-school' });
$('#req-form', card).addEventListener('submit', e => {
e.preventDefault(); const n = $('#req-school').value.trim();
$('#req-form', card).outerHTML = `<div class="mcard-b"><p class="ok-msg">${icon('i-check')}<span>Request sent. We’ll let you know when ${esc(n)} is live.</span></p><button type="button" class="s-btn" data-act="modal-close">Close</button></div>`;
});
}
/* ================= phone drawer: Menu, plus the Room or Shelf panel ================= */
let DRAWER_OPEN = false, DRAWER_TAB = 'menu';
function openDrawer(tab) {
DRAWER_TAB = tab || 'menu'; DRAWER_OPEN = true; closePop();
let w = $('#mdrawer');
if (!w) { w = document.createElement('div'); w.id = 'mdrawer'; $('#ovl').appendChild(w); }
paintDrawer();
const f = $('.md-tabs [aria-selected="true"]', w) || $('.mdrawer button', w); if (f) f.focus();
}
function paintDrawer() {
const w = $('#mdrawer'); if (!w) return;
const shell = shellOf(R), second = shell === 'shelf' ? 'Shelf' : shell === 'room' ? 'Room' : null;
if (!second) DRAWER_TAB = 'menu';
const pins = pinnedRefs(), rec = recentRefs(4);
const menu = `<nav class="nav-main" aria-label="Main">${navItems(navOf(R), false)}</nav>
${pins.length ? `<div class="nav-grp"><div class="nav-h"><span class="s-k">Pinned</span></div>${pins.map(p => navRow(p, false)).join('')}</div>` : ''}
${rec.length ? `<div class="nav-grp"><div class="nav-h"><span class="s-k">Recent</span></div>${rec.map(p => navRow(p, false)).join('')}</div>` : ''}`;
const body = DRAWER_TAB === 'menu' ? menu : shell === 'shelf' ? shelfPanelInner(shelfById(R.id), false) : roomPanelInner(false);
w.innerHTML = `<div class="mdrawer-scrim" data-act="drawer-close"></div><aside class="mdrawer" role="dialog" aria-modal="true" aria-label="Navigation">
<div class="nav-brand">${MARK}<span class="nav-word">Study<i>Boost</i></span><button type="button" class="s-ib sm nav-x" data-act="drawer-close" aria-label="Close menu">${icon('i-x')}</button></div>
${second ? `<div class="md-tabs" role="tablist"><button type="button" role="tab" aria-selected="${DRAWER_TAB === 'menu'}" data-act="drawer-tab" data-k="menu">Menu</button><button type="button" role="tab" aria-selected="${DRAWER_TAB === 'panel'}" data-act="drawer-tab" data-k="panel">${second}</button></div>` : ''}
<div class="md-body${DRAWER_TAB === 'panel' ? ' rpanel-in' : ''}">${body}</div>
${DRAWER_TAB === 'menu' ? navFoot(false) : ''}</aside>`;
}
function closeDrawer(silent) { const w = $('#mdrawer'); if (w) w.remove(); const was = DRAWER_OPEN; DRAWER_OPEN = false; if (was && !silent) { const b = $('.menu-btn'); if (b) b.focus(); } }
/* ================= prototype pill ================= */
function protoPop(anchor) {
const areas = [['room', 'My Room'], ['shelf-' + (S.shelves[0] || {}).id, 'A shelf'], ['documents', 'Documents'], ['school-um', 'Campus Hub'], ['course-um.bil150', 'Course page'], ['doc-mitotic', 'Document page'], ['study', 'Study Room'], ['preferences', 'Preferences']];
openPop(anchor, `<div class="pp">
<div><h3 style="font:750 16px/1.2 var(--display)">StudyBoost combined prototype</h3><p style="margin-top:4px">The best of each prototype, joined into one site. Sample data; nothing is saved to a server.</p></div>
<div class="pp-row"><span class="s-k">View as</span><div class="pp-seg" role="group" aria-label="View as">
<button type="button" aria-pressed="${S.persona === 'returning'}" data-act="persona" data-v="returning">Returning student</button>
<button type="button" aria-pressed="${S.persona === 'new'}" data-act="persona" data-v="new">First visit</button></div></div>
<label class="pp-row"><span class="s-k">Location</span><select data-act-change="loc">${Object.keys(LOCS).map(k => `<option value="${k}" ${S.loc === k ? 'selected' : ''}>Near ${esc(LOCS[k].full)}</option>`).join('')}</select></label>
<div class="pp-row"><span class="s-k">Jump to</span><div class="pp-links">${areas.map(([t, l]) => `<a class="s-chip" href="#${t}">${l}</a>`).join('')}</div></div>
<div class="pp-links"><button type="button" class="s-btn sm" data-act="tour">${icon('i-replay', 'sm')}Replay the welcome tour</button><button type="button" class="s-btn sm quiet" data-act="reset-demo">${icon('i-refresh', 'sm')}Reset demo</button></div>
${BROCHURE_URL ? `<a class="s-link" href="${BROCHURE_URL}" target="_blank" rel="noopener" style="font-size:13px">Open the brochure of the separate prototypes ${icon('i-external', 'xs')}</a>` : ''}
</div>`, { side: 'above', align: 'start', label: 'Prototype controls' });
}
const BROCHURE_URL = 'https://claude.ai/artifact/2vEg4Fk7MpVEN3MyR1tSSD';
/* ================= actions ================= */
const ACT = {};
const RESETS = [];
/* forms submit through one handler too: <form data-form="name"> runs FORMS[name](form, event) */
const FORMS = {};
document.addEventListener('submit', e => {
const f = e.target.closest && e.target.closest('form[data-form]'); if (!f) return;
e.preventDefault();
const h = FORMS[f.dataset.form]; if (h) h(f, e);
});
/* One delegated click handler. Links (#token) navigate; an element with data-act runs its action.
An action inside a link (a star on a card) runs without following the link. */
document.addEventListener('click', e => {
const el = e.target.closest('[data-act]');
const a = e.target.closest('a[href^="#"]');
if (el && a && el !== a && a.contains(el)) { e.preventDefault(); const f = ACT[el.dataset.act]; if (f) f(el, e); return; }
if (a) {
e.preventDefault();
if (el === a) { const f = ACT[el.dataset.act]; if (f) f(el, e); }
const t = a.getAttribute('href').slice(1);
if (!t) return;
if (parseToken(t)) go(t); else toast('That page isn’t part of this prototype.');
return;
}
if (el) { const f = ACT[el.dataset.act]; if (f) f(el, e); }
});
document.addEventListener('change', e => {
const el = e.target.closest('[data-act-change]'); if (!el) return;
const f = ACT['change:' + el.dataset.actChange]; if (f) f(el, e);
});
Object.assign(ACT, {
'go': el => go(el.dataset.to),
'toast': el => toast(el.dataset.msg),
'pop-close': () => closePop(),
'modal-close': () => closeModal(),
'sheet-close': () => closeSheet(),
'nav-toggle': () => { if (shellOf(R) === 'full') S.navMin = !S.navMin; else S.sbOpen = !S.sbOpen; renderNav(shellOf(R)); layoutVars(); save(); const b = $('#nav .nav-x, #nav .nav-exp'); if (b && b.offsetParent) b.focus(); },
'tree-toggle': () => { S.treeOpen = !S.treeOpen; renderPanel(shellOf(R)); layoutVars(); save(); const b = $('#rpanel .rp-round'); if (b) b.focus(); },
'tree': el => { const k = el.dataset.key; S.tree[k] = !S.tree[k]; if (DRAWER_OPEN) paintDrawer(); else renderPanel(shellOf(R)); save(); const b = $(`[data-act="tree"][data-key="${k}"]`); if (b) b.focus(); },
'widget': el => el.closest('[data-wid]').classList.toggle('shut'),
'drawer-open': el => openDrawer(el.dataset.tab),
'drawer-close': () => closeDrawer(),
'drawer-tab': el => { DRAWER_TAB = el.dataset.k; paintDrawer(); const b = $(`#mdrawer [data-k="${DRAWER_TAB}"]`); if (b) b.focus(); },
'new-menu': el => newMenu(el),
'unlocks': el => unlocksPop(el),
'theme-menu': el => themePop(el),
'notifs': el => notifPop(el),
'account': el => accountMenu(el),
'proto': el => protoPop(el),
'sx-seg': el => segMenu(el),
'sx-seg-pick': el => { SX.seg = el.dataset.v; closePop(); renderTop(); const i = $('#sx-q'); i.focus(); },
'sx-unscope': () => { SX.scoped = false; renderTop(); $('#sx-q').focus(); toast('Search now covers all of StudyBoost'); },
'sx-clear': () => { const i = $('#sx-q'); i.value = ''; i.focus(); sxRender(); },
'pro': (el) => { const a = el && el.closest && el.closest('.pop') ? (POP && POP.anchor) : el; proPop(a, el && el.dataset ? el.dataset.feature : null); },
'pro-plan': el => { S.proPlan = el.dataset.v; $$('[data-act="pro-plan"]').forEach(b => { const on = b === el; b.setAttribute('aria-checked', on); b.setAttribute('aria-pressed', on); }); },
'pro-go': () => { S.plan = 'pro'; closePop(); closeModal(true); toast('You’re on Pro for this demo. Premium documents and themes are open.'); rerender(); },
'theme': el => { S.theme = el.dataset.v; applyTheme(S.theme, true); $$('[data-act="theme"]').forEach(b => b.setAttribute('aria-checked', b.dataset.v === S.theme)); const t = THEMES.find(x => x.id === S.theme); if (t.group === 'Pro' && S.plan !== 'pro') toast(`Previewing ${t.name}. Pro keeps it on.`); save(); },
'motion': el => { S.motion = !S.motion; applyTheme(S.theme); el.setAttribute('aria-checked', S.motion); save(); },
'notifs-read': () => { NOTIFS.forEach(n => { n.unread = false; }); closePop(); renderTop(); },
'notif': el => { NOTIFS[+el.dataset.i].unread = false; },
'persona': el => { setPersona(el.dataset.v); RESETS.forEach(f => f()); closePop(); render.tourShown = false; applyTheme(S.theme); go('room', { replace: true }); toast(S.persona === 'new' ? 'Now viewing as a first-time visitor.' : 'Now viewing as Maya, a returning student.'); },
'reset-demo': () => { const p = S.persona; store.clear(); setPersona(p); RESETS.forEach(f => f()); closePop(); render.tourShown = false; applyTheme(S.theme); go('room', { replace: true }); toast('Demo reset.'); },
'tour': () => { closePop(); if (R.page !== 'room') go('room'); setTimeout(() => window.TOUR && TOUR.start(), 120); },
'upload': () => { closePop(); openUpload(); },
'new-note': () => { closePop(); if (R.page !== 'room') go('room'); setTimeout(() => { const j = $('#jot'); if (j) { j.scrollIntoView({ block: 'center' }); j.focus(); } }, 60); },
'ai-course': el => { const a = AI_COURSES.find(x => x.id === el.dataset.id); toast(`${a.name}: resumes at ${a.next}.`); },
'ai-course-make': () => proPop(null, 'AI Courses'),
'notes-sheet': () => openNotesSheet(),
'hoot-open': el => { closePop(); closeDrawer(true); if (window.HOOT) HOOT.open(el && el.dataset ? el.dataset.q : null); }
});
ACT['change:loc'] = el => { S.loc = el.value; if (typeof DV !== 'undefined') { DV.c.country = null; DV.d.r = null; DV.a.all = false; } save(); rerender(); if (R.page === 'docs' && R.view === 'radar' && typeof sweep === 'function') sweep(); toast('Showing schools near ' + LOC().full); };
function openUpload() {
const card = openModal(`<div class="mcard-h"><div><h2>Upload a document</h2><p>Notes, study guides and past exams. You choose a course and who can see it.</p></div><button type="button" class="s-ib" data-act="modal-close" aria-label="Close">${icon('i-x')}</button></div>
<form class="mcard-b" id="up-form">
<label class="drop"><input type="file" id="up-file" accept=".pdf,.doc,.docx,.ppt,.pptx,.png,.jpg" class="sr"><span class="drop-ic">${icon('i-upload', 'lg')}</span><b id="up-name">Drop a file here, or choose one</b><small>PDF, Word, slides or photos · up to 50 MB</small></label>
<label class="fld"><span>Course</span><select class="s-input" id="up-course">${S.starred.map(k => { const [sid, code] = k.split('|'); const c = courseBy(sid, code); return c ? `<option value="${esc(k)}">${esc(c.code + ' · ' + c.title)}</option>` : ''; }).join('')}<option value="">Another course…</option></select></label>
<button type="submit" class="s-btn primary lg">${icon('i-upload', 'sm')}Upload</button>
</form>`, { label: 'Upload a document', focus: '#up-course' });
$('#up-file', card).addEventListener('change', e => { const f = e.target.files && e.target.files[0]; if (f) $('#up-name', card).textContent = f.name; });
$('#up-form', card).addEventListener('submit', e => { e.preventDefault(); closeModal(); toast('Prototype: the upload would start here. Nothing leaves your device.'); });
}
function openNotesSheet() {
openSheet('Notes', S.notes.length ? S.notes.map(n => `<div class="note-row"><span class="nr-ic">${icon('i-pin')}</span><span>${esc(n.text)}</span><button type="button" class="s-ib sm" data-act="delnote" data-id="${n.id}" aria-label="Delete note">${icon('i-x', 'sm')}</button></div>`).join('') : '<p class="muted">No notes yet. Jot one on your pinboard in My Room.</p>');
}
/* category glyph (Course Hub), used across the site */
const GLYPH_PATHS = {
1: '<rect x="3" y="4.5" width="18" height="12" rx="2" fill="var(--cg-fill)"/><path d="M2 20h20"/><path d="m10 8.5-2 2 2 2M14 8.5l2 2-2 2"/>',
2: '<ellipse cx="12" cy="12" rx="9.5" ry="3.8"/><ellipse cx="12" cy="12" rx="9.5" ry="3.8" transform="rotate(60 12 12)"/><ellipse cx="12" cy="12" rx="9.5" ry="3.8" transform="rotate(120 12 12)"/><circle cx="12" cy="12" r="1.8" fill="currentColor"/>',
3: '<circle cx="12" cy="12" r="6.5" fill="var(--cg-fill)"/><circle cx="12" cy="12" r="2.6"/><path d="M12 2.5v3M12 18.5v3M2.5 12h3M18.5 12h3M5.3 5.3l2.1 2.1M16.6 16.6l2.1 2.1M5.3 18.7l2.1-2.1M16.6 7.4l2.1-2.1"/>',
4: '<path d="M7.6 15h8.8l2.6 3.8a1.6 1.6 0 0 1-1.4 2.2H6.4A1.6 1.6 0 0 1 5 18.8z" fill="var(--cg-fill)" stroke="none"/><path d="M9 3h6M10 3v6L5 18.8A1.6 1.6 0 0 0 6.4 21h11.2a1.6 1.6 0 0 0 1.4-2.2L14 9V3"/><path d="M7.6 15h8.8"/>',
5: '<path d="M3 20.5h18"/><rect x="4.5" y="12" width="3.5" height="8.5" rx="1" fill="var(--cg-fill)"/><rect x="10.25" y="8" width="3.5" height="12.5" rx="1" fill="var(--cg-fill)"/><rect x="16" y="11" width="3.5" height="9.5" rx="1" fill="var(--cg-fill)"/><path d="M4 8.5 9 5l4 2.5 7-4.5"/>',
6: '<circle cx="9" cy="8" r="3.2" fill="var(--cg-fill)"/><path d="M3 20c0-3.3 2.7-6 6-6s6 2.7 6 6"/><circle cx="17" cy="9" r="2.5"/><path d="M15.6 14.3A5 5 0 0 1 21 19"/>',
7: '<path d="M2.5 9.5 12 3.5l9.5 6z" fill="var(--cg-fill)"/><path d="M3 21h18M5.5 21v-8.5M10 21v-8.5M14 21v-8.5M18.5 21v-8.5"/>',
8: '<path d="M12 20.5s-7.2-4.5-9.1-9.4C1.6 7.7 3.8 4 7.3 4c2 0 3.6 1.1 4.7 2.8C13.1 5.1 14.7 4 16.7 4c3.5 0 5.7 3.7 4.4 7.1C19.2 16 12 20.5 12 20.5z" fill="var(--cg-fill)"/><path d="M6.5 12h2.8l1.5-2.6 2.2 5 1.5-2.4h3"/>',
9: '<path d="M12 3.5v17M7.5 20.5h9M5 7h14"/><path d="M5 7 2.5 13a2.6 2.6 0 0 0 5 0z" fill="var(--cg-fill)"/><path d="M19 7l-2.5 6a2.6 2.6 0 0 0 5 0z" fill="var(--cg-fill)"/>',
10: '<path d="M3 10v4a1 1 0 0 0 1 1h2l6 4V5L6 9H4a1 1 0 0 0-1 1z" fill="var(--cg-fill)"/><path d="M16 9a4 4 0 0 1 0 6M19 6.5a8 8 0 0 1 0 11"/>',
11: '<path d="M12 3a6 6 0 0 0-3.5 10.9c.6.5 1 1.2 1 2.1h5c0-.9.4-1.6 1-2.1A6 6 0 0 0 12 3z" fill="var(--cg-fill)"/><path d="M9.5 18.5h5M10.5 21h3"/>'
};
function catGlyph(cat, size, cls) {
const c = GLYPH_PATHS[cat] ? cat : 11, s = size || 40, ic = Math.round(s * 0.6);
return `<span class="cg${cls ? ' ' + cls : ''}" data-c="${c}" style="--cg-s:${s}px" aria-hidden="true"><svg width="${ic}" height="${ic}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="${c === 2 ? 1.8 : 2}" stroke-linecap="round" stroke-linejoin="round">${GLYPH_PATHS[c]}</svg></span>`;
}
/* small paper thumbnail (Course Hub DocThumb), used in search and lists */
function miniThumb(hub, w) {
const k = THUMB_OF[hub] || 'guide';
return `<span class="mth mth-${k}" style="--w:${w || 32}px" aria-hidden="true"><i class="h"></i>${k === 'exam' ? '<i class="q"></i><i class="q"></i><i class="q"></i>' : k === 'sheet' ? '<i class="g"></i><i></i><i></i>' : k === 'guide' ? '<i class="b"></i><i></i><i></i><i></i>' : '<i></i><i></i><i></i><i></i><i></i>'}</span>`;
}
/* ===== 72b-ui.js ===== */
/* ================= shared page pieces ================= */
function crumbs(parts) {
const all = [['room', 'Home']].concat(parts || []);
return `<nav class="crumbs" aria-label="Breadcrumb">${all.map(([t, l], i) => i === all.length - 1 ? `<span aria-current="page">${esc(l)}</span>` : `<a href="#${t}">${i === 0 ? icon('i-home', 'xs') : ''}${esc(l)}</a>`).join(icon('i-right', 'xs sep'))}</nav>`;
}
const allTasks = () => S.shelves.flatMap(s => tasksOf(s.id).map(t => Object.assign({ shelf: s.id }, t)));
function findTask(id) { for (const s of S.shelves) { const t = tasksOf(s.id).find(x => x.id === id); if (t) return { t, s }; } return null; }
function dueTxt(day) { if (day < 0) { const d = addDays(day); return `${MON[d.getMonth()]} ${d.getDate()}`; } return day === 0 ? 'Today' : day === 1 ? 'Tomorrow' : day < 7 ? DOW[addDays(day).getDay()] : dateTxt(day).replace(/^\w+, /, ''); }
function docMetaLine(d) { const s = schoolOfDoc(d); return `${esc(d.code || '')}${s ? ' · ' + esc(s.short) : ''} · ${plural(d.pages, 'page')}`; }
/* a compact document row used in sheets, shelves and lists */
function docRowSm(d, extra) {
return `<a class="drow-sm" href="#${tokDoc(d.id)}">${miniThumb(d.hub || hubType(d.type), 30)}<span class="t"><b>${esc(d.title)}</b><small>${esc(d.type)} · ${docMetaLine(d)}</small></span>${d.pro && S.plan !== 'pro' && !S.unlocked.includes(d.id) ? `<span class="s-pro">${icon('i-lock', 'xs')} PRO</span>` : '<span class="free-tag">Free</span>'}${extra || ''}</a>`;
}
function emptyBox(title, body, actions) { return `<div class="empty-box"><b>${title}</b>${body ? `<p>${body}</p>` : ''}${actions ? `<div class="eb-acts">${actions}</div>` : ''}</div>`; }
function toggleSaved(id) { const on = toggleIn(S.saved, id); save(); return on; }
function addNote(text) { S.notes.push({ id: 'n' + Date.now(), text }); save(); }
/* ================= school colours (the Campus Hub colour engine, shared by every page) ================= */
const hex2rgb = h => [1, 3, 5].map(i => parseInt(h.slice(i, i + 2), 16));
const rgb2hex = a => '#' + a.map(v => Math.round(Math.max(0, Math.min(255, v))).toString(16).padStart(2, '0')).join('').toUpperCase();
const lum = h => { const [r, g, b] = hex2rgb(h).map(v => { v /= 255; return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4); }); return 0.2126 * r + 0.7152 * g + 0.0722 * b; };
const contrast = (a, b) => { const x = lum(a), y = lum(b); return (Math.max(x, y) + 0.05) / (Math.min(x, y) + 0.05); };
const mixHex = (a, b, t) => { const A = hex2rgb(a), B = hex2rgb(b); return rgb2hex(A.map((v, i) => v + (B[i] - v) * t)); };
const chroma = h => { const [r, g, b] = hex2rgb(h); return (Math.max(r, g, b) - Math.min(r, g, b)) / 255; };
const fitC = (c, bg, target, toward) => { for (let t = 0; t <= 1.0001; t += 0.03) { const x = mixHex(c, toward, t); if (contrast(x, bg) >= target) return x; } return toward; };
const PAL = new Map();
function schoolPal(s) {
if (PAL.has(s.id)) return PAL.get(s.id);
const c = CAMPUS[s.id] || {};
let given = [c.cp, c.cs].filter(x => x && /^#[0-9a-f]{6}$/i.test(x)).map(x => x.toUpperCase());
if (!given.length && s.color) given = [s.color.toUpperCase()];
let vivid = given.filter(x => chroma(x) >= 0.12);
const darkNeutral = given.filter(x => chroma(x) < 0.12 && lum(x) < 0.03);
const fallback = vivid.length === 0;
if (fallback) { vivid = ['#008B85']; darkNeutral.push('#063F3C'); }
const pool = [...vivid, ...darkNeutral].sort((a, b) => lum(a) - lum(b));
let deep = pool[0];
const bright = vivid.filter(x => x !== deep).sort((a, b) => lum(b) - lum(a))[0] || vivid[0];
if (lum(deep) < 0.004) deep = mixHex(deep, bright, 0.1);
if (contrast(deep, '#FFFFFF') < 4.5) deep = fitC(deep, '#FFFFFF', 4.5, '#000000');
const pickInk = (bg, target, toward) => vivid.find(x => contrast(x, bg) >= target) || fitC(vivid[0], bg, target, toward);
const out = { deep, bright, fallback, given, onDeep: '#FFFFFF', brightOnDeep: fitC(bright, deep, 4.5, '#FFFFFF'),
inkL: pickInk('#FFFFFF', 5, '#000000'), inkD: pickInk('#1D2021', 5, '#FFFFFF'),
tintL: mixHex(bright, '#FFFFFF', 0.88), tintD: mixHex(bright, '#1D2021', 0.8), washL: mixHex(deep, '#FFFFFF', 0.9), washD: mixHex(deep, '#1D2021', 0.72) };
PAL.set(s.id, out);
return out;
}
/* ===== 73-room.js ===== */
/* ================= My Room (the "combined" layout from My Room round 2, inside the shelves epic shell) ================= */
const TEMPLATES = [
{ id: 'blank', name: 'Blank shelf', pv: '<div class="pv blank">Empty</div>', desc: 'Just documents, like today. Add tasks later if you want.', gets: ['Board, Table and List views, empty', 'Your documents'], tasks: [] },
{ id: 'planner', name: 'Student planner', pv: '<div class="pv"><div class="col"><i class="a"></i><i></i><i></i></div><div class="col"><i></i><i class="g"></i></div><div class="col"><i></i></div></div>', desc: 'Weekly readings, assignments and a recurring “review notes” task.', gets: ['Columns: To do, Doing, Done', '6 example tasks: reading, assignment, review', 'The week widget shows what is due each day'], ex: '“Read ch. 3 before Thursday lecture”, “Problem set 4 due Mon”',
tasks: [['Read ch. 3 before Thursday lecture', 2], ['Problem set 4', 5], ['Review lecture notes', 1], ['Assignment 2 draft', 6], ['Office hours question list', 3], ['Review notes for the week', 4]] },
{ id: 'exam', name: 'Exam countdown', pv: '<div class="pv cal">' + Array.from({ length: 14 }, (_, i) => `<i class="${i === 9 ? 'g' : ''}"></i>`).join('') + '</div>', desc: 'Set the exam date; the shelf plans backwards from it.', gets: ['Exam date and countdown in the shelf panel and My Room', 'Columns: To do, Doing, Done', 'Starter tasks: read, flashcards, practice quiz, summary sheet'], ex: '“PSY 110 Final · Dec 14”', exam: true,
tasks: [['Read the chapters the exam covers', 1], ['Make flashcards for the key terms', 3], ['Take a practice quiz', 6], ['One-page summary sheet', 9], ['Ask Hoot to quiz you on weak spots', 11]] },
{ id: 'board', name: 'Task board', pv: '<div class="pv"><div class="col"><i class="a"></i><i></i></div><div class="col"><i></i><i class="a"></i><i></i></div><div class="col"><i></i></div></div>', desc: 'A plain kanban. Good for group projects and theses.', gets: ['Columns: To do, Doing, Done', '3 example cards', 'Priority and due date on cards'], ex: '“Draft intro”, “Collect 5 sources”, “Peer review”',
tasks: [['Draft the introduction', 4], ['Collect 5 sources', 2], ['Peer review with the group', 8]] },
{ id: 'reading', name: 'Reading list', pv: '<div class="pv list"><i style="width:80%"></i><i style="width:65%"></i><i style="width:72%"></i><i style="width:50%"></i></div>', desc: 'One row per document with a “read by” date and a done tick.', gets: ['List view first', 'One task per document on the shelf', 'Sort by read-by date'], ex: '“Chapter 7 · read by Fri”', view: 'list',
tasks: [['Chapter 7 · read by Friday', 3], ['Chapter 8 · read by next Tuesday', 7]] }
];
const NEW_SHELF = { name: '', kind: 'Exam', color: 'violet', tpl: 'exam', days: 21 };
function trow(t, withDay) {
const s = shelfById(t.shelf), done = t.st === 'done';
return `<div class="trow${done ? ' is-done' : ''}"><button type="button" class="check" data-act="task-toggle" data-id="${t.id}" aria-pressed="${done}" aria-label="${done ? 'Mark as not done' : 'Mark done'}: ${esc(t.t)}">${icon('i-check')}</button>
<span class="trow-b"><button type="button" class="trow-t" data-act="task-open" data-id="${t.id}">${esc(t.t)}</button><span class="trow-s">${dotEl(shelfColor(s))}<a href="#${tokShelf(s.id)}">${esc(s.name)}</a>${withDay && !done ? ` · <span class="day">${dueTxt(t.day)}</span>` : ''}${t.pri === 'high' && !done ? ' · <span class="hi">High</span>' : ''}</span></span></div>`;
}
function nextExam() { return S.shelves.filter(s => s.exam).sort((a, b) => a.exam.days - b.exam.days)[0] || null; }
function wallBand() {
const today = allTasks().filter(t => t.day === 0);
const open = today.filter(t => t.st !== 'done');
const nx = nextExam();
const rot = ['-0.8deg', '0.6deg', '-0.4deg'];
const shown = S.notes.slice(-3);
const hello = S.persona === 'new' ? `Welcome, ${PERSONA.first}.` : `${greeting()}, ${PERSONA.first}.`;
return `<section class="wall" aria-label="Today">
<div class="wall-l"><div><h1 class="greet">${hello}</h1><p class="hsub">${open.length ? `<b>${plural(open.length, 'task')} due today</b>` : '<b>Today is clear.</b>'}${nx ? ` · ${esc(nx.exam.title)} ${inDays(nx.exam.days)}` : ''}</p>
<p class="phone-only"><button type="button" class="mbtn" data-act="drawer-open" data-tab="panel">${icon('i-shelf', 'sm')}Your room: shelves, courses, notes</button></p></div>
<div class="today"><span class="sub2">Due today<em>${open.length}</em></span>${today.length ? '' : '<p class="clear-note">Nothing is due today.</p>'}${today.length && !open.length ? '<p class="clear-note">Everything due today is done.</p>' : ''}<div class="trows">${today.map(t => trow(t)).join('')}</div></div></div>
<div class="wall-r"><div class="modh"><span class="ey amber">${icon('i-pin', 'sm')}Pinboard</span><button type="button" class="modlink" data-act="notes-sheet">All notes ${icon('i-right', 'xs')}</button></div>
<div class="stickies">${shown.map((n, i) => `<div class="sticky" style="--r:${rot[i % 3]}">${esc(n.text)}<button type="button" class="nx" data-act="delnote" data-id="${n.id}" aria-label="Delete note: ${esc(n.text)}">${icon('i-x', 'xs')}</button></div>`).join('')}${shown.length < 3 ? `<div class="sticky-more">${shown.length ? `Room for ${3 - shown.length} more` : 'Pin your first note'}</div>` : ''}</div>
<form class="note-add" data-role="jot-form"><label class="sr" for="jot">New note</label><input id="jot" placeholder="Jot a note…" maxlength="80" autocomplete="off"><button type="submit" aria-label="Pin note">${icon('i-plus')}</button></form></div>
</section>`;
}
function roomToolbar() {
const d = [['docs', 'i-file', 'Documents', S.saved.length], ['courses', 'i-cap', 'Courses', S.starred.length], ['ai', 'i-sparkle', 'AI Courses', AI_COURSES.length], ['notes', 'i-pin', 'Notes', S.notes.length]];
return `<div class="toolbar">${d.map(([k, ic, l, n]) => `<button type="button" class="drawer2" data-act="room-drawer" data-k="${k}"><span class="ic">${icon(ic, 'sm')}</span>${l}<em>${n}</em></button>`).join('')}
<span class="spacer"></span><button type="button" class="s-btn sm" data-act="upload">${icon('i-upload', 'sm')}Upload</button><a class="s-btn sm" href="#documents">${icon('i-book', 'sm')}Browse</a></div>`;
}
function shelfCard(s) {
const n = s.exam ? s.exam.days : null, docs = s.docs.length;
return `<a class="shelf2" href="#${tokShelf(s.id)}" style="--c:${shelfColor(s)}" aria-label="Open ${esc(s.name)}">
<span class="shelf-art"><span class="pages">${[0, 1, 2].map(() => '<span class="pg"><i></i><i></i><i></i><i></i></span>').join('')}</span><span class="shelf-line"></span></span>
<span class="shelf-body"><span class="shelf-txt"><span class="shelf-name">${esc(s.name)}</span><span class="shelf-meta">${plural(docs, 'document')} · ${openTasks(s.id)} open</span>
${s.example ? `<span class="chip2 ex">${icon('i-sparkle', 'xs')}Example · edit or delete it</span>` : n != null ? `<span class="chip2${n <= 7 ? ' date' : ''}">${icon('i-calendar', 'xs')}${esc(s.exam.title)} ${inDays(n)}</span>` : `<span class="chip2">${icon('i-shelf', 'xs')}${esc(s.tpl || 'Blank shelf')}</span>`}</span><span class="shelf-go">${icon('i-right', 'sm')}</span></span>
</a>`;
}
function shelvesSection() {
const n = S.shelves.length, lim = S.plan === 'pro' ? 20 : 3, at = n >= lim;
return `<section aria-labelledby="shelves-h"><div class="sechead"><h2 class="lbl" id="shelves-h">${icon('i-shelf', 'sm')}Your shelves <span class="count">${n}</span></h2><span class="lim${at ? ' warn' : ''}"><span class="s-bar"><i style="width:${Math.min(100, n / lim * 100)}%;background:${at ? 'var(--pin-btn)' : 'var(--primary)'}"></i></span>${n} of ${lim}</span></div>
<div class="shelves2">${S.shelves.map(shelfCard).join('')}<button type="button" class="newtile" data-act="new-shelf"><span class="plus">${icon('i-plus')}</span><b>New shelf</b><small>Pick a template, or start blank</small></button></div></section>`;
}
function feedMod() {
if (!S.starred.length) return `<section class="mod feed"><div class="modh"><span class="ey">${icon('i-sparkle', 'sm')}New in your courses</span></div>${emptyBox('Star the courses you’re taking', 'New documents from those courses show up here, so you see them before the exam.', `<a class="s-btn sm primary" href="#documents">${icon('i-search', 'sm')}Find your school</a>`)}</section>`;
const ago = ['2h ago', '5h ago', 'Yesterday', 'Yesterday', '2 days ago', '3 days ago'];
const rows = [];
S.starred.forEach(k => { const [sid, code] = k.split('|'); const ds = courseDocs(sid, code); ds.slice(-2).reverse().forEach(d => rows.push(d)); });
const codes = S.starred.map(k => k.split('|')[1]);
return `<section class="mod feed"><div class="modh"><span class="ey">${icon('i-sparkle', 'sm')}New in your courses</span><a class="modlink" href="#${tokSchool(S.mine || 'um')}">Your Campus Hub ${icon('i-right', 'xs')}</a></div>
<div class="feed-rows">${rows.slice(0, 5).map((d, i) => `<a class="frow" href="#${tokDoc(d.id)}"><span class="fr-ago">${ago[i]}</span>${miniThumb(d.hub || hubType(d.type), 30)}<span class="t"><b>${esc(d.title)}</b><small>${esc(d.type)} · ${esc(d.code)} · ${plural(d.pages, 'page')}</small></span>${catGlyph(catOfCode(d.code), 28)}</a>`).join('')}</div>
<p class="feed-foot">From ${codes.slice(0, -1).join(', ')}${codes.length > 1 ? ' and ' : ''}${codes[codes.length - 1]}. Star more courses on your Campus Hub.</p></section>`;
}
function contMod() {
const a = AI_COURSES[0];
const lastDoc = S.recent.map(r => r.t === 'doc' && DOCS[r.id]).find(Boolean) || DOCS['cell-transport'];
return `<section class="mod"><div class="modh"><span class="ey">${icon('i-clock', 'sm')}Continue</span></div><div class="crows">
${S.persona === 'new' ? '' : `<button type="button" class="crow" data-act="ai-course" data-id="${a.id}"><span class="crow-ic">${icon('i-sparkle')}</span><span class="crow-b"><b>${esc(a.name)}</b><small>AI course · Next: ${esc(a.next)}</small><span class="s-bar"><i style="width:${a.pct}%"></i></span></span>${icon('i-right', 'sm')}</button>`}
<a class="crow" href="#${tokDoc(lastDoc.id)}"><span class="crow-ic doc">${icon('i-file')}</span><span class="crow-b"><b>${esc(lastDoc.title)}</b><small>Document · ${esc(lastDoc.code)} · ${plural(lastDoc.pages, 'page')}</small></span>${icon('i-right', 'sm')}</a></div></section>`;
}
function weekMod() {
const wk = allTasks().filter(t => t.day >= 1 && t.day <= 6 && t.st !== 'done').sort((a, b) => a.day - b.day);
const shown = wk.slice(0, 4);
return `<section class="mod"><div class="modh"><span class="ey rose">${icon('i-clock', 'sm')}Up next</span><button type="button" class="modlink" data-act="all-tasks">All tasks ${icon('i-right', 'xs')}</button></div>
<span class="sub2">Rest of the week<em>${wk.length}</em></span>
<div class="trows">${shown.length ? shown.map(t => trow(t, true)).join('') : '<p class="clear-note">Nothing else planned this week.</p>'}</div>
${wk.length > 4 ? `<button type="button" class="modlink more-link" data-act="all-tasks">${wk.length - 4} more this week ${icon('i-right', 'xs')}</button>` : ''}</section>`;
}
PAGES.room = {
cls: () => 'pg-room',
render: () => `<div class="page-in">${crumbs([['room', 'My Room']])}<div class="room">
${wallBand()}
${roomToolbar()}
${shelvesSection()}
<div class="combo">${feedMod()}<aside class="desk-side">${contMod()}${weekMod()}</aside></div>
</div></div>`,
mount(r, page) {
const f = $('[data-role="jot-form"]', page);
if (f) f.addEventListener('submit', e => { e.preventDefault(); const i = $('#jot'); const v = i.value.trim(); if (!v) { i.focus(); return; } addNote(v); rerender(); const j = $('#jot'); if (j) j.focus(); toast('Pinned to your board'); });
}
};
/* ---------- room drawers (the four library drawers, as sheets) ---------- */
ACT['room-drawer'] = el => {
const k = el.dataset.k;
if (k === 'notes') return openNotesSheet();
if (k === 'docs') return openSheet('Documents', S.saved.length ? S.saved.map(id => DOCS[id] || lazyDoc(id)).filter(Boolean).map(d => docRowSm(d)).join('') : emptyBox('No saved documents yet', 'Save documents from any course page and they land here.', `<a class="s-btn sm primary" href="#documents">Browse documents</a>`));
if (k === 'courses') return openSheet('Courses', (S.starred.length ? S.starred.map(key => { const [sid, code] = key.split('|'); const c = courseBy(sid, code); return c ? `<a class="drow-sm" href="#${tokCourse(sid, c.code)}">${catGlyph(catOfCourse(c), 34)}<span class="t"><b>${esc(c.code)} · ${esc(c.title)}</b><small>${esc(c.s.short)} · ${plural(c.docs, 'document')}</small></span>${icon('i-right', 'sm')}</a>` : ''; }).join('') : emptyBox('No courses yet', 'Star the courses you’re taking on your Campus Hub.')) + `<a class="s-link" href="#${tokSchool(S.mine || 'um')}" style="margin-top:6px">Browse courses on your Campus Hub</a>`);
if (k === 'ai') return openSheet('AI Courses', AI_COURSES.map(a => `<button type="button" class="drow-sm" data-act="ai-course" data-id="${a.id}"><span class="crow-ic">${icon('i-sparkle')}</span><span class="t"><b>${esc(a.name)}</b><small>Built from ${esc(a.from)} · next: ${esc(a.next)}</small><span class="s-bar" style="margin-top:6px"><i style="width:${a.pct}%"></i></span></span><span class="pct tnum">${a.pct}%</span></button>`).join('') + `<button type="button" class="s-btn" data-act="ai-course-make">${icon('i-wand', 'sm')}Build a new AI course</button>`);
};
ACT['delnote'] = el => { S.notes = S.notes.filter(n => n.id !== el.dataset.id); save(); if (SHEET) openNotesSheet(); rerender(); toast('Note removed'); };
ACT['all-tasks'] = () => {
const open = allTasks().filter(t => t.st !== 'done').sort((a, b) => a.day - b.day);
openSheet('All open tasks', open.length ? `<div class="trows">${open.map(t => trow(t, true)).join('')}</div>` : emptyBox('No open tasks', 'Everything is done. Nice.'));
};
ACT['task-toggle'] = el => {
const f = findTask(el.dataset.id); if (!f) return;
f.t.st = f.t.st === 'done' ? 'todo' : 'done';
save(); rerender();
if (SHEET) ACT['all-tasks']();
if (f.t.st === 'done') toast('Done: ' + f.t.t, { label: 'Undo', run: () => { f.t.st = 'todo'; save(); rerender(); } });
};
/* ---------- the shelf page ---------- */
const COLS = [['todo', 'To do', 'var(--faint)'], ['doing', 'Doing', 'var(--warn)'], ['done', 'Done', 'var(--success)']];
const pillDue = (t) => t.st === 'done' ? `<span class="pill">${icon('i-check', 'xs')}${dueTxt(t.day)}</span>` : t.day <= 0 ? `<span class="pill late">${icon('i-clock', 'xs')}${t.day < 0 ? 'Overdue' : 'Today'}</span>` : `<span class="pill due">${icon('i-calendar', 'xs')}${dueTxt(t.day)}</span>`;
const pillPri = t => t.pri === 'high' ? `<span class="pill hi">${icon('i-flame', 'xs')}High</span>` : '';
const pillDoc = t => t.doc && DOCS[t.doc] ? `<a class="pill doc" href="#${tokDoc(t.doc)}">${icon('i-file', 'xs')}${esc(DOCS[t.doc].title)}</a>` : '';
function boardView(s) {
const ts = tasksOf(s.id);
return `<div class="board" data-role="board">${COLS.map(([k, l, c]) => { const rows = ts.filter(t => t.st === k).sort((a, b) => a.day - b.day); return `<div class="bcol" data-col="${k}">
<div class="colh"><span class="st" style="background:${c}"></span><b>${l}</b><span class="n">${rows.length}</span></div>
<div class="col" data-drop="${k}">${rows.map(t => `<div class="card${k === 'done' ? ' done' : ''}" draggable="true" data-task="${t.id}"><span class="grip" aria-hidden="true">${icon('i-grip', 'xs')}</span><button type="button" class="card-t" data-act="task-open" data-id="${t.id}">${esc(t.t)}</button><div class="row">${pillDue(t)}${pillPri(t)}${pillDoc(t)}</div></div>`).join('')}
<button type="button" class="addc" data-act="task-add" data-st="${k}">${icon('i-plus', 'xs')}Add task</button></div></div>`; }).join('')}</div>`;
}
function tableView(s) {
const st = { todo: 'To do', doing: 'Doing', done: 'Done' };
const ts = tasksOf(s.id).slice().sort((a, b) => (a.st === 'done') - (b.st === 'done') || a.day - b.day);
return `<div class="tbl-wrap"><table class="tbl"><thead><tr><th style="width:42%">Task</th><th>Status</th><th>Due</th><th>Linked document</th><th>Priority</th></tr></thead><tbody>
${ts.map(t => `<tr><td class="t"><button type="button" class="cb${t.st === 'done' ? ' on' : ''}" data-act="task-toggle" data-id="${t.id}" aria-pressed="${t.st === 'done'}" aria-label="Done: ${esc(t.t)}">${t.st === 'done' ? icon('i-check') : ''}</button><button type="button" class="tt" data-act="task-open" data-id="${t.id}">${esc(t.t)}</button></td><td><span class="pill">${st[t.st]}</span></td><td>${pillDue(t)}</td><td>${pillDoc(t) || '<span class="muted">—</span>'}</td><td>${pillPri(t) || '<span class="muted">—</span>'}</td></tr>`).join('')}
</tbody></table></div>`;
}
function listView(s) {
const ts = tasksOf(s.id);
const groups = [['Today', t => t.st !== 'done' && t.day <= 0], ['Tomorrow', t => t.st !== 'done' && t.day === 1], ['This week', t => t.st !== 'done' && t.day > 1 && t.day <= 7], ['Later', t => t.st !== 'done' && t.day > 7], ['Done', t => t.st === 'done']];
return `<div class="lgroup">${groups.map(([g, f]) => { const rows = ts.filter(f).sort((a, b) => a.day - b.day); if (!rows.length) return ''; return `<h3 class="lg-h">${g}<span class="n">${rows.length}</span></h3>${rows.map(t => `<div class="lrow${t.st === 'done' ? ' done' : ''}"><button type="button" class="cb${t.st === 'done' ? ' on' : ''}" data-act="task-toggle" data-id="${t.id}" aria-pressed="${t.st === 'done'}" aria-label="Done: ${esc(t.t)}">${t.st === 'done' ? icon('i-check') : ''}</button><button type="button" class="t" data-act="task-open" data-id="${t.id}">${esc(t.t)}</button>${pillDoc(t)}${pillPri(t)}${g === 'This week' || g === 'Later' ? `<span class="pill">${dueTxt(t.day)}</span>` : ''}</div>`).join('')}`; }).join('') || emptyBox('No tasks yet', 'Add one, or start from a template on a new shelf.', `<button type="button" class="s-btn sm primary" data-act="task-add">${icon('i-plus', 'sm')}Add task</button>`)}</div>`;
}
function docsView(s) {
const docs = s.docs.map(id => DOCS[id]).filter(Boolean);
const used = id => tasksOf(s.id).filter(t => t.doc === id).length;
return `<div class="dlist">${docs.map(d => `<div class="docrow">${miniThumb(d.hub || hubType(d.type), 34)}<a class="t" href="#${tokDoc(d.id)}"><b>${esc(d.title)}</b><small>${esc(d.type)} · ${docMetaLine(d)}${used(d.id) ? ` · used by ${plural(used(d.id), 'task')}` : ''}</small></a><button type="button" class="s-ib sm" data-act="shelf-doc-remove" data-id="${d.id}" aria-label="Remove ${esc(d.title)} from this shelf" title="Remove from shelf">${icon('i-x', 'sm')}</button></div>`).join('')}
<button type="button" class="addc wide" data-act="add-docs">${icon('i-plus', 'xs')}Add documents from your library or your courses</button></div>`;
}
PAGES.shelf = {
cls: () => 'pg-room',
render(r) {
const s = shelfById(r.id), view = S.shelfView[s.id] || 'board';
const V = [['board', 'i-kanban', 'Board'], ['table', 'i-table', 'Table'], ['list', 'i-list', 'List'], ['docs', 'i-file', 'Documents']];
const body = view === 'board' ? boardView(s) : view === 'table' ? tableView(s) : view === 'list' ? listView(s) : docsView(s);
const pinned = S.pins.some(p => p.t === 'shelf' && p.id === s.id);
return `<div class="page-in">${crumbs([['room', 'My Room'], [tokShelf(s.id), s.name]])}
<div class="ph"><div class="ph-l">
${s.example ? `<div class="ex-name"><span class="sq" style="background:${shelfColor(s)}">${icon('i-shelf')}</span><label class="sr" for="shelf-name">Shelf name</label><input id="shelf-name" class="title-in" value="${esc(s.name)}" maxlength="40" aria-describedby="ex-help"><span class="chip2 ex">${icon('i-sparkle', 'xs')}Example</span></div><p class="ex-help" id="ex-help">This shelf is an example. Rename it, fill it with your documents, or delete it.</p>`
: `<h1><span class="sq" style="background:${shelfColor(s)}">${icon('i-shelf')}</span>${esc(s.name)}</h1>`}
<div class="meta">${s.exam ? `<span class="chipc">${icon('i-calendar', 'xs')}${esc(s.exam.title)} ${inDays(s.exam.days)}</span>` : ''}<span>${plural(s.docs.length, 'document')}</span><span>${openTasks(s.id)} of ${plural(tasksOf(s.id).length, 'task')} open</span><span>Template: ${esc(s.tpl || 'Blank shelf')}</span></div></div>
<div class="acts"><button type="button" class="s-btn sm phone-only" data-act="drawer-open" data-tab="panel">${icon('i-panel', 'sm')}Shelf menu</button><button type="button" class="s-btn sm" data-act="hoot-open" data-q="Quiz me on ${esc(s.name)}">${icon('i-message', 'sm')}Ask Hoot on this shelf</button><button type="button" class="s-btn sm" data-act="shelf-game">${icon('i-gamepad', 'sm')}Make a game from it</button><button type="button" class="s-btn sm primary" data-act="task-add">${icon('i-plus', 'sm')}Add task</button><button type="button" class="s-ib" data-act="shelf-more" aria-haspopup="menu" aria-expanded="false" aria-label="More shelf actions" title="More">${icon('i-more')}</button></div></div>
${pinned ? '' : ''}
<div class="views" role="tablist" aria-label="Shelf views">${V.map(([k, ic, l]) => `<button type="button" role="tab" aria-selected="${view === k}" class="vt" data-act="shelf-view" data-view="${k}">${icon(ic, 'sm')}${l}${k === 'docs' ? ` <span class="n">${s.docs.length}</span>` : ''}</button>`).join('')}
<span class="tools"><button type="button" class="s-btn sm quiet" data-act="toast" data-msg="Prototype: filter by status, document or priority.">${icon('i-filter', 'sm')}Filter</button><button type="button" class="s-btn sm quiet" data-act="toast" data-msg="Sorted by due date.">${icon('i-list', 'sm')}Sort: due</button></span></div>
<div class="vbody" role="tabpanel">${body}</div></div>`;
},
mount(r, page) {
const s = shelfById(r.id);
const inp = $('#shelf-name', page);
if (inp) {
const commit = () => { const v = inp.value.trim(); if (v && v !== s.name) { s.name = v; s.example = false; s.tpl = s.tpl === 'Example' ? 'Blank shelf' : s.tpl; save(); renderNav(shellOf(R)); renderPanel(shellOf(R)); toast('Renamed to ' + v + '. It’s your shelf now.'); rerender(); } };
inp.addEventListener('keydown', e => { if (e.key === 'Enter') { e.preventDefault(); inp.blur(); } });
inp.addEventListener('change', commit);
}
// drag cards between columns (keyboard: open the card and change its status)
const board = $('[data-role="board"]', page);
if (board) {
let dragId = null;
board.addEventListener('dragstart', e => { const c = e.target.closest('.card'); if (!c) return; dragId = c.dataset.task; c.classList.add('dragging'); try { e.dataTransfer.setData('text/plain', dragId); e.dataTransfer.effectAllowed = 'move'; } catch (er) { /* ignore */ } });
board.addEventListener('dragend', e => { const c = e.target.closest('.card'); if (c) c.classList.remove('dragging'); $$('.col.over', board).forEach(x => x.classList.remove('over')); });
board.addEventListener('dragover', e => { const col = e.target.closest('[data-drop]'); if (!col || !dragId) return; e.preventDefault(); $$('.col.over', board).forEach(x => x !== col && x.classList.remove('over')); col.classList.add('over'); });
board.addEventListener('drop', e => { const col = e.target.closest('[data-drop]'); if (!col || !dragId) return; e.preventDefault(); const f = findTask(dragId); dragId = null; if (f && f.t.st !== col.dataset.drop) { f.t.st = col.dataset.drop; save(); rerender(); toast(`Moved to ${COLS.find(c => c[0] === f.t.st)[1]}`); } });
}
}
};
ACT['shelf-view'] = el => { const id = R.page === 'shelf' ? R.id : null; if (!id) return; S.shelfView[id] = el.dataset.view; save(); if (DRAWER_OPEN) closeDrawer(true); rerender(); const t = $(`.views [data-view="${el.dataset.view}"]`); if (t) t.focus(); };
ACT['shelf-more'] = el => {
const s = shelfById(R.id), pinned = S.pins.some(p => p.t === 'shelf' && p.id === s.id);
openPop(el, `<div class="menu" role="menu" aria-label="Shelf actions">
<button type="button" class="mi" role="menuitem" data-act="shelf-pin">${icon('i-pin')}<span class="mt">${pinned ? 'Unpin from the sidebar' : 'Pin to the sidebar'}</span></button>
<button type="button" class="mi" role="menuitem" data-act="shelf-rename">${icon('i-edit')}<span class="mt">Rename</span></button>
<hr><button type="button" class="mi danger" role="menuitem" data-act="shelf-delete">${icon('i-trash')}<span class="mt">Delete shelf<small>Documents stay in your library</small></span></button></div>`, { role: 'menu', label: 'Shelf actions' });
};
ACT['shelf-pin'] = () => { const s = shelfById(R.id); const i = S.pins.findIndex(p => p.t === 'shelf' && p.id === s.id); if (i >= 0) S.pins.splice(i, 1); else S.pins.unshift({ t: 'shelf', id: s.id }); closePop(); save(); renderNav(shellOf(R)); toast(i >= 0 ? 'Unpinned from the sidebar' : 'Pinned to the sidebar'); };
ACT['shelf-rename'] = () => {
closePop(); const s = shelfById(R.id);
const card = openModal(`<div class="mcard-h"><div><h2>Rename shelf</h2></div><button type="button" class="s-ib" data-act="modal-close" aria-label="Close">${icon('i-x')}</button></div><form class="mcard-b" id="ren-form"><label class="fld"><span>Name</span><input class="s-input" id="ren-in" value="${esc(s.name)}" maxlength="40" required></label><button type="submit" class="s-btn primary">Save</button></form>`, { label: 'Rename shelf', focus: '#ren-in' });
$('#ren-form', card).addEventListener('submit', e => { e.preventDefault(); const v = $('#ren-in').value.trim(); if (!v) return; s.name = v; s.example = false; closeModal(); save(); rerender(); toast('Shelf renamed'); });
};
ACT['shelf-delete'] = () => {
closePop(); const s = shelfById(R.id);
const card = openModal(`<div class="mcard-h"><div><h2>Delete “${esc(s.name)}”?</h2><p>The shelf and its tasks go away. Its documents stay in your library.</p></div></div><div class="mcard-b" style="flex-direction:row;justify-content:flex-end"><button type="button" class="s-btn" data-act="modal-close">Cancel</button><button type="button" class="s-btn danger" id="del-go">${icon('i-trash', 'sm')}Delete shelf</button></div>`, { label: 'Delete shelf', focus: '.s-btn' });
$('#del-go', card).addEventListener('click', () => { S.shelves = S.shelves.filter(x => x.id !== s.id); delete S.tasks[s.id]; S.pins = S.pins.filter(p => !(p.t === 'shelf' && p.id === s.id)); S.recent = S.recent.filter(p => !(p.t === 'shelf' && p.id === s.id)); closeModal(true); save(); go('room'); toast(`Deleted ${s.name}`); });
};
ACT['shelf-doc-remove'] = el => { const s = shelfById(R.id); s.docs = s.docs.filter(x => x !== el.dataset.id); save(); rerender(); toast('Removed from this shelf', { label: 'Undo', run: () => { s.docs.push(el.dataset.id); save(); rerender(); } }); };
ACT['add-docs'] = () => {
const s = shelfById(R.id); if (!s) return;
const pool = []; const seen = new Set(s.docs);
S.saved.forEach(id => { const d = DOCS[id] || lazyDoc(id); if (d && !seen.has(id)) { pool.push(d); seen.add(id); } });
S.starred.forEach(k => { const [sid, code] = k.split('|'); courseDocs(sid, code).slice(0, 6).forEach(d => { if (!seen.has(d.id)) { pool.push(d); seen.add(d.id); } }); });
openSheet('Add documents to ' + s.name, pool.length ? `<p class="muted" style="font-size:13px">From your saved documents and your courses.</p>${pool.map(d => `<div class="add-row">${miniThumb(d.hub || hubType(d.type), 30)}<span class="t"><b>${esc(d.title)}</b><small>${esc(d.type)} · ${esc(d.code)}</small></span><button type="button" class="s-btn sm" data-act="shelf-doc-add" data-id="${d.id}">${icon('i-plus', 'sm')}Add</button></div>`).join('')}` : emptyBox('Nothing to add yet', 'Save documents from a course page first.', `<a class="s-btn sm primary" href="#documents">Browse documents</a>`));
};
ACT['shelf-doc-add'] = el => { const s = shelfById(R.id); if (!s.docs.includes(el.dataset.id)) s.docs.push(el.dataset.id); el.outerHTML = `<span class="added">${icon('i-check', 'sm')}Added</span>`; save(); const p = $('#page'), y = p.scrollTop; render(); $('#page').scrollTop = y; };
ACT['shelf-game'] = () => {
const s = R.page === 'shelf' ? shelfById(R.id) : null;
if (window.STUDY) STUDY.fromShelf(s);
go('study-create');
};
/* ---------- a task, opened ---------- */
function taskSheet(f, isNew, st) {
const s = f ? f.s : shelfById(R.id), t = f ? f.t : { id: 't' + Date.now(), t: '', st: st || 'todo', day: 1, doc: null, pri: null };
const docs = s.docs.map(id => DOCS[id]).filter(Boolean);
const days = [0, 1, 2, 3, 4, 5, 6, 7, 10, 14];
const sheet = openSheet(isNew ? 'New task' : 'Task', `<form class="task-form" id="task-form">
<label class="fld"><span>Task</span><input class="s-input" id="tk-t" value="${esc(t.t)}" placeholder="e.g. Practice quiz on chapter 5" maxlength="90" required></label>
<div class="fld"><span>Status</span><div class="pp-seg" role="radiogroup" aria-label="Status">${COLS.map(([k, l]) => `<button type="button" role="radio" aria-checked="${t.st === k}" aria-pressed="${t.st === k}" data-st="${k}">${l}</button>`).join('')}</div></div>
<label class="fld"><span>Due</span><select class="s-input" id="tk-day">${(days.includes(t.day) ? days : days.concat(t.day).sort((a, b) => a - b)).map(d => `<option value="${d}" ${d === t.day ? 'selected' : ''}>${d < 0 ? 'Overdue · ' + dueTxt(d) : d === 0 ? 'Today' : d === 1 ? 'Tomorrow' : dateTxt(d)}</option>`).join('')}</select></label>
<label class="fld"><span>Linked document</span><select class="s-input" id="tk-doc"><option value="">None</option>${docs.map(d => `<option value="${d.id}" ${t.doc === d.id ? 'selected' : ''}>${esc(d.title)}</option>`).join('')}</select></label>
<button type="button" class="pri-tog" role="switch" aria-checked="${t.pri === 'high'}" id="tk-pri"><span class="s-switch"><i></i></span>High priority</button>
<div class="task-acts"><button type="submit" class="s-btn primary">${isNew ? icon('i-plus', 'sm') + 'Add task' : 'Save'}</button>${isNew ? '' : `<button type="button" class="s-btn" data-act="hoot-open" data-q="Help me with: ${esc(t.t)}">${icon('i-message', 'sm')}Ask Hoot about this</button><button type="button" class="s-btn quiet danger" id="tk-del">${icon('i-trash', 'sm')}Delete</button>`}</div>
${t.doc && DOCS[t.doc] && !isNew ? `<a class="s-link" href="#${tokDoc(t.doc)}">${icon('i-file', 'xs')} Open ${esc(DOCS[t.doc].title)}</a>` : ''}
</form>`, { lead: `<span class="sdot" style="background:${shelfColor(s)};width:12px;height:12px"></span>` });
let stv = t.st, pri = t.pri;
$$('[data-st]', sheet).forEach(b => b.addEventListener('click', () => { stv = b.dataset.st; $$('[data-st]', sheet).forEach(x => { x.setAttribute('aria-checked', x === b); x.setAttribute('aria-pressed', x === b); }); }));
$('#tk-pri', sheet).addEventListener('click', e => { pri = pri === 'high' ? null : 'high'; e.currentTarget.setAttribute('aria-checked', pri === 'high'); });
$('#task-form', sheet).addEventListener('submit', e => {
e.preventDefault(); const v = $('#tk-t').value.trim(); if (!v) return;
Object.assign(t, { t: v, st: stv, day: +$('#tk-day').value, doc: $('#tk-doc').value || null, pri });
if (isNew) tasksOf(s.id).push(t);
closeSheet(true); save(); rerender(); toast(isNew ? 'Task added' : 'Task saved');
});
const del = $('#tk-del', sheet);
if (del) del.addEventListener('click', () => { S.tasks[s.id] = tasksOf(s.id).filter(x => x.id !== t.id); closeSheet(true); save(); rerender(); toast('Task deleted', { label: 'Undo', run: () => { tasksOf(s.id).push(t); save(); rerender(); } }); });
setTimeout(() => { const i = $('#tk-t'); if (i) i.focus(); }, 30);
}
ACT['task-open'] = el => { const f = findTask(el.dataset.id); if (f) taskSheet(f, false); };
ACT['task-add'] = el => { if (R.page !== 'shelf') { const s = S.shelves[0]; if (!s) return; go(tokShelf(s.id)); } if (DRAWER_OPEN) closeDrawer(true); taskSheet(null, true, el && el.dataset ? el.dataset.st : null); };
/* ---------- the New shelf modal ---------- */
function newShelfModal() {
const t = TEMPLATES.find(x => x.id === NEW_SHELF.tpl);
const kinds = ['Course', 'Exam', 'Standardized test', 'Project', 'Other'];
const n = S.shelves.length, lim = S.plan === 'pro' ? 20 : 3;
return `<div class="mh"><div><h2 id="ns-h">New shelf</h2><p>Name it, pick a colour, then start from a template with example tasks you can edit or delete.</p></div><button type="button" class="s-ib" data-act="modal-close" aria-label="Close">${icon('i-x')}</button></div>
<div class="mb">
<div class="mcol">
<label class="fld"><span>Name</span><input class="s-input title-in2" id="ns-name" value="${esc(NEW_SHELF.name)}" placeholder="e.g. PSY 110 · Final" maxlength="40"></label>
<div class="fld"><span>What is it for?</span><div class="kind" role="radiogroup" aria-label="What is it for?">${kinds.map(k => `<button type="button" role="radio" aria-checked="${NEW_SHELF.kind === k}" data-act="ns-kind" data-k="${k}">${k}</button>`).join('')}</div><small class="hint">Picks a matching template. You can still change it.</small></div>
<div class="fld"><span>Colour</span><div class="sw" role="radiogroup" aria-label="Colour">${Object.entries(SHELF_COLORS).map(([k, v]) => `<button type="button" role="radio" aria-checked="${NEW_SHELF.color === k}" aria-label="${k}" style="background:${v}" data-act="ns-color" data-k="${k}"></button>`).join('')}</div><small class="hint">The same seven colours shelves have today.</small></div>
${t.exam ? `<label class="fld"><span>Exam date</span><select class="s-input" id="ns-days">${[7, 14, 21, 30, 45, 60].map(d => `<option value="${d}" ${NEW_SHELF.days === d ? 'selected' : ''}>${dateTxt(d)} · in ${d} days</option>`).join('')}</select></label>` : ''}
</div>
<div class="mcol right">
<div class="tp-head"><span class="s-k">Template</span><span class="s-k" style="color:var(--primary-ink)">${esc(t.name)}</span></div>
<div class="tpls" role="radiogroup" aria-label="Template">${TEMPLATES.map(x => `<button type="button" class="tpl" role="radio" aria-checked="${x.id === NEW_SHELF.tpl}" data-act="ns-tpl" data-k="${x.id}">${x.pv}<b>${x.name}</b><small>${x.desc}</small>${x.id === NEW_SHELF.tpl ? '<span class="pick">Selected</span>' : ''}</button>`).join('')}</div>
<div class="preview"><h3 class="s-k">What you get with “${esc(t.name)}”</h3><ul>${t.gets.map(g => `<li>${g}</li>`).join('')}</ul>${t.ex ? `<p class="ex">Example data: ${t.ex}. Every example task can be edited or deleted.</p>` : ''}</div>
</div>
</div>
<div class="mf"><span class="lim${n >= lim ? ' warn' : ''}"><span class="s-bar"><i style="width:${Math.min(100, n / lim * 100)}%;background:var(--pin-btn)"></i></span>${n} of ${lim} shelves on ${plan()}${S.plan === 'pro' ? '' : ' · <button type="button" class="s-link" data-act="pro">Upgrade for 20</button>'}</span>
<button type="button" class="s-btn" data-act="modal-close">Cancel</button><button type="button" class="s-btn primary" data-act="ns-create">${icon('i-plus', 'sm')}Create shelf</button></div>
${n >= lim ? '<p class="ns-note">Prototype: the Free limit isn’t enforced here, so you can try every template.</p>' : ''}`;
}
function openNewShelf() {
closePop(); closeDrawer(true);
const card = openModal(newShelfModal(), { cls: 'ns-card', label: 'New shelf', focus: '#ns-name', scope: 'pg-room ns-scope' });
card.addEventListener('input', e => { if (e.target.id === 'ns-name') NEW_SHELF.name = e.target.value; });
card.addEventListener('change', e => { if (e.target.id === 'ns-days') NEW_SHELF.days = +e.target.value; });
}
function repaintNewShelf(focusSel) {
if (!MODAL) return;
const card = MODAL.scrim.firstElementChild;
card.innerHTML = newShelfModal();
const f = focusSel && $(focusSel, card); if (f) f.focus();
}
ACT['new-shelf'] = () => { NEW_SHELF.name = ''; openNewShelf(); };
ACT['ns-kind'] = el => { NEW_SHELF.kind = el.dataset.k; NEW_SHELF.tpl = { Course: 'planner', Exam: 'exam', 'Standardized test': 'exam', Project: 'board', Other: 'blank' }[NEW_SHELF.kind]; repaintNewShelf(`[data-act="ns-kind"][data-k="${el.dataset.k}"]`); };
ACT['ns-color'] = el => { NEW_SHELF.color = el.dataset.k; repaintNewShelf(`[data-act="ns-color"][data-k="${el.dataset.k}"]`); };
ACT['ns-tpl'] = el => { NEW_SHELF.tpl = el.dataset.k; repaintNewShelf(`[data-act="ns-tpl"][data-k="${el.dataset.k}"]`); };
ACT['ns-create'] = () => {
const name = (NEW_SHELF.name || '').trim();
if (!name) { const i = $('#ns-name'); i.focus(); i.classList.add('bad'); toast('Give your shelf a name first.'); return; }
const t = TEMPLATES.find(x => x.id === NEW_SHELF.tpl);
const id = 's' + Date.now().toString(36);
const sh = { id, name, color: NEW_SHELF.color, docs: [], tpl: t.name, exam: t.exam ? { title: name.replace(/\s*·.*$/, '') + (/final/i.test(name) ? '' : ' exam'), days: NEW_SHELF.days, time: '' } : null };
if (t.exam && /final|midterm|exam|test|quiz/i.test(name)) sh.exam.title = name.replace(/\s*·\s*/, ' ');
S.shelves.push(sh);
S.tasks[id] = t.tasks.map(([tt, d], i) => ({ id: id + 't' + i, t: tt, st: 'todo', day: t.exam ? Math.min(d, NEW_SHELF.days - 1) : d, doc: null }));
S.tree.shelves = true; S.tree[id] = true;
if (t.view) S.shelfView[id] = t.view;
closeModal(true); save();
go(tokShelf(id));
toast(`Shelf created from “${t.name}”. The example tasks are yours to edit.`);
};
/* ===== 74-docs.js ===== */
/* ================= Documents (Documents hub round 2: smart search, Hoot's match for first visits, atlas, radar) ================= */
const DV = {};
function resetDV() {
Object.assign(DV, {
a: { scope: 'near', cat: null, all: false },
b: { step: 0, type: null, other: false, more: false, q: '' },
c: { mode: 'place', country: null, region: null, city: null, filter: '', touched: false },
d: { r: null, type: 'All', code: '' }
});
}
resetDV(); RESETS.push(resetDV);
/* ---------- shared markup (ported) ---------- */
function logo(s, size) {
const t = size === 'xs' ? s.mini : s.acr, p = schoolPal(s);
return `<span class="logo${size ? ' ' + size : ''}${t.length > 4 ? ' long' : ''}" style="--c:${p.deep};--c2:${p.bright === p.deep ? 'transparent' : p.bright}" aria-hidden="true">${esc(t)}</span>`;
}
function thumbD(type, size) { return `<span class="thumb${size ? ' ' + size : ''} t-${TYPE_TONE[type] || 'mint'}" aria-hidden="true"><i></i><i></i><i></i><i></i></span>`; }
function pinBtn(s) { const on = S.mine === s.id; return `<button type="button" class="pin" data-act="pin-school" data-id="${s.id}" aria-pressed="${on}" aria-label="${on ? esc(s.short) + ' is your school' : 'Set ' + esc(s.short) + ' as my school'}" title="${on ? 'Your school' : 'Set as my school'}">${icon('i-star')}</button>`; }
function schoolCard(s) {
const d = distTxt(s), mine = S.mine === s.id, cs = coursesOf(s);
const tags = cs.slice().sort((a, b) => b.docs - a.docs).slice(0, 3).map(c => `<a class="tag" href="#${tokCourse(s.id, c.code)}">${esc(c.code)}</a>`).join('');
const more = s.courses - Math.min(3, cs.length);
return `<article class="scard${mine ? ' is-mine' : ''}">
<div class="scard-top">${logo(s)}<div class="scard-id">${mine ? `<span class="mine-badge">${icon('i-star', 'sm star-f')}Your school</span>` : ''}<h3><a class="stretch" href="#${tokSchool(s.id)}">${esc(s.name)}</a></h3><p class="meta">${d ? d + ' away · ' : ''}${esc(placeTxt(s))}</p></div>${pinBtn(s)}</div>
<p class="counts"><span><b>${fmt(s.docs)}</b> documents</span><span><b>${fmt(s.courses)}</b> courses</span></p>
<div class="tags">${tags}${more > 0 ? `<span class="more">+${fmt(more)} more</span>` : ''}</div>
</article>`;
}
function courseCard(c) {
return `<a class="ccard" href="#${tokCourse(c.s.id, c.code)}">
<span class="cc-top"><span class="code">${esc(c.code)}</span>${catGlyph(catOfCourse(c), 34)}</span>
<span class="ttl">${esc(c.title)}</span>
<span class="cc-school">${logo(c.s, 'xs')}<span>${esc(c.s.short)}</span></span>
<span class="cc-n">${plural(c.docs, 'document')}</span>
</a>`;
}
function docCardD(d) {
const s = docSchool(d), D = d.id ? DOCS[d.id] : ensureDoc(d);
return `<a class="dcard" href="#${tokDoc(D.id)}">${thumbD(d.type)}<span class="dt"><b>${esc(d.title)}</b><span class="meta">${esc(d.type)} · ${esc(docCode(d))}${s ? ' · ' + esc(s.short) : ''}</span><span class="rate">${icon('i-thumb', 'sm')}${d.rate}% helpful</span></span></a>`;
}
function nearRow(s, extra) {
const mine = S.mine === s.id;
return `<a class="nrow" href="#${tokSchool(s.id)}">${logo(s, 'sm')}<span class="nt"><b>${esc(s.name)}</b><span>${extra || plural(s.docs, 'document')}</span></span>${mine ? icon('i-star', 'sm star-f') : ''}<span class="nd">${distTxt(s)}</span></a>`;
}
function emptyD(title, body) { return `<div class="empty"><b>${title}</b>${body}</div>`; }
const locSelect = id => `<span class="loc-sel">${icon('i-pin', 'sm')}<label for="${id}" class="sr">Your location</label><select id="${id}" data-act-change="loc">${Object.keys(LOCS).map(k => `<option value="${k}" ${S.loc === k ? 'selected' : ''}>Near ${esc(LOCS[k].full)}</option>`).join('')}</select>${icon('i-down', 'sm chev-d')}</span>`;
/* ---------- combobox for the hero and Hoot's "type its name" step (the global search uses the same engine) ---------- */
function makeOmni(input, panel, opts) {
opts = opts || {};
const pid = panel.id, wrap = input.closest('.omni'); let items = [], active = -1;
input.setAttribute('role', 'combobox'); input.setAttribute('aria-autocomplete', 'list'); input.setAttribute('aria-expanded', 'false');
input.setAttribute('aria-controls', pid); input.setAttribute('autocomplete', 'off'); input.setAttribute('spellcheck', 'false');
panel.setAttribute('role', 'listbox'); panel.setAttribute('aria-label', 'Suggestions');
const pickSchool = id => (opts.onSchool ? opts.onSchool(id) : go(tokSchool(id)));
const opt = (it, inner) => { it.i = items.length; items.push(it); return `<div class="opt" role="option" id="${pid}-o${it.i}" data-i="${it.i}" aria-selected="false">${inner}</div>`; };
const group = (label, inner) => inner ? `<div class="og" role="group" aria-label="${esc(label)}"><div class="og-h" aria-hidden="true">${esc(label)}</div>${inner}</div>` : '';
const sOpt = (s, why, cls, q) => opt({ run: () => pickSchool(s.id) }, `${logo(s, 'sm')}<span class="ot"><b>${q ? hl(s.name, q) : esc(s.name)}</b><span>${esc(placeTxt(s))}${distTxt(s) ? ' · ' + distTxt(s) : ''}</span></span>${why ? `<span class="why${cls ? ' ' + cls : ''}">${esc(why)}</span>` : ''}`);
function build() {
items = []; const raw = input.value; const Rz = search(raw); let html = '';
if (!Rz.q) {
const ids = []; if (S.mine) ids.push(S.mine); S.recents.forEach(id => { if (!ids.includes(id) && BY_ID[id]) ids.push(id); });
html += group('Jump back in', ids.slice(0, 3).map(id => sOpt(BY_ID[id], id === S.mine ? 'Your school' : 'Viewed', id === S.mine ? 'mine' : '')).join(''));
html += group('Closest to ' + LOC().label, nearList().filter(s => !ids.includes(s.id)).slice(0, 3).map(s => sOpt(s, 'Nearby', 'near')).join(''));
if (!opts.schoolsOnly) html += group('Try searching', EXAMPLES.map(ex => opt({ keep: true, run: () => { input.value = ex.q; } }, `<span class="oi">${icon(ex.icon)}</span><span class="ot"><b>${esc(ex.q)}</b><span>${esc(ex.note)}</span></span>`)).join(''));
return html;
}
if (Rz.dom !== null) {
html += Rz.email.length ? group('Matched your school email', Rz.email.map(x => sOpt(x.s, '@' + x.m, 'near')).join('')) : `<p class="og-empty">${Rz.dom.includes('.') ? `No school on StudyBoost uses <b>${esc(Rz.dom)}</b> yet.` : 'Keep typing your school email…'}</p>`;
if (Rz.email.length > 1) html += `<p class="og-note">${Rz.email.length} campuses share this email domain. Pick yours.</p>`;
} else {
html += group('Schools', Rz.schools.map(x => sOpt(x.s, x.r.why || (dist(x.s) <= nearMax() ? 'Nearby' : ''), x.r.why ? '' : 'near', Rz.q)).join(''));
const same = Rz.schools.filter(x => x.r.why === 'Acronym');
if (same.length > 1) html += `<p class="og-note">${same.length} schools go by ${esc(same[0].s.acr)}. The one closest to you is first.</p>`;
if (!opts.schoolsOnly) {
html += group(`Courses matching ${Rz.code || ''}`, Rz.courses.map(c => opt({ run: () => go(tokCourse(c.s.id, c.code)) }, `${catGlyph(catOfCourse(c), 32)}<span class="ot"><b>${esc(c.code)} · ${esc(c.title)}</b><span>${esc(c.s.short)} · ${plural(c.docs, 'document')}</span></span>${dist(c.s) <= nearMax() ? '<span class="why near">Nearby</span>' : ''}`)).join(''));
html += group('Documents', Rz.docs.map(d => opt({ run: () => go(tokDoc((d.id ? DOCS[d.id] : ensureDoc(d)).id)) }, `${thumbD(d.type, 'xs')}<span class="ot"><b>${hl(d.title, Rz.q)}</b><span>${esc(d.type)} · ${esc(docWhere(d))}</span></span>`)).join(''));
}
if (!Rz.schools.length && !Rz.courses.length && !Rz.docs.length) html += `<p class="og-empty">Nothing matches “${esc(raw.trim())}” yet.</p>`;
}
let foot = '';
if (!opts.schoolsOnly && Rz.dom === null) foot += opt({ run: () => toast(`Prototype: this would search all 2M+ study guides for “${raw.trim()}”.`) }, `<span class="oi">${icon('i-search')}</span><span class="ot"><b>Search all documents for “${esc(raw.trim())}”</b></span>`);
foot += opt({ run: () => openRequest(Rz.dom !== null ? '' : raw.trim()) }, `<span class="oi">${icon('i-plus')}</span><span class="ot"><b>Can’t find your school? Request it</b><span>We’ll add it and let you know.</span></span>`);
return html + `<div class="og og-foot">${foot}</div>`;
}
function paint(scroll) {
$$('.opt', panel).forEach(el => el.setAttribute('aria-selected', +el.dataset.i === active ? 'true' : 'false'));
const el = active >= 0 ? $(`#${pid}-o${active}`) : null;
if (el) { input.setAttribute('aria-activedescendant', el.id); if (scroll) el.scrollIntoView({ block: 'nearest' }); } else input.removeAttribute('aria-activedescendant');
}
function render() { panel.innerHTML = build(); active = input.value.trim() && items.length ? 0 : -1; paint(false); if (opts.onChange) opts.onChange(input.value); }
function open() { render(); panel.hidden = false; input.setAttribute('aria-expanded', 'true'); }
function close() { panel.hidden = true; input.setAttribute('aria-expanded', 'false'); input.removeAttribute('aria-activedescendant'); }
function choose(it) { if (!it) return; if (it.keep) { it.run(); input.focus(); render(); return; } close(); it.run(); }
input.addEventListener('focus', open);
input.addEventListener('input', () => { if (panel.hidden) open(); else render(); });
input.addEventListener('keydown', e => {
if (e.key === 'ArrowDown') { e.preventDefault(); if (panel.hidden) { open(); return; } active = Math.min(items.length - 1, active + 1); paint(true); }
else if (e.key === 'ArrowUp') { e.preventDefault(); active = Math.max(0, active - 1); paint(true); }
else if (e.key === 'Enter') { e.preventDefault(); if (panel.hidden) { open(); return; } choose(items[active >= 0 ? active : 0]); }
else if (e.key === 'Escape' && !panel.hidden) { e.preventDefault(); e.stopPropagation(); close(); }
});
panel.addEventListener('mousedown', e => e.preventDefault());
panel.addEventListener('click', e => { const o = e.target.closest('.opt'); if (o) choose(items[+o.dataset.i]); });
panel.addEventListener('mousemove', e => { const o = e.target.closest('.opt'); if (o && +o.dataset.i !== active) { active = +o.dataset.i; paint(false); } });
input.addEventListener('blur', () => setTimeout(() => { if (document.activeElement !== input) close(); }, 150));
document.addEventListener('pointerdown', e => { if (wrap && document.contains(wrap) && !wrap.contains(e.target)) close(); });
return { input, close, open, set(v) { input.value = v; input.focus(); open(); }, submit() { if (panel.hidden) open(); if (input.value.trim()) choose(items[active >= 0 ? active : 0]); else input.focus(); } };
}
/* ---------- A · smart search ---------- */
function aHero() {
const ids = []; if (S.mine) ids.push(S.mine); S.recents.forEach(id => { if (!ids.includes(id) && BY_ID[id]) ids.push(id); });
return `<section class="card a-hero" aria-labelledby="a-h">
<div class="a-main">
${ids.length ? `<div class="jump"><span class="jump-l">Jump back in</span>${ids.slice(0, 4).map(id => { const s = BY_ID[id], m = id === S.mine; return `<a class="jchip${m ? ' is-mine' : ''}" href="#${tokSchool(id)}">${logo(s, 'xs')}<span>${esc(s.short)}</span>${m ? icon('i-star', 'sm star-f') : ''}</a>`; }).join('')}</div>` : ''}
<div class="a-title"><h2 id="a-h">Find your school</h2><p>Type its name, a nickname, a course code, or your school email.</p></div>
<form class="omni omni-hero" id="a-omni" role="search">
${icon('i-search')}<label for="a-q" class="sr">Find your school</label><input id="a-q" type="text" placeholder="e.g. University of Miami">
<button type="button" class="omni-clear" id="a-clear" aria-label="Clear search" hidden>${icon('i-x')}</button>
<button type="submit" class="btn btn-primary omni-go">Search</button>
<div class="omni-panel" id="a-panel" hidden></div>
</form>
<div class="try"><span class="try-l">Try</span>${EXAMPLES.map(ex => `<button type="button" class="try-chip" data-act="a-try" data-val="${esc(ex.q)}">${icon(ex.icon, 'sm')}${esc(ex.q)}</button>`).join('')}</div>
<div class="a-ways"><span class="try-l">Other ways in</span><a class="way" href="#documents-atlas">${icon('i-globe', 'sm')}Browse by country</a><a class="way" href="#documents-radar">${icon('i-target', 'sm')}See schools on a map</a><button type="button" class="way" data-act="request">${icon('i-plus', 'sm')}Request your school</button></div>
</div>
<aside class="a-side" aria-labelledby="a-near-h">
<div class="a-side-h"><h3 id="a-near-h">Closest to you</h3>${locSelect('a-loc')}</div>
<div class="near-rows">${nearList().slice(0, 4).map(s => nearRow(s)).join('') || '<p class="hint">No schools near here in this preview.</p>'}</div>
<p class="stats-line"><b>2M+</b> study guides and notes · <b>27,000</b> schools · <b>200</b> countries</p>
</aside>
</section>`;
}
function aSections() {
const L = LOC(), near = nearList(), shown = DV.a.all ? near : near.slice(0, 6);
const list = topCourses(DV.a.scope, DV.a.cat), take = list.length >= 6 ? 6 : list.length >= 3 ? 3 : list.length;
return `<section class="sec" aria-labelledby="a-schools-h">
<div class="sh"><div><h2 id="a-schools-h">Schools near ${esc(L.label)}</h2><p>${plural(near.length, 'school')} within ${nearMax()} ${L.unit}, closest first.</p></div><a class="sh-link" href="#documents-atlas">Browse every school${icon('i-arrow', 'sm')}</a></div>
<div class="${gridFor(shown.length)}">${shown.map(schoolCard).join('')}</div>
${near.length > 6 ? `<div class="center-row"><button type="button" class="btn btn-ghost" data-act="a-more">${DV.a.all ? 'Show fewer' : `Show all ${near.length} schools`}</button></div>` : ''}
</section>
<section class="sec" aria-labelledby="a-subj-h">
<div class="sh"><div><h2 id="a-subj-h">Browse by subject</h2><p>Don’t know the course code? Start from what you study.</p></div></div>
<div class="subjects">${CATS.map(c => `<button type="button" class="subj" data-act="a-cat" data-val="${c.id}" aria-pressed="${DV.a.cat === c.id}">${catGlyph(c.id, 38)}<span>${esc(c.name)}</span></button>`).join('')}</div>
</section>
<section class="sec" id="a-top-sec" aria-labelledby="a-top-h">
<div class="sh"><div><h2 id="a-top-h">Top courses right now</h2><p>${DV.a.scope === 'near' ? `Most documents at schools near ${esc(L.label)}.` : 'Most documents across every school on StudyBoost.'}</p></div>
<div class="seg" role="group" aria-label="Ranking scope"><button type="button" data-act="a-scope" data-val="near" aria-pressed="${DV.a.scope === 'near'}">Near you</button><button type="button" data-act="a-scope" data-val="all" aria-pressed="${DV.a.scope === 'all'}">Everywhere</button></div></div>
${DV.a.cat ? `<div><button type="button" class="filter-on" data-act="a-cat" data-val="${DV.a.cat}" aria-label="Remove filter ${esc(CAT(DV.a.cat).name)}">${esc(CAT(DV.a.cat).name)}${icon('i-x', 'sm')}</button></div>` : ''}
${list.length ? `<div class="${gridFor(take)}">${list.slice(0, take).map(courseCard).join('')}</div>` : emptyD(`No ${esc(CAT(DV.a.cat).name)} courses near ${esc(L.label)} yet.`, `<span>Try <button type="button" class="link" data-act="a-scope" data-val="all">everywhere</button>, or clear the subject filter.</span>`)}
</section>
<section class="sec" aria-labelledby="a-trend-h">
<div class="sh"><div><h2 id="a-trend-h">Trending documents</h2><p>What students are opening this week.</p></div></div>
<div class="grid4">${trendingDocs().map(docCardD).join('')}</div>
</section>`;
}
function trendingDocs() {
// the four from the Documents hub, with this term's BIL 150 study guide first for students near Miami
const um = LOC().label === 'Miami' ? [hubDoc(DOCS['exam2-guide'] || courseDocs('um', 'BIL 150')[0])] : [];
return um.concat(TRENDING).slice(0, 4);
}
/* ---------- B · Mr. Hoot's 3-tap match (first visits) ---------- */
function candidates() {
const t = DV.b.type;
if (t === 'Online') return SCHOOLS.filter(s => s.online).sort((a, b) => b.docs - a.docs);
if (t === 'High school') return [];
return nearList(s => s.type === t);
}
function hootHtml() {
const L = LOC(), st = DV.b; let say = '', small = '', body = '', n = 1;
if (st.step === 0) {
say = 'Hi, I’m Mr. Hoot. Let’s find your school.'; small = `It looks like you’re near ${L.full}. Is that right?`;
body = `<div class="choice-row"><button type="button" class="btn btn-primary lg" data-act="b-yes">${icon('i-check')}Yes, near ${esc(L.label)}</button><button type="button" class="btn btn-ghost lg" data-act="b-other" aria-expanded="${st.other}">Somewhere else</button><button type="button" class="btn btn-quiet lg" data-act="b-online">${icon('i-laptop')}I study online</button></div>`;
const q3 = nearList().slice(0, 3);
if (!st.other && q3.length) body += `<div class="quick"><p class="field-label">Already see it? Tap your school</p><div class="chips">${q3.map(s => `<button type="button" class="chip" data-act="b-pick" data-id="${s.id}">${logo(s, 'xs')}${esc(s.short)}<span class="meta">${distTxt(s)}</span></button>`).join('')}</div></div>`;
if (st.other) body += `<div class="loc-box"><p class="field-label">Where do you study?</p><div class="chips">${Object.keys(LOCS).filter(k => k !== S.loc).map(k => `<button type="button" class="chip" data-act="b-loc" data-val="${k}">${icon('i-pin', 'sm')}${esc(LOCS[k].full)}</button>`).join('')}</div><p class="hint">Somewhere else? <button type="button" class="link" data-act="b-typeit">Type your school’s name</button></p></div>`;
} else if (st.step === 1) {
n = 2; say = 'Great. What kind of school is it?'; small = `Schools near ${L.full}`;
body = `<div class="choice-grid">${KINDS.map(k => `<button type="button" class="choice" data-act="b-type" data-val="${k.v}"><span class="cat-ic tone-${k.tone}">${icon(k.icon)}</span><span class="ct"><b>${k.label}</b><span>${k.note}</span></span></button>`).join('')}</div>`;
} else if (st.step === 2) {
n = 3; const list = candidates(); const shown = st.more ? list : list.slice(0, 4);
if (list.length) { say = 'Is it one of these?'; small = st.type === 'Online' ? 'Online schools you can join from anywhere' : `${st.type === 'University' ? 'Universities' : 'Colleges'} closest to ${L.label}`; }
else { say = st.type === 'High school' ? 'High schools are coming soon.' : `I couldn’t find a ${st.type.toLowerCase()} near ${L.label}.`; small = 'You can type its name, or ask us to add it.'; }
body = list.length ? `<div class="pick-list">${shown.map(s => `<button type="button" class="pick" data-act="b-pick" data-id="${s.id}">${logo(s)}<span class="pt"><b>${esc(s.name)}</b><span>${fmt(s.docs)} documents · ${fmt(s.courses)} courses</span></span><span class="pd">${st.type === 'Online' ? 'Online' : distTxt(s)}</span></button>`).join('')}</div>${list.length > 4 && !st.more ? `<button type="button" class="link" data-act="b-more" style="width:fit-content">Show ${list.length - 4} more</button>` : ''}` : '';
body += `<p class="hint">Not here? <button type="button" class="link" data-act="b-typeit">Type its name</button> or <button type="button" class="link" data-act="request">ask us to add it</button>.</p>`;
} else {
n = 3; say = 'No problem. What’s it called?'; small = 'Names, nicknames, acronyms and old names all work.';
body = `<div class="omni omni-sm" id="b-omni">${icon('i-search')}<label for="b-q" class="sr">School name</label><input id="b-q" type="text" placeholder="Start typing your school’s name"><div class="omni-panel" id="b-panel" hidden></div></div><p class="hint">Still missing? <button type="button" class="link" data-act="request">Request your school</button></p>`;
}
const top = `<div class="hc-top"><span>Step ${n} of 3</span><span class="dots" aria-hidden="true">${[1, 2, 3].map(i => `<i class="${i <= n ? 'on' : ''}"></i>`).join('')}</span></div>`;
const foot = `<div class="hc-foot">${st.step > 0 ? `<button type="button" class="link" data-act="b-back">Back</button>` : ''}<button type="button" class="link" data-act="b-skip">Skip and browse</button></div>`;
return `<section class="card hoot-card" aria-label="Find your school with Mr. Hoot">${top}<div class="say"><span class="hoot-av"><svg viewBox="0 0 64 64" aria-hidden="true"><use href="#hoot64"/></svg></span><p class="bubble" aria-live="polite">${esc(say)}<small>${esc(small)}</small></p></div>${body}${foot}</section>`;
}
function insideHtml() {
const T = [['i-doc', 'Lecture notes', 'Class notes organized by course and week.'], ['i-exam', 'Past exams', 'Old midterms and finals to practice with.'], ['i-book', 'Study guides', 'Reviewers and summaries before exams.'], ['i-pencil', 'Practice problems', 'Worked problem sets with solutions.']];
return `<section class="ink-card" aria-label="What you’ll find inside"><h2>What you’ll find inside</h2><div class="types">${T.map(t => `<div>${icon(t[0])}<b>${t[1]}</b><span>${t[2]}</span></div>`).join('')}</div><p>Over 2 million study guides and notes, shared by students at 27,000 schools in 200 countries.</p></section>`;
}
/* ---------- C · campus atlas ---------- */
function atlasDefaults() { if (DV.c.country === null) { const L = LOC(); Object.assign(DV.c, { country: L.country, region: L.region, city: L.city }); } }
function placeRow(level, v, badge, list, cur) {
return `<button type="button" class="arow" data-act="c-pick" data-level="${level}" data-val="${esc(v)}" data-key="${level}:${esc(v)}" aria-current="${cur}">${badge ? `<span class="cc">${badge}</span>` : ''}<span class="an">${esc(v)}<small>${plural(list.length, 'school')} · ${compact(sumDocs(list))} docs</small></span>${icon('i-chev-r', 'sm chev')}</button>`;
}
function atlasSchoolRow(s) {
return `<a class="arow arow-s" href="#${tokSchool(s.id)}" data-key="school:${s.id}">${logo(s, 'sm')}<span class="an"><b>${esc(s.name)}</b><small>${fmt(s.docs)} docs${distTxt(s) ? ' · ' + distTxt(s) : ''}</small></span>${S.mine === s.id ? icon('i-star', 'sm star-f') : ''}</a>`;
}
function placeHtml() {
atlasDefaults(); const c = DV.c, cols = [];
const countries = Object.keys(TREE).sort((a, b) => schoolsIn(b).length - schoolsIn(a).length);
cols.push({ title: 'Country', rows: countries.map(n => placeRow('country', n, CC[n], schoolsIn(n), c.country === n)).join('') + `<button type="button" class="arow arow-more" data-act="toast" data-msg="Prototype: the live directory lists all 200 countries.">${icon('i-globe', 'sm')}<span class="an">196 more countries</span></button>` });
if (c.country) cols.push({ title: 'Region in ' + c.country, back: ['all', 'countries'], rows: Object.keys(TREE[c.country]).sort((a, b) => schoolsIn(c.country, b).length - schoolsIn(c.country, a).length).map(r => placeRow('region', r, null, schoolsIn(c.country, r), c.region === r)).join('') });
if (c.region) cols.push({ title: 'City in ' + c.region, back: ['country', c.country], rows: Object.keys(TREE[c.country][c.region]).sort((a, b) => schoolsIn(c.country, c.region, b).length - schoolsIn(c.country, c.region, a).length).map(ct => placeRow('city', ct, null, schoolsIn(c.country, c.region, ct), c.city === ct)).join('') });
if (c.city) cols.push({ title: 'Schools in ' + c.city, back: ['region', c.region], rows: schoolsIn(c.country, c.region, c.city).sort((a, b) => dist(a) - dist(b) || b.docs - a.docs).map(atlasSchoolRow).join('') });
const hints = ['', 'Pick a country to see its regions.', 'Pick a region to see its cities.', 'Pick a city to see its schools.'];
let html = cols.map((col, i) => `<div class="acol${i === cols.length - 1 ? ' cur' : i === cols.length - 2 ? ' prev' : ''}" data-col="${i}" role="group" aria-label="${esc(col.title)}"><div class="acol-h">${col.back ? `<button type="button" class="icon-btn sm acol-back" data-act="c-crumb" data-val="${col.back[0]}" aria-label="Back to ${esc(col.back[1])}">${icon('i-chev-l')}</button>` : ''}<span>${esc(col.title)}</span></div><div class="acol-list">${col.rows}</div></div>`).join('');
for (let i = cols.length; i < 4; i++) html += `<div class="acol acol-empty${i === cols.length && cols.length === 1 ? ' next' : ''}"><p>${hints[i]}</p></div>`;
const parts = [`<button type="button" data-act="c-crumb" data-val="all">All countries</button>`];
[['country', c.country], ['region', c.region], ['city', c.city]].forEach(([lv, v], i, arr) => { if (!v) return; const last = !arr[i + 1] || !arr[i + 1][1]; parts.push(icon('i-chev-r', 'sm') + (last ? `<span class="here">${esc(v)}</span>` : `<button type="button" data-act="c-crumb" data-val="${lv}">${esc(v)}</button>`)); });
return `<nav class="acrumbs" aria-label="Location">${parts.join('')}</nav><div class="acols" id="c-place">${html}</div>`;
}
function azHtml() {
const f = norm(DV.c.filter);
const list = SCHOOLS.filter(s => !f || norm(s.name + ' ' + s.acr + ' ' + s.city + ' ' + s.aliases.map(a => a.t).join(' ')).includes(f)).sort((a, b) => a.name.replace(/^The /, '').localeCompare(b.name.replace(/^The /, '')));
const groups = {}; list.forEach(s => { const Lt = s.name.replace(/^The /, '')[0].toUpperCase(); (groups[Lt] = groups[Lt] || []).push(s); });
return `<div class="az-rail" role="group" aria-label="Jump to letter">${'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('').map(Lt => `<button type="button" data-act="c-letter" data-val="${Lt}" ${groups[Lt] ? '' : 'disabled'} aria-label="Jump to ${Lt}">${Lt}</button>`).join('')}</div>
<div class="az-list" id="c-list">${list.length ? Object.keys(groups).sort().map(Lt => `<div class="az-g" id="az-${Lt}"><h3 class="az-l">${Lt}</h3><div class="az-rows">${groups[Lt].map(atlasSchoolRow).join('')}</div></div>`).join('') : `<div style="padding:16px">${emptyD(`No school matches “${esc(DV.c.filter)}”.`, `<button type="button" class="link" data-act="request" data-val="${esc(DV.c.filter)}">Request it</button>`)}</div>`}</div>`;
}
function atlasView() {
const place = DV.c.mode === 'place';
const rec = (S.mine ? [S.mine] : []).concat(S.recents.filter(id => id !== S.mine)).filter(id => BY_ID[id]).slice(0, 6).map(id => BY_ID[id]);
return `<section class="card atlas" id="c-atlas" aria-labelledby="c-h">
<div class="atlas-top"><div><h2 id="c-h">Find your school by place</h2><p>27,000 schools in 200 countries. Pick a country, then narrow it down.</p></div>
<div class="atlas-tools"><div class="seg" role="group" aria-label="Browse mode"><button type="button" data-act="c-mode" data-val="place" aria-pressed="${place}">By place</button><button type="button" data-act="c-mode" data-val="az" aria-pressed="${!place}">A–Z</button></div>
<button type="button" class="btn btn-ghost" data-act="c-locate">${icon('i-target')}Use my location</button></div></div>
<div id="c-body">${place ? placeHtml() : `<div class="az-tools"><div class="omni omni-sm">${icon('i-search')}<label for="c-filter" class="sr">Filter schools</label><input id="c-filter" type="text" placeholder="Filter schools by name, acronym or city" autocomplete="off" value="${esc(DV.c.filter)}"></div></div><div id="c-az">${azHtml()}</div>`}</div>
<p class="atlas-foot">${place ? 'Tip: use <span class="kbd">↑</span> <span class="kbd">↓</span> to move and <span class="kbd">→</span> to open the next column.' : `Showing ${SCHOOLS.length} sample schools. The live directory lists all 27,000.`}</p>
</section>
<div class="c-lower">
<section class="card c-recent" aria-labelledby="c-rec-h"><h2 id="c-rec-h">Recently viewed</h2>${rec.length ? `<div class="rec-grid">${rec.map(s => nearRow(s, s.id === S.mine ? 'Your school' : placeTxt(s))).join('')}</div>` : '<p class="hint">Schools you open will show up here, so you can get back in one tap.</p>'}</section>
<section class="ink-card c-req" aria-labelledby="c-req-h"><h2 id="c-req-h">Can’t find your school?</h2><p>Tell us where you study. We’ll add it and let you know when the first documents arrive.</p>
<form id="c-req-form" style="display:flex;flex-direction:column;gap:12px"><div class="field"><label for="c-req-school">School name</label><input id="c-req-school" type="text" placeholder="e.g. Cebu Normal University" required></div><div class="field"><label for="c-req-city">City and country</label><input id="c-req-city" type="text" placeholder="e.g. Cebu City, Philippines" required></div><button type="submit" class="btn btn-primary">Request my school</button><p class="ok-msg" id="c-req-ok" role="status" hidden></p></form></section>
</div>
<section class="card ranked" aria-labelledby="c-top-h"><div class="sh"><div><h2 id="c-top-h">Top courses right now</h2><p>Most documents across every school.</p></div></div>
<ol>${topCourses('all').slice(0, 6).map((c, i) => `<li><a class="rk-row" href="#${tokCourse(c.s.id, c.code)}"><span class="rk-n">${i + 1}</span>${catGlyph(catOfCourse(c), 34)}<span class="rk-t"><b>${esc(c.code)} · ${esc(c.title)}</b><span>${esc(c.s.name)}</span></span><span class="rk-d">${fmt(c.docs)} docs</span></a></li>`).join('')}</ol></section>`;
}
function atlasPick(btn, viaKey) {
const lv = btn.dataset.level, v = btn.dataset.val, k = btn.dataset.key; DV.c.touched = true;
if (lv === 'country' && DV.c.country !== v) Object.assign(DV.c, { country: v, region: null, city: null });
if (lv === 'region' && DV.c.region !== v) Object.assign(DV.c, { region: v, city: null });
if (lv === 'city') DV.c.city = v;
$('#c-body').innerHTML = placeHtml();
const narrow = matchMedia('(max-width: 760px)').matches;
if (narrow || viaKey === 'right') { const nx = $('#c-place .acol.cur .arow'); if (nx) nx.focus(); }
else { const t = $(`#c-place [data-key="${CSS.escape(k)}"]`); if (t) t.focus(); }
}
/* ---------- D · around you ---------- */
const curR = () => { const L = LOC(); return DV.d.r == null ? L.def : Math.min(DV.d.r, L.radii.length - 1); };
function typeOk(s) { const t = DV.d.type; return t === 'All' || (t === 'Online' ? !!s.online : s.type === t); }
function relax(pts) {
for (let it = 0; it < 160; it++) {
let moved = false;
for (let i = 0; i < pts.length; i++) for (let j = i + 1; j < pts.length; j++) {
const a = pts[i], b = pts[j]; let dx = b.x - a.x, dy = b.y - a.y, dd = Math.hypot(dx, dy); const min = (a.w + b.w) / 2 + 1;
if (dd < min) { if (dd < 0.01) { dx = Math.cos(i + j * 2.3); dy = Math.sin(i + j * 2.3); dd = 1; } const push = (min - dd) / 2, ux = dx / dd, uy = dy / dd; a.x -= ux * push; a.y -= uy * push; b.x += ux * push; b.y += uy * push; moved = true; }
}
pts.forEach(p => { const dx = p.x - 50, dy = p.y - 50, r = Math.hypot(dx, dy) || 0.01, minR = 5 + p.w / 2, maxR = 46 - p.w / 2; if (r < minR) { p.x = 50 + dx / r * minR; p.y = 50 + dy / r * minR; } if (r > maxR) { p.x = 50 + dx / r * maxR; p.y = 50 + dy / r * maxR; } });
if (!moved) break;
}
}
function sweep() { const r = $('#d-radar'); if (!r || reduceMotion()) return; r.classList.remove('sweep'); void r.offsetWidth; r.classList.add('sweep'); }
function radarInner() {
const L = LOC(), ri = curR(), R0 = L.radii[ri], online = DV.d.type === 'Online';
const pool = online ? SCHOOLS.filter(s => s.online).sort((a, b) => b.docs - a.docs) : SCHOOLS.filter(typeOk).sort((a, b) => dist(a) - dist(b));
const inR = online ? pool : pool.filter(s => dist(s) <= R0);
const beyond = online ? [] : pool.filter(s => dist(s) > R0 && dist(s) <= nearMax());
const count = online ? 'Online schools you can join from anywhere' : inR.length ? `${plural(inR.length, 'school')} within ${R0} ${L.unit}` : `No ${DV.d.type === 'All' ? '' : DV.d.type.toLowerCase() + ' '}schools within ${R0} ${L.unit}`;
const listHtml = inR.map(s => { const d = dist(s); return `<li><a class="dl-row" href="#${tokSchool(s.id)}" data-hot="${s.id}"><span class="dl-d">${online ? 'Web' : (d < 10 ? d.toFixed(1) : Math.round(d))}<small>${online ? 'online' : L.unit}</small></span>${logo(s, 'sm')}<span class="nt"><b>${esc(s.name)}</b><span>${fmt(s.docs)} documents · ${fmt(s.courses)} courses</span></span>${S.mine === s.id ? icon('i-star', 'sm star-f') : ''}</a></li>`; }).join('') || `<li class="hint" style="padding:8px 10px">${DV.d.type === 'College' ? 'No colleges nearby in this preview.' : 'Try a wider distance.'}</li>`;
const rings = [1 / 3, 2 / 3, 1].map(f => { const v = R0 * f; return `<div class="r-ring" style="width:${f * 88}%;height:${f * 88}%"><b>${(v < 10 ? +v.toFixed(1) : Math.round(v))} ${L.unit}</b></div>`; }).join('');
let dots = '', msg = '';
if (online) msg = `<p class="r-msg">Online schools don’t have a spot on the map. They’re listed on the right.</p>`;
else if (!inR.length) msg = `<p class="r-msg">Nothing within ${R0} ${L.unit}. Try a wider distance.</p>`;
else {
const logs = inR.map(s => Math.log10(s.docs + 1)); const lo = Math.min(...logs), hi = Math.max(...logs);
const pts = inR.map((s, i) => { const d = dist(s), b = bearing(L, s), r = Math.max(5, d / R0 * 44); const k = 6.4 + (hi > lo ? (logs[i] - lo) / (hi - lo) : 0.5) * 2.4; return { s, k, w: Math.max(k, s.acr.length * 1.35 + 2.2), x: 50 + r * Math.sin(b), y: 50 - r * Math.cos(b) }; });
relax(pts);
dots = pts.map(p => { const pc = schoolPal(p.s); return `<a class="rdot${S.mine === p.s.id ? ' is-mine' : ''}" href="#${tokSchool(p.s.id)}" style="left:${p.x.toFixed(2)}%;top:${p.y.toFixed(2)}%;--c:${pc.deep};--k:${p.k.toFixed(2)}" data-hot="${p.s.id}" data-tip="${esc(p.s.short)} · ${distTxt(p.s)}" aria-label="${esc(p.s.name)}, ${distTxt(p.s)} away">${esc(p.s.acr)}</a>`; }).join('');
}
return { count, listHtml, more: beyond.length && ri < L.radii.length - 1 ? `<button type="button" class="btn btn-ghost sm" data-act="d-wider">${plural(beyond.length, 'more school')} within ${L.radii[L.radii.length - 1]} ${L.unit}</button>` : '', radar: `<div class="r-disc"></div><div class="r-axis"></div><div class="r-axis h"></div>${rings}<div class="r-sweep"></div><div class="r-you"><span></span><b>You</b></div>${dots}${msg}` };
}
function codeRow(c) { return `<a class="code-row" href="#${tokCourse(c.s.id, c.code)}">${logo(c.s, 'sm')}<span class="ot"><b>${esc(c.code)} · ${esc(c.title)}</b><span>${esc(c.s.short)} · ${distTxt(c.s) || esc(placeTxt(c.s))}</span></span><span class="nd">${fmt(c.docs)} docs</span></a>`; }
function codeRes() {
const q = DV.d.code.trim(), L = LOC();
if (!q) { const ex = Array.from(new Set(topCourses('near').map(c => c.code))).slice(0, 3); return `<div class="try"><span class="try-l">Popular near ${esc(L.label)}</span>${ex.map(c => `<button type="button" class="try-chip" data-act="d-code-ex" data-val="${esc(c)}">${icon('i-hash', 'sm')}${esc(c)}</button>`).join('')}</div>`; }
const qk = key(q); const m = ALL_COURSES.filter(c => key(c.code).startsWith(qk));
if (!m.length) return `<p class="hint">No course code starts with “${esc(q)}” yet. <button type="button" class="link" data-act="request">Request your school</button></p>`;
const near = m.filter(c => dist(c.s) <= nearMax()).sort((a, b) => dist(a.s) - dist(b.s)).slice(0, 8);
const far = m.filter(c => dist(c.s) > nearMax()).sort((a, b) => b.docs - a.docs).slice(0, 4);
return (near.length ? `<p class="res-h">Taught at ${plural(near.length, 'school')} around ${esc(L.label)}</p><div class="code-grid">${near.map(codeRow).join('')}</div>` : `<p class="res-h">Not taught near ${esc(L.label)} yet</p>`) + (far.length ? `<p class="res-h">Elsewhere</p><div class="code-grid">${far.map(codeRow).join('')}</div>` : '');
}
function radarView() {
const L = LOC(), ri = curR(), x = radarInner();
const near = nearList().slice(0, 6);
return `<div class="d-band">
<section class="card radar-card" id="d-radar-card" aria-labelledby="d-h">
<div class="radar-h"><div><h2 id="d-h">Schools around you</h2><p class="loc-line">${locSelect('d-loc')}<button type="button" class="link" data-act="toast" data-msg="Prototype: this would ask your browser for your exact location. The preview uses ${esc(L.full)}.">Use precise location</button></p></div>
<div class="seg" role="group" aria-label="Show schools within">${L.radii.map((r, i) => `<button type="button" data-act="d-r" data-val="${i}" aria-pressed="${i === ri}">${r} ${L.unit}</button>`).join('')}</div></div>
<div class="radar-wrap"><div class="radar" id="d-radar">${x.radar}</div></div>
<p class="radar-note">Positions are approximate. Bigger dots have more documents.</p>
</section>
<section class="card d-list-card" aria-labelledby="d-count"><h3 id="d-count">${x.count}</h3>
<div class="chips" role="group" aria-label="School type">${['All', 'University', 'College', 'Online'].map(t => `<button type="button" class="chip" data-act="d-type" data-val="${t}" aria-pressed="${DV.d.type === t}">${t === 'All' ? 'All schools' : t}</button>`).join('')}</div>
<ol class="d-list" id="d-list">${x.listHtml}</ol><div>${x.more}</div></section>
</div>
<section class="card d-code" aria-labelledby="d-code-h"><div><h2 id="d-code-h">Know your course code?</h2><p>Type it from your syllabus. We’ll show which schools around you teach it.</p></div>
<div class="omni omni-sm">${icon('i-hash')}<label for="d-code-q" class="sr">Course code</label><input id="d-code-q" type="text" placeholder="e.g. BIL 150" autocomplete="off" spellcheck="false" value="${esc(DV.d.code)}"></div>
<div class="d-code-res" id="d-code-res" aria-live="polite">${codeRes()}</div></section>
<div class="d-lower">
<section class="sec" aria-labelledby="d-feed-h"><div class="sh"><div><h2 id="d-feed-h">Just added around you</h2><p>New uploads at schools near ${esc(L.label)}.</p></div></div>
<ol class="feed">${near.map((s, i) => { const cs = coursesOf(s), c = cs[i % cs.length], d = courseDocs(s.id, c.code)[0]; return `<li><a class="fd" href="#${tokDoc(d.id)}"><span class="fd-time">${AGO[i]}</span>${thumbD(HUB_TO_DH[d.hub], 'sm')}<span class="dt"><b>${esc(d.title)}</b><span class="meta">${esc(d.type)} · ${esc(s.short)}</span></span></a></li>`; }).join('')}</ol></section>
<section class="sec" aria-labelledby="d-pop-h"><div class="sh"><div><h2 id="d-pop-h">Popular courses around you</h2><p>Most documents at nearby schools.</p></div></div>
<div class="grid2">${topCourses('near').slice(0, 4).map(courseCard).join('')}</div></section>
</div>`;
}
/* ---------- the page ---------- */
const DTABS = [['search', 'documents', 'i-search', 'Search'], ['atlas', 'documents-atlas', 'i-globe', 'Browse by place'], ['radar', 'documents-radar', 'i-target', 'Around you']];
PAGES.docs = {
cls: () => 'pg-docs',
render(r) {
const v = r.view;
let body = '';
if (v === 'search') body = (S.mine ? aHero() : `<div class="b-band" id="b-band"><div class="b-left" id="b-left">${hootHtml()}</div><div class="b-right">${insideHtml()}</div></div>`) + aSections();
else if (v === 'atlas') body = atlasView();
else body = radarView();
return `<div class="page-in">${crumbs([['documents', 'Documents']].concat(v !== 'search' ? [[TOKEN, v === 'atlas' ? 'Browse by place' : 'Around you']] : []))}
<div class="page-head"><h1>Study notes, guides and past exams for your courses</h1><p>Find your school and course, then study the documents students shared, with AI tools.</p></div>
<nav class="dtabs" aria-label="Ways to find your school">${DTABS.map(([k, t, ic, l]) => `<a href="#${t}" ${k === v ? 'aria-current="page"' : ''}>${icon(ic, 'sm')}${l}</a>`).join('')}</nav>
<div class="view">${body}</div></div>`;
},
mount(r, page) {
if (r.view === 'search') {
if (S.mine) {
const om = makeOmni($('#a-q'), $('#a-panel'));
PAGES.docs.omni = om;
$('#a-omni').addEventListener('submit', e => { e.preventDefault(); om.submit(); });
$('#a-q').addEventListener('input', () => { $('#a-clear').hidden = !$('#a-q').value; });
$('#a-clear').addEventListener('click', () => { $('#a-q').value = ''; $('#a-clear').hidden = true; om.set(''); });
} else if (DV.b.step === 3) {
makeOmni($('#b-q'), $('#b-panel'), { schoolsOnly: true, onSchool: id => setMine(id) });
}
}
if (r.view === 'atlas') {
const f = $('#c-req-form');
f.addEventListener('submit', e => { e.preventDefault(); const ok = $('#c-req-ok'); const name = $('#c-req-school').value.trim(); ok.innerHTML = icon('i-check') + `<span>Thanks. We’ll let you know when ${esc(name)} is on StudyBoost.</span>`; ok.hidden = false; e.target.reset(); });
wireAtlas();
}
if (r.view === 'radar') {
sweep();
$('#d-code-q').addEventListener('input', e => { DV.d.code = e.target.value; $('#d-code-res').innerHTML = codeRes(); });
const view = $('.view', page);
const setHot = id => $$('[data-hot]', view).forEach(el => el.classList.toggle('is-hot', !!id && el.dataset.hot === id));
['mouseover', 'focusin'].forEach(ev => view.addEventListener(ev, e => { const h = e.target.closest('[data-hot]'); setHot(h ? h.dataset.hot : null); }));
view.addEventListener('mouseleave', () => setHot(null));
}
}
};
function wireAtlas() {
const box = $('#c-atlas'); if (!box) return;
box.addEventListener('keydown', e => {
const row = e.target.closest('#c-place .arow'); if (!row) return;
const col = row.closest('.acol'), rows = $$('.arow', col), i = rows.indexOf(row);
if (e.key === 'ArrowDown') { e.preventDefault(); (rows[i + 1] || row).focus(); }
else if (e.key === 'ArrowUp') { e.preventDefault(); (rows[i - 1] || row).focus(); }
else if (e.key === 'ArrowRight') { e.preventDefault(); if (row.dataset.act === 'c-pick') atlasPick(row, 'right'); else if (row.getAttribute('href')) go(row.getAttribute('href').slice(1)); }
else if (e.key === 'ArrowLeft') { e.preventDefault(); const prev = $(`#c-place .acol[data-col="${+col.dataset.col - 1}"]`); if (prev) { const cur = $('.arow[aria-current="true"]', prev) || $('.arow', prev); if (cur) cur.focus(); } }
});
box.addEventListener('input', e => { if (e.target.id === 'c-filter') { DV.c.filter = e.target.value; $('#c-az').innerHTML = azHtml(); } });
}
function setMine(id, quiet) {
S.mine = id; if (id) S.recents = [id].concat(S.recents.filter(x => x !== id)).slice(0, 6);
DV.b.step = 0; save();
rerender();
if (!quiet) toast(id ? `Saved. ${BY_ID[id].short} is your school now.` : 'Removed your school.');
}
function docsRerender(sel) { const p = $('#page'), y = p.scrollTop; const a = document.activeElement; const s = a && a.dataset && a.dataset.act ? `[data-act="${a.dataset.act}"]${a.dataset.val ? `[data-val="${CSS.escape(a.dataset.val)}"]` : ''}${a.dataset.id ? `[data-id="${a.dataset.id}"]` : ''}` : null; render(); $('#page').scrollTop = y; const t = sel ? $(sel) : (s ? $(s, $('#page')) : null); if (t) t.focus({ preventScroll: true }); save(); }
Object.assign(ACT, {
'request': el => openRequest(el.dataset.val || ''),
'pin-school': el => { const id = el.dataset.id; setMine(S.mine === id ? null : id); const b = $(`[data-act="pin-school"][data-id="${id}"]`); if (b) b.focus({ preventScroll: true }); },
'a-try': el => { const om = PAGES.docs.omni; if (om) { om.set(el.dataset.val); $('#a-clear').hidden = false; } },
'a-more': () => { DV.a.all = !DV.a.all; docsRerender(); },
'a-scope': el => { DV.a.scope = el.dataset.val; docsRerender(); },
'a-cat': el => { const v = +el.dataset.val; DV.a.cat = DV.a.cat === v ? null : v; docsRerender(); if (DV.a.cat && el.classList.contains('subj')) { const s = $('#a-top-sec'); if (s) s.scrollIntoView({ behavior: reduceMotion() ? 'auto' : 'smooth', block: 'start' }); } },
'b-yes': () => { DV.b.step = 1; DV.b.other = false; docsRerender('#b-left .choice'); },
'b-other': () => { DV.b.other = !DV.b.other; docsRerender(); },
'b-online': () => { DV.b.type = 'Online'; DV.b.step = 2; DV.b.more = false; docsRerender('#b-left .pick'); },
'b-loc': el => { DV.b.other = false; DV.b.step = 1; S.loc = el.dataset.val; DV.c.country = null; DV.d.r = null; save(); docsRerender('#b-left .choice'); toast('Showing schools near ' + LOC().full); },
'b-type': el => { DV.b.type = el.dataset.val; DV.b.step = 2; DV.b.more = false; docsRerender('#b-left .pick, #b-left .link'); },
'b-more': () => { DV.b.more = true; docsRerender(); },
'b-typeit': () => { DV.b.step = 3; DV.b.other = false; docsRerender('#b-q'); },
'b-back': () => { DV.b.step = DV.b.step === 3 ? (DV.b.type ? 2 : 0) : Math.max(0, DV.b.step - 1); docsRerender('#b-left button'); },
'b-pick': el => setMine(el.dataset.id),
'b-skip': () => { const s = $('#a-schools-h'); if (s) s.scrollIntoView({ behavior: reduceMotion() ? 'auto' : 'smooth', block: 'start' }); },
'c-mode': el => { DV.c.mode = el.dataset.val; docsRerender(); },
'c-pick': (el, e) => atlasPick(el, e && e.detail === 0 ? 'key' : null),
'c-crumb': el => { const lv = el.dataset.val; DV.c.touched = true; if (lv === 'all') Object.assign(DV.c, { country: '', region: null, city: null }); if (lv === 'country') Object.assign(DV.c, { region: null, city: null }); if (lv === 'region') DV.c.city = null; $('#c-body').innerHTML = placeHtml(); const f = $('#c-place .acol.cur .arow'); if (f) f.focus(); },
'c-locate': () => { const L = LOC(); DV.c.mode = 'place'; DV.c.touched = true; Object.assign(DV.c, { country: L.country, region: L.region, city: L.city }); docsRerender(); const f = $('#c-place .acol.cur .arow'); if (f) f.focus({ preventScroll: true }); },
'c-letter': el => { const g = $('#az-' + el.dataset.val), box = $('#c-list'); if (g && box) box.scrollTo({ top: g.offsetTop, behavior: reduceMotion() ? 'auto' : 'smooth' }); },
'd-r': el => { DV.d.r = +el.dataset.val; docsRerender(); sweep(); },
'd-type': el => { DV.d.type = el.dataset.val; docsRerender(); sweep(); },
'd-wider': () => { DV.d.r = Math.min(LOC().radii.length - 1, curR() + 1); docsRerender(); sweep(); },
'd-code-ex': el => { DV.d.code = el.dataset.val; const i = $('#d-code-q'); if (i) i.value = DV.d.code; $('#d-code-res').innerHTML = codeRes(); }
});
/* ===== 75-campus.js ===== */
/* ================= Campus Hub (A layout, B department tiles and trending row, C starred courses) ================= */
const CH = { q: '', prefix: null, sort: 'pop', view: 'list', page: 1, sid: null };
RESETS.push(() => Object.assign(CH, { q: '', prefix: null, sort: 'pop', view: 'list', page: 1, sid: null }));
function campusOf(id) {
const s = BY_ID[id], c = CAMPUS[id] || {};
return { id, n: s.name, a: s.acr, aa: c.aa, c: s.city, st: s.region, co: s.country, t: c.t || s.type, f: c.f, e: c.e, d: c.d, w: c.w, m: c.m || (s.mascot || null), lat: s.lat, lon: s.lng, src: s };
}
function campusCourses(id) {
const s = BY_ID[id];
return coursesOf(s).map(c => { const [pre, num] = c.code.split(/\s+(?=\d)/); return { code: c.code, title: c.title, docs: c.docs, pre: (pre || c.code).replace(/\s+/g, ''), num: num || '', cat: c.cat }; });
}
function campusStats(id) {
const s = BY_ID[id], cs = campusCourses(id), sum = cs.reduce((a, c) => a + c.docs, 0);
const pre = new Map(); cs.forEach(c => { const p = pre.get(c.pre) || { pre: c.pre, courses: 0, docs: 0 }; p.courses++; p.docs += c.docs; pre.set(c.pre, p); });
const prefixes = [...pre.values()].sort((a, b) => a.pre.localeCompare(b.pre));
const um = id === 'um';
return { courses: cs, prefixes, maxPre: Math.max(1, ...prefixes.map(p => p.docs)), total: um ? sum + UM_UNSORTED : s.docs, unsorted: um ? UM_UNSORTED : 0, preview: !um && s.courses > cs.length, allCourses: um ? cs.length : s.courses, departments: um ? prefixes.length : Math.max(prefixes.length, Math.round(s.courses / 14)) };
}
const chMascot = s => { if (!s.m) return null; const m = s.m.match(/^(.*?)\s*\((.*)\)\s*$/); return m ? { team: m[1], who: m[2] } : { team: s.m, who: null }; };
const chShort = s => s.aa && s.aa.length <= 14 ? s.aa : s.a && s.a.length <= 8 ? s.a : s.n;
function chLoc(s) { const p = [s.c]; if (s.st && s.st !== s.c) p.push(s.st); if (s.co && s.co !== 'United States') p.push(s.co); return p.filter(Boolean).join(', '); }
const chType = t => t ? t.charAt(0) + t.slice(1).toLowerCase() : '';
const chDomain = w => { try { return new URL(w).hostname.replace(/^www\./, ''); } catch (e) { return w; } };
const usesMiles = s => !s.co || s.co === 'United States' || s.co === 'United Kingdom';
function chNearby(id, k) {
const me = BY_ID[id], miles = usesMiles(campusOf(id));
const list = SCHOOLS.filter(o => o.id !== id).map(o => { const kmv = km(me.lat, me.lng, o.lat, o.lng); return { s: o, km: kmv, d: miles ? kmv * 0.621371 : kmv, b: bearing({ lat: me.lat, lng: me.lng }, o) }; }).sort((x, y) => x.km - y.km).slice(0, k || 5);
list.unit = miles ? 'mi' : 'km';
return list;
}
const fmtDist = (d, u) => (d < 10 ? d.toFixed(1) : Math.round(d)) + ' ' + u;
/* documents for the rail and the trending row: UM's own list from the Campus Hub, otherwise the most viewed from its courses */
function campusDocs(id) {
if (id === 'um') return UM_POPULAR.map(p => Object.assign({ doc: docByTitle(p.title) }, p)).filter(p => p.doc);
return coursesOf(BY_ID[id]).slice().sort((a, b) => b.docs - a.docs).slice(0, 8).map((c, i) => { const d = courseDocs(id, c.code)[i % 3] || courseDocs(id, c.code)[0]; const h = hsh(d.id); return { doc: d, title: d.title, code: c.code, type: d.hub === 'worksheet' ? 'practice' : d.hub === 'exam' ? 'exam' : d.hub === 'notes' ? 'notes' : 'guide', pages: d.pages, views: 300 + h % 2400, saves: 40 + h % 300 }; });
}
function chThumb(d, w) {
const R0 = rng(hsh(d.title));
const ln = (a, b) => `<i style="width:${Math.round(a + R0() * (b - a))}%"></i>`;
const n = (k, a, b) => Array.from({ length: k }, () => ln(a, b)).join('');
let h = '<i class="h"></i>';
switch (d.type) {
case 'guide': h += n(2, 70, 100) + `<i class="hl" style="width:${Math.round(60 + R0() * 30)}%"></i>` + n(2, 60, 95) + '<i class="h2"></i>' + n(4, 55, 100); break;
case 'lab': h += n(2, 60, 95) + '<span class="tbl">' + '<b></b>'.repeat(9) + '</span>' + n(4, 55, 100); break;
case 'exam': for (let k = 0; k < 6; k++) h += `<span class="q"><em></em>${ln(50, 100)}</span>`; break;
case 'practice': for (let k = 0; k < 3; k++) h += `<span class="q"><em></em>${ln(55, 100)}</span><span class="box"></span>`; break;
case 'cheat': h += `<span class="cols"><span>${n(7, 55, 100)}</span><span>${n(7, 50, 100)}</span></span>`; break;
default: h += n(10, 45, 100);
}
return `<span class="thumb" style="--w:${w}px" aria-hidden="true">${h}</span>`;
}
function chStar(sid, c) {
const on = isStarred(sid, c.code);
return `<button type="button" class="star${on ? ' on' : ''}" data-act="star-course" data-sid="${sid}" data-code="${esc(c.code)}" aria-pressed="${on}" aria-label="${on ? 'Remove ' + esc(c.code) + ' from' : 'Add ' + esc(c.code) + ' to'} your courses" title="${on ? 'In your courses' : 'Add to your courses'}">${icon('i-star', 'sm' + (on ? ' fill' : ''))}</button>`;
}
const chDocsLabel = n => `<span class="cdocs">${icon('i-book', 'xs')}${plural(n, 'doc')}</span>`;
const chRow = (sid, c) => `<li class="crow"><a class="go" href="#${tokCourse(sid, c.code)}"><span class="ccode">${esc(c.code)}</span><span class="ctitle">${esc(c.title)}</span></a>${chDocsLabel(c.docs)}${chStar(sid, c)}<span class="arr" aria-hidden="true">${icon('i-arrow', 'sm')}</span></li>`;
const chCard = (sid, c) => `<li class="ccard"><a class="go" href="#${tokCourse(sid, c.code)}"><span class="ccode">${esc(c.code)}</span><span class="ctitle">${esc(c.title)}</span></a><footer>${chDocsLabel(c.docs)}${chStar(sid, c)}</footer></li>`;
function chFiltered(st) {
const q = CH.q.trim().toLowerCase().replace(/[\s-]+/g, '');
const byCode = (a, b) => a.pre.localeCompare(b.pre) || (parseInt(a.num) - parseInt(b.num));
const list = st.courses.filter(c => (!CH.prefix || c.pre === CH.prefix) && (!q || (c.code + ' ' + c.title).toLowerCase().replace(/[\s-]+/g, '').includes(q)));
if (CH.sort === 'code') return list.sort(byCode);
if (CH.sort === 'title') return list.sort((a, b) => a.title.localeCompare(b.title));
return list.sort((a, b) => b.docs - a.docs || byCode(a, b));
}
function chPageNums(p, n) {
if (n <= 7) return Array.from({ length: n }, (_, i) => i + 1);
const a = [...new Set([1, n, p - 1, p, p + 1].filter(x => x >= 1 && x <= n))].sort((x, y) => x - y);
const out = []; a.forEach((x, i) => { if (i && x - a[i - 1] > 1) out.push('…'); out.push(x); }); return out;
}
function chRegion(sid) {
const s = campusOf(sid), st = campusStats(sid), list = chFiltered(st), active = !!(CH.q.trim() || CH.prefix);
const parts = [];
parts.push(`<div class="fsum"><span>${active ? `<b>${plural(list.length, 'course')}</b>${CH.prefix ? ` in <b>${CH.prefix}</b>` : ''}${CH.q.trim() ? ` matching “${esc(CH.q.trim())}”` : ''}` : `${plural(st.courses.length, 'course')} across ${plural(st.prefixes.length, 'department')}`}</span>${active ? '<button type="button" class="link" data-act="ch-clear">Clear filters</button>' : ''}</div>`);
if (!list.length) return parts.join('') + `<div class="empty"><span class="empty-ic">${icon('i-search')}</span><h3>No courses match ${CH.q ? '“' + esc(CH.q) + '”' : 'that filter'}${CH.prefix ? ' in ' + CH.prefix : ''}</h3><p>Try only the code, like <b>${esc(st.courses[0] ? st.courses[0].code : 'BIL 150')}</b>, or clear the prefix. If your course isn’t listed yet, ask for it and we’ll set it up.</p><div class="row"><button type="button" class="btn soft sm" data-act="ch-clear">Clear filters</button><button type="button" class="btn primary sm" data-act="toast" data-msg="Course request sent. We’ll email you when it’s live.">Request this course</button></div></div>`;
if (!active && CH.page === 1 && st.unsorted) parts.push(`<div class="unsorted"><span class="ic">${icon('i-inbox')}</span><p class="tx"><b>${fmt(st.unsorted)} ${esc(chShort(s))} documents</b> haven’t been matched to a course yet.<span>They still show up in search. Opening a few helps us sort them faster.</span></p><button type="button" class="btn soft sm" data-act="toast" data-msg="Prototype: opens the ${fmt(st.unsorted)} unsorted documents">Browse them</button></div>`);
if (!active && st.preview) parts.push(`<div class="unsorted"><span class="ic">${icon('i-layers')}</span><p class="tx"><b>Showing ${st.courses.length} of ${fmt(st.allCourses)} courses</b> in this preview.<span>The live Campus Hub lists every course at ${esc(chShort(s))}.</span></p></div>`);
const per = 24, pages = Math.ceil(list.length / per);
CH.page = Math.max(1, Math.min(CH.page, pages));
const slice = list.slice((CH.page - 1) * per, CH.page * per);
parts.push(CH.view === 'grid' ? `<ul class="cgrid">${slice.map(c => chCard(sid, c)).join('')}</ul>` : `<ul class="clist">${slice.map(c => chRow(sid, c)).join('')}</ul>`);
if (pages > 1) { const p = CH.page; parts.push(`<nav class="pager" aria-label="Course pages"><button type="button" class="btn ghost sm" data-act="ch-page" data-p="${p - 1}" ${p === 1 ? 'disabled' : ''}>${icon('i-left', 'sm')}<span class="t">Prev</span></button><div class="pages">${chPageNums(p, pages).map(n => n === '…' ? '<span class="pg-gap">…</span>' : `<button type="button" class="pg${n === p ? ' on' : ''}" data-act="ch-page" data-p="${n}" ${n === p ? 'aria-current="page"' : ''} aria-label="Page ${n}">${n}</button>`).join('')}</div><button type="button" class="btn ghost sm" data-act="ch-page" data-p="${p + 1}" ${p === pages ? 'disabled' : ''}><span class="t">Next</span>${icon('i-right', 'sm')}</button></nav>`); }
return parts.join('');
}
function chToolbar() {
return `<div class="toolbar">
<div class="field f-search"><label for="cq">Search courses</label><div class="fi">${icon('i-search', 'sm')}<input id="cq" data-role="cq" type="search" placeholder="Code or title, e.g. BIL 150" value="${esc(CH.q)}" autocomplete="off"></div></div>
<div class="field combo" data-combo><label for="pfx">Filter by prefix</label><div class="fi"><input id="pfx" data-role="pfx" role="combobox" aria-expanded="false" aria-controls="pfx-list" aria-autocomplete="list" placeholder="Select prefix…" value="${CH.prefix || ''}" autocomplete="off" spellcheck="false"><button type="button" class="combo-x" data-act="ch-prefix-clear" aria-label="Clear prefix filter" ${CH.prefix ? '' : 'hidden'}>${icon('i-x', 'xs')}</button><span class="combo-chev" aria-hidden="true">${icon('i-down', 'sm')}</span></div><ul class="combo-list" id="pfx-list" role="listbox" aria-label="Course prefixes" hidden></ul></div>
<div class="field f-sort"><label for="cs">Sort by</label><select id="cs" data-role="sort"><option value="pop" ${CH.sort === 'pop' ? 'selected' : ''}>Most popular</option><option value="code" ${CH.sort === 'code' ? 'selected' : ''}>Course code</option><option value="title" ${CH.sort === 'title' ? 'selected' : ''}>Title A–Z</option></select></div>
<div class="seg2" role="group" aria-label="Layout"><button type="button" data-act="ch-view" data-val="list" aria-pressed="${CH.view === 'list'}" aria-label="List view" title="List view">${icon('i-list')}</button><button type="button" data-act="ch-view" data-val="grid" aria-pressed="${CH.view === 'grid'}" aria-label="Grid view" title="Grid view">${icon('i-grid')}</button></div>
</div>`;
}
function chShelf(sid) {
const s = campusOf(sid), sn = esc(chShort(s)), st = campusStats(sid);
const mine = st.courses.filter(c => isStarred(sid, c.code));
const head = `<div class="panel-h"><h2>Your courses at ${sn} ${mine.length ? `<span class="count">${mine.length}</span>` : ''}</h2>${mine.length ? '<a class="link" href="#room">Open My Room</a>' : ''}</div>`;
if (!mine.length) {
const sug = st.courses.slice().sort((a, b) => b.docs - a.docs).slice(0, 4);
return head + `<div class="shelf-empty"><p>Star the courses you’re taking this term and they’ll stay here, one tap away. New documents in them show up in My Room.</p><div class="suggest"><span class="lab">Popular picks</span>${sug.map(c => `<button type="button" class="sug" data-act="star-course" data-sid="${sid}" data-code="${esc(c.code)}">${icon('i-plus', 'xs')}${esc(c.code)}</button>`).join('')}</div></div>`;
}
return head + `<ul class="cgrid shelf-grid">${mine.map(c => chCard(sid, c)).join('')}</ul>`;
}
function chDocRow(p, side) {
return `<li class="drow">${chThumb(p, 40)}<a class="go" href="#${tokDoc(p.doc.id)}"><span class="dtitle">${esc(p.title)}</span><span class="dmeta"><span class="code">${esc(p.code)}</span><span class="dot-sep">${CH_TYPES[p.type] || 'Study guide'}</span></span><span class="dmeta sub"><span class="dstat">${side}</span><span class="dot-sep">${plural(p.pages, 'page')}</span></span></a></li>`;
}
PAGES.campus = {
cls: () => 'pg-campus',
render(r) {
const sid = r.id; if (CH.sid !== sid) Object.assign(CH, { q: '', prefix: null, page: 1, sid });
const s = campusOf(sid), st = campusStats(sid), m = chMascot(s), sn = esc(chShort(s)), near = chNearby(sid);
const facts = []; const loc = chLoc(s); if (loc) facts.push(['i-pin', loc]); if (s.t) facts.push(['i-building', chType(s.t)]); if (s.f) facts.push(['i-calendar', 'Est. ' + s.f]);
const chips = [];
if (m) chips.push(`<span class="chip sc">${icon('i-pennant', 'sm')}<span>Home of the ${esc(m.team)}${m.who ? `<span class="sub"> · ${esc(m.who)}</span>` : ''}</span></span>`);
if (s.w) chips.push(`<a class="chip" href="${esc(s.w)}" target="_blank" rel="noopener">${icon('i-globe', 'sm')}${esc(chDomain(s.w))}${icon('i-external', 'xs')}</a>`);
const stats = [['Documents', fmt(st.total)], ['Courses', fmt(st.allCourses)], ['Departments', fmt(st.departments)]];
if (s.e) stats.push(['Students', fmt(s.e)]); else if (s.f) stats.push(['Founded', String(s.f)]);
const mineOn = S.mine === sid;
const docs = campusDocs(sid);
const top = st.prefixes.slice().sort((a, b) => b.docs - a.docs).slice(0, 7);
return `<div class="page-in">${crumbs([['documents', 'Documents'], [tokSchool(sid), s.n]])}
<section class="card hero-a" id="hero" aria-labelledby="schoolName">
<div class="rule" aria-hidden="true"></div>
<div class="hero-a-body">
${schoolMono(s.src, 64)}
<div style="min-width:0">
<div class="eyebrow">Campus Hub ${mineOn ? '<span class="badge">Your school</span>' : ''}</div>
<h1 id="schoolName">${esc(s.n)}</h1>
<ul class="facts">${facts.map(([i, t]) => `<li>${icon(i, 'sm')}${esc(t)}</li>`).join('')}</ul>
${s.d ? `<p class="desc">${esc(s.d)}</p>` : ''}
${chips.length ? `<div class="chips">${chips.join('')}</div>` : ''}
</div>
<div class="hero-actions"><button type="button" class="btn primary${mineOn ? ' is-on' : ''}" data-act="ch-mine" aria-pressed="${mineOn}">${icon(mineOn ? 'i-check' : 'i-plus', 'sm')}${mineOn ? 'My school' : 'Set as my school'}</button><button type="button" class="icon-btn" data-act="ch-more" aria-haspopup="menu" aria-expanded="false" aria-label="More actions">${icon('i-more')}</button></div>
</div>
<dl class="statbar" style="--n:${stats.length}">${stats.map(([k, v]) => `<div><dt>${k}</dt><dd>${v}</dd></div>`).join('')}</dl>
</section>
<section class="panel ch-mine" id="shelf" aria-label="Your courses">${chShelf(sid)}</section>
<div class="sec-h"><div><h2>Browse by department</h2><p>Tap one to filter the course list below</p></div></div>
<div class="depts">${top.map(p => `<button type="button" class="dept" data-act="ch-prefix" data-val="${p.pre}" data-toggle="1" aria-pressed="${CH.prefix === p.pre}"><b>${p.pre}</b><span>${plural(p.courses, 'course')} · ${plural(p.docs, 'doc')}</span><span class="bar" aria-hidden="true"><i style="width:${Math.round(p.docs / st.maxPre * 100)}%"></i></span></button>`).join('')}
<button type="button" class="dept all" data-act="ch-focus-prefix"><b>All ${st.prefixes.length} departments ${icon('i-arrow', 'sm')}</b><span>Search every prefix</span></button></div>
<div class="sec-h"><div><h2>Trending at ${sn} this week</h2><p>Most opened documents from ${sn} courses</p></div>
<div class="car-nav"><button type="button" class="icon-btn" data-act="ch-car" data-dir="-1" aria-label="Scroll left">${icon('i-left')}</button><button type="button" class="icon-btn" data-act="ch-car" data-dir="1" aria-label="Scroll right">${icon('i-right')}</button></div></div>
<div class="carousel" id="car" tabindex="0" aria-label="Trending documents">${docs.map(p => `<article class="dcard"><div class="tw">${chThumb(p, 150)}</div><div class="bd"><span class="dmeta"><span class="code">${esc(p.code)}</span><span class="type-pill">${CH_TYPES[p.type] || 'Study guide'}</span></span><a class="go" href="#${tokDoc(p.doc.id)}"><span class="dtitle">${esc(p.title)}</span></a><div class="dfoot"><span>${plural(p.pages, 'page')}</span><span style="display:inline-flex;gap:5px;align-items:center">${icon('i-eye', 'xs')}${compact(p.views)}</span></div></div></article>`).join('')}</div>
<div class="a-grid">
<section class="panel" id="coursesPanel" aria-labelledby="ch-a">
<div class="panel-h"><h2 id="ch-a">Courses <span class="count">${fmt(st.allCourses)}</span></h2></div>
${chToolbar()}
<div id="cregion">${chRegion(sid)}</div>
</section>
<aside class="rail" aria-label="More from ${sn}">
<section class="card rcard"><div class="rcard-h"><div><h3>Popular at ${sn}</h3><p>Most opened this month</p></div></div>
<ul class="clist">${docs.slice(0, 5).map(p => chDocRow(p, `${icon('i-eye', 'xs')}${compact(p.views)} views`)).join('')}</ul></section>
<section class="card rcard"><div class="rcard-h"><div><h3>Nearby schools</h3><p>On StudyBoost, by distance</p></div></div>
<ul class="clist">${near.map(n => `<li class="nrow">${schoolMono(n.s, 32)}<a class="go" href="#${tokSchool(n.s.id)}"><span class="nname">${esc(n.s.name)}</span><span class="nmeta">${esc(chType((CAMPUS[n.s.id] || {}).t || n.s.type))}${n.s.city ? ' · ' + esc(n.s.city) : ''}</span></a><span class="ndist">${fmtDist(n.d, near.unit)}</span></li>`).join('')}</ul></section>
<section class="card upload"><span class="ic">${icon('i-upload')}</span><h3>Took a ${sn} course?</h3><p>Share your notes and give the next class a head start.</p><div><button type="button" class="btn soft sm" data-act="upload">Upload notes</button></div></section>
</aside>
</div></div>`;
},
mount(r, page) {
const p = schoolPal(BY_ID[r.id]);
const vars = { '--sc-deep': p.deep, '--sc-bright': p.bright, '--sc-on-deep': p.onDeep, '--sc-bright-on-deep': p.brightOnDeep, '--sc-ink-l': p.inkL, '--sc-ink-d': p.inkD, '--sc-tint-l': p.tintL, '--sc-tint-d': p.tintD, '--sc-wash-l': p.washL, '--sc-wash-d': p.washD };
Object.entries(vars).forEach(([k, v]) => page.style.setProperty(k, v));
wireCampusControls($('.page-in', page));
}
};
function chUpdate() { const rg = $('#cregion'); if (rg) rg.innerHTML = chRegion(R.id); $$('[data-act="ch-prefix"]').forEach(b => b.setAttribute('aria-pressed', String((b.dataset.val || null) === CH.prefix))); const i = $('#pfx'); if (i && document.activeElement !== i) i.value = CH.prefix || ''; const x = $('.combo-x'); if (x) x.hidden = !CH.prefix; $$('[data-act="ch-view"]').forEach(b => b.setAttribute('aria-pressed', String(b.dataset.val === CH.view))); }
function chScroll() { const el = $('#coursesPanel'), pg = $('#page'); if (!el || !pg) return; const top = el.getBoundingClientRect().top - pg.getBoundingClientRect().top + pg.scrollTop - 12; if (el.getBoundingClientRect().top > innerHeight * 0.6 || el.getBoundingClientRect().top < 0) pg.scrollTo({ top, behavior: reduceMotion() ? 'auto' : 'smooth' }); }
function setPrefix(p, scroll) { CH.prefix = p || null; CH.page = 1; chUpdate(); if (scroll) chScroll(); }
/* the prefix combobox (ported) */
let comboActive = -1;
function comboOpen(input) {
const box = input.closest('[data-combo]'), list = $('.combo-list', box), st = campusStats(R.id);
const q = input.value.trim().toUpperCase();
const opts = q && q !== CH.prefix ? st.prefixes.filter(p => p.pre.startsWith(q)).concat(st.prefixes.filter(p => !p.pre.startsWith(q) && p.pre.includes(q))) : st.prefixes;
list.innerHTML = (!q || q === CH.prefix ? `<li role="option" id="pfx-list-all" data-val="" aria-selected="false"><b>All prefixes</b><span>${st.courses.length}</span></li>` : '') +
(opts.length ? opts.map(p => `<li role="option" id="pfx-list-${p.pre}" data-val="${p.pre}" class="${p.pre === CH.prefix ? 'is-current' : ''}" aria-selected="false"><b>${p.pre}</b><span>${plural(p.courses, 'course')}</span></li>`).join('') : '<li class="none" aria-disabled="true">No prefix starts with that</li>');
list.hidden = false; box.classList.add('open'); input.setAttribute('aria-expanded', 'true'); comboActive = -1;
const cur = $('.is-current', list); if (cur) cur.scrollIntoView({ block: 'nearest' });
}
function comboClose(input, restore) {
const box = input.closest('[data-combo]'); if (!box) return;
$('.combo-list', box).hidden = true; box.classList.remove('open');
input.setAttribute('aria-expanded', 'false'); input.removeAttribute('aria-activedescendant');
if (restore !== false) input.value = CH.prefix || '';
}
function comboMove(input, d) {
const items = $$('li[role="option"]', input.closest('[data-combo]')); if (!items.length) return;
comboActive = (comboActive + d + items.length) % items.length;
items.forEach((li, i) => li.setAttribute('aria-selected', String(i === comboActive)));
input.setAttribute('aria-activedescendant', items[comboActive].id); items[comboActive].scrollIntoView({ block: 'nearest' });
}
function wireCampusControls(page) {
page.addEventListener('input', e => {
const t = e.target;
if (t.dataset.role === 'cq') { CH.q = t.value; CH.page = 1; chUpdate(); }
else if (t.dataset.role === 'pfx') comboOpen(t);
});
page.addEventListener('change', e => { if (e.target.dataset.role === 'sort') { CH.sort = e.target.value; CH.page = 1; chUpdate(); } });
page.addEventListener('focusin', e => { if (e.target.dataset.role === 'pfx') { e.target.select(); comboOpen(e.target); } });
page.addEventListener('focusout', e => { if (e.target.dataset.role === 'pfx') { const i = e.target; setTimeout(() => { if (document.activeElement !== i) comboClose(i); }, 120); } });
page.addEventListener('mousedown', e => {
const li = e.target.closest('.combo-list li[role="option"]');
if (li) { e.preventDefault(); const input = $('[data-role="pfx"]', li.closest('[data-combo]')); setPrefix(li.dataset.val || null); comboClose(input); input.blur(); }
});
page.addEventListener('keydown', e => {
const t = e.target; if (!t.dataset || t.dataset.role !== 'pfx') return;
const list = $('.combo-list', t.closest('[data-combo]'));
if (e.key === 'ArrowDown') { e.preventDefault(); if (list.hidden) comboOpen(t); comboMove(t, 1); }
else if (e.key === 'ArrowUp') { e.preventDefault(); if (list.hidden) comboOpen(t); comboMove(t, -1); }
else if (e.key === 'Enter') { e.preventDefault(); const items = $$('li[role="option"]', list); const pick = items[comboActive] || items.find(li => li.dataset.val && li.dataset.val.startsWith(t.value.trim().toUpperCase())) || (t.value.trim() ? null : items[0]); if (pick) { setPrefix(pick.dataset.val || null); comboClose(t); } }
else if (e.key === 'Escape') { e.stopPropagation(); comboClose(t); }
});
}
Object.assign(ACT, {
'star-course': el => {
const k = starKey(el.dataset.sid, el.dataset.code), on = toggleIn(S.starred, k); save();
if (R.page === 'campus') { chUpdate(); const sh = $('#shelf'); if (sh) sh.innerHTML = chShelf(R.id); renderNav(shellOf(R)); }
else rerender();
toast(on ? `Added ${el.dataset.code} to your courses` : `Removed ${el.dataset.code} from your courses`);
},
'ch-mine': () => { const id = R.id, on = S.mine !== id; S.mine = on ? id : null; save(); rerender(); toast(on ? `${BY_ID[id].name} is now your school. It’s pinned in the sidebar.` : 'Removed from My school'); },
'ch-more': el => openPop(el, `<div class="menu" role="menu" aria-label="More actions"><button type="button" class="mi" role="menuitem" data-act="ch-note">${icon('i-pin')}<span class="mt">Add to notes</span></button><button type="button" class="mi" role="menuitem" data-act="ch-copy">${icon('i-link')}<span class="mt">Copy page link</span></button><button type="button" class="mi" role="menuitem" data-act="toast" data-msg="Thanks. We’ll review the school details.">${icon('i-edit')}<span class="mt">Suggest an edit</span></button></div>`, { role: 'menu', label: 'More actions' }),
'ch-note': () => { closePop(); addNote(BY_ID[R.id].name); toast(`Added ${BY_ID[R.id].short} to your notes`); },
'ch-copy': () => { closePop(); const url = 'https://studyboost.com/institutions/' + R.id; const done = () => toast('Link copied'); try { navigator.clipboard.writeText(url).then(done, () => toast(url)); } catch (e) { toast(url); } },
'ch-prefix': el => { const val = el.dataset.val || null; setPrefix(el.dataset.toggle && CH.prefix === val ? null : val, true); },
'ch-prefix-clear': el => { setPrefix(null); const i = el.parentElement.querySelector('input'); i.value = ''; i.focus(); },
'ch-focus-prefix': () => { chScroll(); const i = $('#pfx'); if (i) setTimeout(() => { i.focus(); comboOpen(i); }, reduceMotion() ? 0 : 350); },
'ch-clear': () => { CH.q = ''; const i = $('#cq'); if (i) i.value = ''; setPrefix(null); },
'ch-view': el => { CH.view = el.dataset.val; CH.page = 1; chUpdate(); },
'ch-page': el => { const p = +el.dataset.p; if (p >= 1) { CH.page = p; chUpdate(); chScroll(); } },
'ch-car': el => { const c = $('#car'); c.scrollBy({ left: +el.dataset.dir * (c.clientWidth * 0.8), behavior: reduceMotion() ? 'auto' : 'smooth' }); }
});
/* ===== 76-course.js ===== */
/* ================= Course Hub (the Calm hub, with the Shelves hub's exam prep path, related courses and category art) ================= */
const CP = { key: null, q: '', sort: 'popular', type: 'all', view: 'list', showAll: false };
RESETS.push(() => Object.assign(CP, { key: null, q: '', sort: 'popular', type: 'all', view: 'list', showAll: false }));
const CH_TYPE = {
guide: { label: 'Study guide', plural: 'Study guides' },
exam: { label: 'Exam & quiz', plural: 'Exams & quizzes' },
notes: { label: 'Notes', plural: 'Notes' },
worksheet: { label: 'Worksheet', plural: 'Worksheets' }
};
const CH_ORDER = ['guide', 'exam', 'notes', 'worksheet'];
const EXAMS = [
{ key: 'e1', label: 'Exam 1', short: '1', re: /\b(exam|test)\s*(1|one)\b/i },
{ key: 'e2', label: 'Exam 2', short: '2', re: /\b(exam|test)\s*(2|two)\b/i },
{ key: 'e3', label: 'Exam 3', short: '3', re: /\b(exam|test)\s*(3|three)\b/i },
{ key: 'e4', label: 'Exam 4', short: '4', re: /\b(exam|test)\s*(4|four)\b/i },
{ key: 'fin', label: 'Final exam', short: 'F', re: /\bfinal\b/i }
];
/* paper thumbnail (the Course Hub's DocThumb), coloured by document type */
function docThumb(d, w, h) {
const kind = d.thumb || THUMB_OF[d.hub] || 'guide', scan = kind === 'scan';
const u = Math.max(2, Math.round(w / 26));
const widths = [100, 92, 97, 84, 95, 70, 100, 88, 94, 76, 98, 82, 90, 64, 96, 86];
const count = kind === 'notes' ? 16 : scan ? 14 : 10;
let inner = `<span class="dt-top"><i class="dt-head"></i>${scan ? '<i class="dt-mark"></i>' : ''}</span>`;
if (kind === 'guide') inner += '<span class="dt-box"><i></i><i style="width:64%"></i></span>';
if (kind === 'sheet') inner += `<span class="dt-grid">${Array.from({ length: 15 }, (_, i) => `<i${i < 3 ? ' class="t"' : ''}></i>`).join('')}</span>`;
if (kind === 'exam') inner += [78, 64, 82, 58].map(x => `<span class="dt-q"><b></b><i style="width:${x}%"></i></span>`).join('');
inner += widths.slice(0, count).map(x => `<i class="dt-l" style="width:${x}%"></i>`).join('');
return `<span class="dthumb ty-${d.hub || 'guide'}${scan ? ' scan' : ''}" style="--w:${w}px;--h:${h}px;--u:${u}px" aria-hidden="true">${inner}</span>`;
}
function courseModel(sid, code) {
const s = BY_ID[sid], c = courseBy(sid, code);
const info = sid === 'um' ? (COURSE_INFO[code] || {}) : {};
const docs = courseDocs(sid, code).map((d, i) => Object.assign({ rank: i }, d));
const cat = catOfCourse(c), n = docs.length;
const counts = { guide: 0, exam: 0, notes: 0, worksheet: 0, free: 0 };
docs.forEach(d => { counts[d.hub] = (counts[d.hub] || 0) + 1; if (!d.pro) counts.free++; });
const title = c.title && slug(c.title) !== slug(c.code) ? c.title : c.code;
const subject = info.subject || (PFX[c.code.split(/\s/)[0]] ? PFX[c.code.split(/\s/)[0]][1] : '');
const word = n === 1 ? 'document' : 'documents';
const blurb = info.desc || (subject ? `A ${subject} course at ${s.name}, with ${plural(docs.total || n, 'study document')} shared by students.` : `${plural(docs.total || n, 'study document')} shared by students at ${s.name}.`);
const sameCat = coursesOf(s).filter(x => x.code !== c.code && catOfCourse(x) === cat).sort((a, b) => b.docs - a.docs).slice(0, 4);
const related = sameCat.length ? sameCat : coursesOf(s).filter(x => x.code !== c.code).sort((a, b) => b.docs - a.docs).slice(0, 4);
return { s, c, sid, code: c.code, title, showCode: title !== c.code, subject, blurb, tags: info.tags || [], cat, catName: (CH_CATS[cat] || CH_CATS[11]).name, docs, n, total: docs.total || n, word, counts, related, relPopular: !sameCat.length };
}
function cpList(m) {
const q = CP.q.trim().toLowerCase();
let list = m.docs.slice();
if (CP.type === 'free') list = list.filter(d => !d.pro);
else if (CP.type === 'saved') list = list.filter(d => S.saved.includes(d.id));
else if (CP.type !== 'all') list = list.filter(d => d.hub === CP.type);
if (q) list = list.filter(d => d.title.toLowerCase().includes(q));
if (CP.sort === 'az') list.sort((a, b) => a.title.localeCompare(b.title));
else if (CP.sort === 'pages') list.sort((a, b) => (b.pages || -1) - (a.pages || -1) || a.rank - b.rank);
else if (CP.sort === 'free') list.sort((a, b) => (Number(!b.pro) - Number(!a.pro)) || a.rank - b.rank);
return list;
}
function cpMeta(d) {
const viewed = S.viewed.includes(d.id), open = !d.pro || S.plan === 'pro' || S.unlocked.includes(d.id);
return `${d.pages ? `<span class="m-pg">${icon('i-file', 'xs')}${plural(d.pages, 'page')}</span>` : ''}${!d.pro ? '<span class="m-free"><i></i>Free to read</span>' : open ? `<span class="m-pro open">${icon('i-check', 'xs')}Unlocked</span>` : `<span class="m-pro">${icon('i-lock', 'xs')}Premium</span>`}${viewed ? `<span class="m-seen">${icon('i-check', 'xs')}Opened</span>` : ''}`;
}
const cpSaveBtn = (d, cls) => { const on = S.saved.includes(d.id); return `<button type="button" class="cp-save${cls ? ' ' + cls : ''}${on ? ' on' : ''}" data-act="cp-save" data-id="${d.id}" aria-pressed="${on}" aria-label="${on ? 'Remove ' + esc(d.title) + ' from saved' : 'Save ' + esc(d.title)}" title="${on ? 'Saved' : 'Save for later'}">${icon('i-star', 'sm')}</button>`; };
function cpDocsHtml(m) {
if (m.n === 1) {
const d = m.docs[0];
return `<div class="cp-one"><div class="one-card"><div class="one-art">${docThumb(d, 150, 196)}</div><div class="one-b"><span class="ty-label ty-${d.hub}"><i></i>${CH_TYPE[d.hub].label}</span><h3>${esc(d.title)}</h3><div class="cp-meta">${cpMeta(d)}</div><div class="one-acts"><a class="cp-btn primary" href="#${tokDoc(d.id)}">Open document${icon('i-arrow', 'sm')}</a>${cpSaveBtn(d, 'wide')}</div></div></div>
<div class="cp-start"><span class="cs-ic">${icon('i-upload')}</span><div><b>This course hub is just getting started</b><span>Took ${esc(m.code)}? Your notes could help the next class find their footing.</span></div><button type="button" class="cp-btn" data-act="upload">Share your notes</button></div></div>`;
}
const list = cpList(m), LIMIT = 10, shown = CP.showAll ? list : list.slice(0, LIMIT);
if (!list.length) return `<div class="cp-empty"><span class="ce-ic">${icon('i-search')}</span><strong>${CP.q.trim() ? `No documents match “${esc(CP.q.trim())}”` : 'Nothing here yet'}</strong><span>Try another word, or clear the filters to see everything.</span><button type="button" class="cp-btn" data-act="cp-reset">Clear filters</button></div>`;
const body = CP.view === 'grid'
? `<ul class="cp-grid">${shown.map(d => `<li class="cp-gcard${S.viewed.includes(d.id) ? ' seen' : ''}"><div class="g-art ty-${d.hub}">${docThumb(d, 110, 146)}${cpSaveBtn(d, 'over')}</div><div class="g-b"><span class="ty-label ty-${d.hub} sm">${CH_TYPE[d.hub].label}</span><a class="cp-title" href="#${tokDoc(d.id)}">${esc(d.title)}</a><div class="cp-meta">${cpMeta(d)}</div></div></li>`).join('')}</ul>`
: `<ul class="cp-list">${shown.map(d => `<li class="cp-row${S.viewed.includes(d.id) ? ' seen' : ''}">${docThumb(d, 62, 78)}<div class="r-b"><span class="ty-label ty-${d.hub}"><i></i>${CH_TYPE[d.hub].label}</span><a class="cp-title" href="#${tokDoc(d.id)}">${esc(d.title)}</a><div class="cp-meta">${cpMeta(d)}</div></div>${cpSaveBtn(d)}<a class="cp-open" href="#${tokDoc(d.id)}" aria-label="Open ${esc(d.title)}"><span>Open</span>${icon('i-arrow', 'sm')}</a></li>`).join('')}</ul>`;
const more = !CP.showAll && list.length > LIMIT ? `<button type="button" class="cp-btn more" data-act="cp-more">Show all ${list.length} documents${icon('i-down', 'sm')}</button>` : '';
const preview = m.total > m.n ? `<p class="cp-note">Showing ${m.n} of ${fmt(m.total)} documents in this preview.</p>` : '';
return body + more + preview;
}
function cpChips(m) {
const defs = [{ k: 'all', label: 'All', n: m.n }];
CH_ORDER.forEach(t => { if (m.counts[t] > 0) defs.push({ k: t, label: CH_TYPE[t].plural, n: m.counts[t] }); });
if (m.counts.free > 0 && m.counts.free < m.n) defs.push({ k: 'free', label: 'Free', n: m.counts.free });
const sv = m.docs.filter(d => S.saved.includes(d.id)).length;
if (sv > 0) defs.push({ k: 'saved', label: 'Saved', n: sv });
if (!defs.some(d => d.k === CP.type)) CP.type = 'all';
return defs.length > 2 ? `<div class="cp-chips" role="group" aria-label="Filter documents">${defs.map(d => `<button type="button" class="cp-chip" data-act="cp-type" data-k="${d.k}" aria-pressed="${CP.type === d.k}">${d.label}<span>${d.n}</span></button>`).join('')}</div>` : '';
}
function examPath(m) {
const groups = EXAMS.map(g => ({ g, docs: m.docs.filter(d => g.re.test(d.title)) })).filter(x => x.docs.length);
if (groups.length < 2) return '';
return `<section class="cp-card"><h3>Exam prep path</h3><p class="sub">Documents grouped by the exam they cover.</p>
<ol class="ex-path">${groups.map((x, i) => `<li><div class="ex-rail"><span class="ex-dot${x.g.key === 'fin' ? ' fin' : ''}">${x.g.short}</span>${i < groups.length - 1 ? '<span class="ex-line"></span>' : ''}</div><div class="ex-b"><b>${x.g.label} <span>· ${plural(x.docs.length, 'document')}</span></b><div class="ex-docs">${x.docs.map(d => `<a class="ex-chip" href="#${tokDoc(d.id)}">${esc(d.title)}</a>`).join('')}</div></div></li>`).join('')}</ol></section>`;
}
PAGES.course = {
cls: () => 'pg-course',
render(r) {
const m = courseModel(r.sid, r.code), key = r.sid + '|' + m.code;
if (CP.key !== key) Object.assign(CP, { key, q: '', type: 'all', showAll: false });
const glance = [{ n: m.n, label: m.word }];
if (m.counts.free > 0) glance.push({ n: m.counts.free, label: 'free to read' });
CH_ORDER.forEach(t => { if (m.counts[t] > 0 && m.counts[t] < m.n) glance.push({ n: m.counts[t], label: CH_TYPE[t].plural.toLowerCase() }); });
const inCourses = isStarred(r.sid, m.code), inNotes = S.courseNotes.includes(key);
const viewedN = m.docs.filter(d => S.viewed.includes(d.id)).length, savedN = m.docs.filter(d => S.saved.includes(d.id)).length;
const pct = m.n ? viewedN / m.n : 0, circ = 213.63;
const sortWords = { popular: 'most popular first', pages: 'longest first', az: 'A to Z', free: 'free first' };
const filtered = m.n === 1 ? 1 : cpList(m).length;
return `<div class="page-in">${crumbs([['documents', 'Documents'], [tokSchool(r.sid), m.s.name], [TOKEN, m.showCode ? `${m.code} · ${m.title}` : m.title]])}
<section class="cp-hero" data-c="${m.cat}" aria-labelledby="course-title">
<div class="cp-hero-main">
<div class="cp-tags"><span class="cp-pill brand">${icon('i-sparkle', 'xs fill')}Course hub</span>${m.cat !== 11 ? `<span class="cp-pill cat">${catGlyph(m.cat, 24)}${esc(m.catName)}</span>` : ''}${m.subject ? `<span class="cp-pill soft">${esc(m.subject)}</span>` : ''}<a class="cp-pill soft" href="#${tokSchool(r.sid)}">${schoolMono(m.s, 18)}${esc(m.s.short)}</a></div>
<div class="cp-titles">${m.showCode ? `<span class="cp-code">${esc(m.code)}</span>` : ''}<h1 id="course-title">${esc(m.title)}</h1></div>
<p class="cp-blurb">${esc(m.blurb)}</p>
<ul class="cp-glance" aria-label="At a glance">${glance.map(g => `<li><strong>${g.n}</strong>${g.label}</li>`).join('')}</ul>
${m.tags.length ? `<div class="cp-topics"><span>Topics</span>${m.tags.map(t => `<span class="topic">${esc(t)}</span>`).join('')}</div>` : ''}
<div class="cp-acts"><button type="button" class="cp-btn primary lg" data-act="star-course" data-sid="${r.sid}" data-code="${esc(m.code)}" aria-pressed="${inCourses}">${icon(inCourses ? 'i-check' : 'i-bookmark', 'sm')}${inCourses ? 'In My Courses' : 'Save to My Courses'}</button>
<button type="button" class="cp-btn lg" data-act="cp-notes" aria-pressed="${inNotes}">${icon(inNotes ? 'i-check' : 'i-plus', 'sm')}${inNotes ? 'Added to notes' : 'Add to notes'}</button>
<button type="button" class="cp-btn lg icon" data-act="cp-share" aria-label="Share this course" title="Share">${icon('i-share', 'sm')}</button></div>
</div>
<div class="cp-hero-art">${CAT_ART[m.cat] || CAT_ART[11]}</div>
</section>
<div class="cp-cols">
<section class="cp-docs" aria-labelledby="docs-h">
<div class="cp-docs-h"><div><h2 id="docs-h">Documents</h2><p>${filtered === m.n ? `${m.n} ${m.word}` : `Showing ${filtered} of ${m.n} documents`} · ${sortWords[CP.sort]}</p></div>
${m.n > 3 ? `<div class="cp-seg" role="group" aria-label="Layout"><button type="button" data-act="cp-view" data-v="list" aria-pressed="${CP.view === 'list'}" aria-label="List view" title="List view">${icon('i-list')}</button><button type="button" data-act="cp-view" data-v="grid" aria-pressed="${CP.view === 'grid'}" aria-label="Grid view" title="Grid view">${icon('i-grid')}</button></div>` : ''}</div>
${m.n > 5 ? `<div class="cp-tools"><label class="cp-search">${icon('i-search')}<span class="sr">Search documents</span><input type="search" id="cp-q" placeholder="Search ${m.n} documents in ${esc(m.code)}" value="${esc(CP.q)}"></label><label class="cp-sort">Sort<select id="cp-sort"><option value="popular" ${CP.sort === 'popular' ? 'selected' : ''}>Most popular</option><option value="pages" ${CP.sort === 'pages' ? 'selected' : ''}>Most pages</option><option value="az" ${CP.sort === 'az' ? 'selected' : ''}>A to Z</option><option value="free" ${CP.sort === 'free' ? 'selected' : ''}>Free first</option></select></label></div>` : ''}
${cpChips(m)}
<div id="cp-docs">${cpDocsHtml(m)}</div>
</section>
<aside class="cp-side" aria-label="Course tools">
${examPath(m)}
<section class="cp-card"><h3>Your progress</h3>
<div class="prog"><div class="prog-ring"><svg width="86" height="86" viewBox="0 0 86 86" aria-hidden="true"><circle cx="43" cy="43" r="34" fill="none" stroke="var(--primary-soft)" stroke-width="10"/><circle cx="43" cy="43" r="34" fill="none" stroke="var(--primary)" stroke-width="10" stroke-linecap="${viewedN ? 'round' : 'butt'}" stroke-dasharray="${(pct * circ).toFixed(1)} ${circ}" transform="rotate(-90 43 43)"/></svg><span>${viewedN}/${m.n}</span></div>
<div><b>${viewedN === 0 ? 'Ready when you are' : viewedN === m.n ? 'Course explored' : 'Nice momentum'}</b><span>${viewedN === 0 ? 'Open a document to start tracking your study time here.' : viewedN === m.n ? 'You have opened every document in this hub.' : `You have opened ${viewedN} of ${m.n}. Keep going.`}</span></div></div>
<div class="prog-n"><div><span>Opened</span><b>${viewedN}</b></div><div><span>Saved</span><b>${savedN}</b></div></div></section>
<section class="cp-card"><h3>Study tools</h3><p class="sub">Built from the documents in this course.</p>
<button type="button" class="tool-row" data-act="hoot-open" data-q="Make flashcards from ${esc(m.code)}"><span class="tr-ic t1">${icon('i-cards')}</span><span><b>Flashcards</b><small>Turn these notes into a deck</small></span>${icon('i-right', 'sm')}</button>
<button type="button" class="tool-row" data-act="cp-quiz"><span class="tr-ic t2">${icon('i-gamepad')}</span><span><b>Practice quiz</b><small>Questions drawn from ${esc(m.code)}</small></span>${icon('i-right', 'sm')}</button>
<button type="button" class="tool-row" data-act="hoot-open" data-q="Summarize ${esc(m.code)} for the exam"><span class="tr-ic t3">${icon('i-list')}</span><span><b>Quick summary</b><small>The key ideas in two minutes</small></span>${icon('i-right', 'sm')}</button></section>
<section class="cp-card"><h3>${m.relPopular ? 'Popular at ' : 'Related courses at '}${esc(m.s.short)}</h3>
<ul class="rel">${m.related.map(x => `<li><a class="rel-row" href="#${tokCourse(r.sid, x.code)}">${catGlyph(catOfCourse(x), 42)}<span><b>${esc(x.code)}</b><small>${esc(x.title)}</small></span><span class="rel-n">${plural(x.docs, 'doc')}</span></a></li>`).join('')}</ul>
<a class="cp-link" href="#${tokSchool(r.sid)}">Browse all ${esc(m.s.short)} courses${icon('i-arrow', 'xs')}</a></section>
<section class="cp-up"><h3>Have notes from ${esc(m.code)}?</h3><p>Share them and help the next class find their footing.</p><button type="button" class="cp-btn gold" data-act="upload">${icon('i-upload', 'sm')}Upload notes</button></section>
</aside>
</div></div>`;
},
mount(r, page) {
const host = $('.page-in', page);
const q = $('#cp-q', host);
if (q) q.addEventListener('input', () => { CP.q = q.value; CP.showAll = false; cpRefresh(); });
const so = $('#cp-sort', host);
if (so) so.addEventListener('change', () => { CP.sort = so.value; cpRefresh(); });
if (reduceMotion() || !S.motion) $$('.cp-hero-art animate, .cp-hero-art animateTransform', host).forEach(a => a.remove());
}
};
function cpRefresh() {
const m = courseModel(R.sid, R.code);
$('#cp-docs').innerHTML = cpDocsHtml(m);
const filtered = cpList(m).length, sortWords = { popular: 'most popular first', pages: 'longest first', az: 'A to Z', free: 'free first' };
const sub = $('.cp-docs-h p'); if (sub) sub.textContent = `${filtered === m.n ? `${m.n} ${m.word}` : `Showing ${filtered} of ${m.n} documents`} · ${sortWords[CP.sort]}`;
const ch = $('.cp-chips'); if (ch) ch.outerHTML = cpChips(m);
}
Object.assign(ACT, {
'cp-save': el => { const on = toggleSaved(el.dataset.id); if (R.page === 'course') cpRefresh(); else rerender(); toast(on ? 'Saved. Find it in My Room under Documents.' : 'Removed from saved'); },
'cp-type': el => { CP.type = el.dataset.k; CP.showAll = false; cpRefresh(); const b = $(`.cp-chip[data-k="${el.dataset.k}"]`); if (b) b.focus(); },
'cp-view': el => { CP.view = el.dataset.v; $$('[data-act="cp-view"]').forEach(b => b.setAttribute('aria-pressed', b.dataset.v === CP.view)); cpRefresh(); },
'cp-more': () => { CP.showAll = true; cpRefresh(); },
'cp-reset': () => { CP.q = ''; CP.type = 'all'; CP.showAll = false; const q = $('#cp-q'); if (q) q.value = ''; cpRefresh(); },
'cp-notes': () => { const key = R.sid + '|' + R.code, on = toggleIn(S.courseNotes, key); if (on) addNote(`${R.code} · ${courseBy(R.sid, R.code).title}`); else S.notes = S.notes.filter(n => n.text !== `${R.code} · ${courseBy(R.sid, R.code).title}`); save(); rerender(); toast(on ? `${R.code} added to your notes` : `${R.code} removed from your notes`); },
'cp-share': () => { const url = 'https://studyboost.com/courses/' + R.sid + '/' + slug(R.code); try { navigator.clipboard.writeText(url).then(() => toast('Course link copied'), () => toast(url)); } catch (e) { toast(url); } },
'cp-quiz': () => { if (window.STUDY) STUDY.fromCourse(R.sid, R.code); go('study-create'); }
});
/* ===== 77-docpage.js ===== */
/* ================= Document page (the Reading Rail layout) ================= */
const DPV = { zoom: 100 };
const PAPER_HEAD = (d, c) => `<div class="pg-head"><span>${esc(d.code || 'Shared document')}${c ? ' · ' + esc(c.title) : ''}</span><span>Name <i class="pg-blank s"></i> Section <i class="pg-blank xs"></i></span></div>`;
/* the worksheet from the prototype, page by page */
const MI = [['0.0813', '0.0592', '0.0447'], ['0.0318', '0.0705', '0.0521'], ['0.0664', '0.0386', '0.0574'], ['0.0459', '0.0617', '0.0498']];
const MITOTIC = {
1: () => `<p class="pg-title">Worksheet on Mitotic Index and Phase Indices</p>
<p class="pg-formula"><i>Mitotic index</i> (MI) = number of cells in mitosis ÷ total number of cells counted</p>
<p><b>1.</b> Refer to the raw data from all teams to calculate the mitotic index for each of the 12 control samples. Record them in the following table and create an appropriate table legend.</p>
<p class="pg-cap">Table 1. <span class="pg-hand">Mitotic index of onion root tip control samples</span></p>
<table class="pg-table"><thead><tr><th>Sample</th><th>Mitotic Index</th><th>Sample</th><th>Mitotic Index</th><th>Sample</th><th>Mitotic Index</th></tr></thead><tbody>${MI.map((r, i) => `<tr><td class="g">Control ${i + 1}</td><td>${r[0]}</td><td class="g">Control ${i + 5}</td><td>${r[1]}</td><td class="g">Control ${i + 9}</td><td>${r[2]}</td></tr>`).join('')}</tbody></table>
<p><b>2.</b> Calculate the mean mitotic index of the control samples. Mean MI = <span class="pg-hand">0.0550</span></p>
<p><b>3.</b> Which control samples fall farthest from the mean? Suggest one reason counts might differ between teams.</p>
<p class="pg-hand block">Controls 1 and 2. Teams may have counted different parts of the root tip, and only the meristem just behind the cap divides quickly.</p>
<p><b>4.</b> <i>Phase index</i> = number of cells in a given phase ÷ total number of cells in mitosis. Use this formula to complete Table 2.</p>`,
2: () => { const phase = Array.from({ length: 12 }, (_, i) => { const p = .46 + ((i * 37) % 11) / 100, m = .14 + ((i * 13) % 7) / 100, a = .09 + ((i * 5) % 4) / 100; return [p, m, a, 1 - p - m - a].map(v => v.toFixed(2)); }); return `<p class="pg-sub">Phase Indices</p>
<p><b>5.</b> Refer to the raw data from all teams to calculate phase indices for each of the 12 control samples. Record them in the following table and create an appropriate table legend.</p>
<p class="pg-cap">Table 2. <i class="pg-blank"></i></p>
<table class="pg-table"><thead><tr><th>Sample</th><th>Prophase</th><th>Metaphase</th><th>Anaphase</th><th>Telophase</th></tr></thead><tbody>${phase.map((r, i) => `<tr><td class="g">Control ${i + 1}</td>${r.map(v => `<td>${v}</td>`).join('')}</tr>`).join('')}</tbody></table>
<p><b>6.</b> Which phase has the highest phase index? What does this tell you about the relative length of that phase?</p><div class="pg-lines"><i></i><i></i><i></i></div>`; },
3: () => `<p class="pg-sub">Treatment vs. Control</p>
<p><b>7.</b> Enter your treatment (T) and control (C) mitotic indices in Table 3. Create an appropriate legend.</p>
<table class="pg-table t-narrow"><thead><tr><th>Sample #</th><th>MI treatment</th><th>MI control</th></tr></thead><tbody>${[1, 2, 3, 4].map(n => `<tr><td>${n}</td><td></td><td></td></tr>`).join('')}</tbody></table>
<p><b>8.</b> Rank your treatment and control MI values in Table 4 and provide an appropriate legend.</p>
<table class="pg-table t-narrow"><thead><tr><th>Rank</th><th>Ranked MI values</th><th>Cell population (T or C)</th></tr></thead><tbody>${[1, 2, 3, 4, 5, 6, 7, 8].map(n => `<tr><td>${n}</td><td></td><td></td></tr>`).join('')}</tbody></table>
<p><b>9.</b> In a separate document, create the figure your team judges best to represent the results.</p><div class="pg-lines"><i></i><i></i></div>`
};
/* first pages: the prototype's sample papers for biology, plus pages written for Maya's other courses */
const FIRST_PAGE = {
ws: { cls: '', html: `<p class="t-blue"><u>Instructions:</u></p><ol class="t-blue"><li>Use the worksheet for each index your team chooses.</li><li>At the end of the last worksheet, include your team's discussion and analysis of <u>why</u> you observed these results.</li><li>In a separate document, create the figures (bar graph, histogram, box plot, as appropriate for your study) your team judges best to represent the results.</li></ol><p>Enter your treatment (T) and control (C) mitotic indices in <b>Table 1</b>. Create an appropriate legend.</p><p class="pg-cap">Table 1. <i class="pg-blank"></i></p><table class="pg-table t-narrow"><thead><tr><th>Sample #</th><th>M<sub>treatment</sub></th><th>M<sub>control</sub></th></tr></thead><tbody><tr><td>1</td><td>0.583</td><td>0.7</td></tr><tr><td>2</td><td>0</td><td>0</td></tr><tr><td>3</td><td>0.33</td><td>0.5714</td></tr><tr><td>4</td><td>0</td><td>0.364</td></tr></tbody></table><p>Rank your treatment and control MI values in <b>Table 2</b> and provide an appropriate legend.</p>` },
defs: { cls: '', html: `<p>Prepare for your workshop by reading in your textbook (<u>Campbell Biology</u>, 11th edition, Chapter 2) and completing the Pre-Workshop Activities below.</p><p><b>II. Pre-Workshop Activities</b></p><p><b>Activity 1. Define each of the following:</b></p><ol><li>Acid - <span class="t-red">A substance that increases the hydrogen ion concentration of a solution.</span></li><li>Anion - <span class="t-red">A negatively charged ion.</span></li><li>Atom - <span class="t-red">The smallest unit of matter that retains the properties of an element.</span></li><li>Atomic mass - <span class="t-red">The total mass of an atom, numerically equivalent to the mass in grams of 1 mole of the atom.</span></li><li>Atomic nucleus - <span class="t-red">An atom's dense central core, containing protons and neutrons.</span></li><li>Atomic number - <span class="t-red">The number of protons in the nucleus of an atom.</span></li><li>Base - <span class="t-red">A substance that reduces the hydrogen ion concentration of a solution.</span></li><li>Cation - <span class="t-red">A positively charged ion.</span></li></ol>` },
bullets: { cls: 'f-arial', html: `<p><b>Chapter 1: Evolution, the Themes of Biology</b></p><ul><li>Levels of biological organization<ul><li>Organ system</li><li>Organ</li><li>Tissue</li><li>Cell</li><li>Organelle</li><li>Molecule</li><li>Atoms</li></ul></li><li>Emergent properties: result from interactions among components at the lower levels</li><li>Systems biology: scientists attempt to model the dynamic behavior of whole biological systems</li><li>Mutualism (+/+), commensalism (0/+), parasitism (+/−)</li><li>Inductive reasoning: draw a general conclusion which can be developed into a testable hypothesis</li><li>Deductive reasoning: make predictions which test the hypothesis</li></ul><p><b>Chapter 6: The Cell</b></p>` },
mcq: { cls: 'f-arial', html: `<ol start="3"><li>An inquiry based research approach in biology includes?<ol class="t-alpha"><li>Seeing what motivated the researchers to do this experiment</li><li>Understanding how the experiments are designed</li><li>Determining what new techniques were used in the experiments</li><li>Seeing if the results influenced the future course of this type of research</li><li>All of the above</li></ol></li><li>The scientific method involves the construction of knowledge based upon observation, testing, and measurement. Identify the statement that can NOT be experimentally based.<ol class="t-alpha"><li>Alcohol consumption by pregnant women causes birth defects</li><li>The sun rises in the East every morning</li><li>4 out of 5 dentists recommend crest toothpaste</li><li>The SARS-CoV2 virus may infect other human cells than just lung cells</li></ol></li></ol>` },
hand: { cls: 'grid', html: `<div class="t-hand"><p>B. (A<sup>−3</sup>/B<sup>−4</sup>)<sup>2</sup> · (b/a)<sup>−3</sup></p><p> = (b<sup>8</sup>/a<sup>6</sup>) · (a/b)<sup>3</sup></p><p> = b<sup>5</sup>/a<sup>3</sup><span class="t-redpen">✓</span></p><p>4) 3x/√(x+2) + 3√(x+2)</p><p> = (3x + 3(x+2)) / √(x+2)</p><p> = (6x + 6)/√(x+2)<span class="t-redpen">= 6(x+1)/√(x+2)</span></p></div>` },
hand2: { cls: 'grid', html: `<div class="t-hand"><p>1) f(x) = √(2 − x) / (x + 3)</p><p> 2 − x ≥ 0 → x ≤ 2</p><p> x + 3 ≠ 0 → x ≠ −3</p><p> Domain: (−∞, −3) ∪ (−3, 2]</p><p>2) f(x) = 4 − x if x ≤ 2, x + 1 if x > 2</p><svg class="t-sketch" viewBox="0 0 130 70"><path d="M8 60H124M40 4V68"/><path d="M14 18 64 52M64 52l2 0M66 40l50-30"/><circle cx="64" cy="52" r="2.5" fill="#2A2D3A"/><circle cx="66" cy="40" r="2.5" fill="none"/></svg></div>` },
notes: { cls: 'f-arial', html: `<p class="t-nh">Lecture 9: The Cell Cycle <span class="t-date">Oct 2</span></p><ul><li>Interphase = G1 → S → G2 (about 90% of the cycle)</li><li>M phase = mitosis + cytokinesis</li><li>PMAT: prophase, metaphase, anaphase, telophase</li><li>Prophase: chromatin condenses, spindle forms</li><li>Metaphase: chromosomes line up at the metaphase plate</li><li>Anaphase: sister chromatids separate</li><li>Telophase: nuclear envelopes re-form</li><li>Checkpoints at G1, G2 and M, driven by cyclins and Cdks</li></ul><p class="t-nh">Lecture 10: Meiosis <span class="t-date">Oct 7</span></p>` },
report: { cls: '', html: `<p class="t-rt">Effect of Caffeine on the Mitotic Index of <i>Allium cepa</i> Root Tips</p><p class="t-ra">BIL 151 · Section 4 · Team 3</p><p><b>Abstract</b></p><p>We compared the mitotic index of onion root tip cells grown in 0.1% caffeine with untreated controls. Treated root tips showed a lower mean mitotic index than controls.</p><div class="t-fig"><span style="height:72%"></span><span style="height:44%"></span></div><p class="t-cap">Figure 1. Mean mitotic index, control vs. caffeine (n = 4).</p>` }
};
const OWN_PAGE = {
stoich: { cls: 'f-arial', html: `<p class="t-nh">Stoichiometry Practice Set <span class="t-date">Unit 3</span></p><ol><li>How many moles of O<sub>2</sub> react with 4.0 mol of H<sub>2</sub> in 2H<sub>2</sub> + O<sub>2</sub> → 2H<sub>2</sub>O?<ol class="t-alpha"><li>1.0 mol</li><li><b>2.0 mol</b></li><li>4.0 mol</li><li>8.0 mol</li></ol></li><li>What mass of CO<sub>2</sub> forms when 16.0 g of CH<sub>4</sub> burns completely?<ol class="t-alpha"><li>22.0 g</li><li><b>44.0 g</b></li><li>16.0 g</li><li>88.0 g</li></ol></li><li>A reaction makes 18.2 g of product. The theoretical yield is 20.0 g. What is the percent yield?</li></ol><p class="pg-hand block">91% — actual ÷ theoretical × 100</p>` },
limiting: { cls: '', html: `<p class="pg-title">Limiting Reagents Worksheet</p><p class="pg-formula">Find the reactant that runs out first: compare mole ratios, not grams.</p><p><b>1.</b> 3.0 mol of Al reacts with 4.0 mol of Cl<sub>2</sub>: 2Al + 3Cl<sub>2</sub> → 2AlCl<sub>3</sub>. Which is limiting?</p><p class="pg-hand block">Cl₂ — 4.0 mol Cl₂ only reacts with 2.67 mol Al</p><p><b>2.</b> How many moles of AlCl<sub>3</sub> form? <span class="pg-hand">2.67 mol</span></p><p><b>3.</b> How much of the excess reactant is left over?</p><div class="pg-lines"><i></i><i></i></div>` },
titration: { cls: '', html: `<p class="t-rt">Titration & Standardization of a NaOH Solution</p><p class="t-ra">CHM 113 · Section 2 · Lab 5</p><p><b>Purpose</b></p><p>Standardize a sodium hydroxide solution against potassium hydrogen phthalate (KHP), then use it to find the concentration of an unknown acid.</p><div class="t-fig"><span style="height:38%"></span><span style="height:52%"></span></div><p class="t-cap">Figure 1. Volume of NaOH at the endpoint, trials 1 and 2.</p><p><b>Result</b> [NaOH] = 0.1012 M ± 0.0004</p>` },
'unit3-review': { cls: 'f-arial', html: `<p class="t-nh">Unit 3 Review <span class="t-date">Slides 1–6</span></p><ul><li>The mole: 6.022 × 10<sup>23</sup> particles</li><li>Molar mass links grams and moles</li><li>Balanced equations give mole ratios</li><li>Limiting reagent sets the theoretical yield</li><li>Percent yield = actual ÷ theoretical × 100</li><li>Molarity = moles of solute ÷ liters of solution</li></ul><p class="t-nh">Quiz Friday: bring a calculator</p>` },
conditioning: { cls: 'f-arial', html: `<p class="t-nh">Classical Conditioning <span class="t-date">Week 6</span></p><ul><li>Pavlov: dogs salivate to a tone paired with food</li><li>US (food) → UR (salivation), unlearned</li><li>CS (tone) → CR (salivation), learned</li><li>Acquisition: CS and US paired until the CS alone works</li><li>Extinction: the CR fades when the US stops</li><li>Spontaneous recovery after a rest</li><li>Generalization vs. discrimination</li></ul><p class="t-nh">Next: operant conditioning (Skinner)</p>` },
'research-quiz': { cls: 'f-arial', html: `<ol><li>A researcher randomly assigns students to sleep 4 or 8 hours, then tests recall. This is:<ol class="t-alpha"><li>A correlational study</li><li><b>An experiment</b></li><li>A case study</li><li>A naturalistic observation</li></ol></li><li>In that study, hours of sleep is the:<ol class="t-alpha"><li>Dependent variable</li><li><b>Independent variable</b></li><li>Confounding variable</li><li>Control group</li></ol></li><li>Why does random assignment matter?</li></ol>` },
'memory-lec': { cls: 'f-arial', html: `<p class="t-nh">Memory and Learning <span class="t-date">Lecture 11</span></p><ul><li>Encoding → storage → retrieval</li><li>Sensory memory: under a second for images</li><li>Short-term memory: about 7 ± 2 items, roughly 20 s</li><li>Working memory (Baddeley): phonological loop, sketchpad, central executive</li><li>Long-term memory: explicit (episodic, semantic) and implicit</li><li>Spacing effect: spread out study beats cramming</li></ul>` },
'stats-sheet': { cls: '', html: `<p class="pg-title">Stats Formula Sheet</p><ol><li>Mean - <span class="t-red">x̄ = Σx ÷ n</span></li><li>Variance - <span class="t-red">s² = Σ(x − x̄)² ÷ (n − 1)</span></li><li>Standard deviation - <span class="t-red">s = √s²</span></li><li>z-score - <span class="t-red">z = (x − x̄) ÷ s</span></li><li>Correlation - <span class="t-red">r from −1 to +1; sign shows direction</span></li><li>p-value - <span class="t-red">chance of results this extreme if the null is true</span></li></ol>` },
ethics: { cls: 'f-arial', html: `<p><b>Research Ethics Review</b></p><ul><li>Informed consent before taking part</li><li>Protection from harm, physical or psychological</li><li>Confidentiality of data</li><li>Debriefing, especially after deception</li><li>Right to withdraw at any time<ul><li>Without losing promised credit</li></ul></li><li>IRB review before the study starts</li></ul><p><b>Case: the Stanford prison study</b></p>` }
};
/* pages nobody wrote out: real headings from the course's topics, with text drawn as grey lines */
const CAT_TOPICS = {
1: ['Variables and types', 'Control flow', 'Functions', 'Arrays and lists', 'Recursion', 'Big-O notation'],
2: ['Limits', 'Derivatives', 'Vectors', 'Newton’s laws', 'Energy and work', 'Probability'],
3: ['Free-body diagrams', 'Stress and strain', 'Circuits', 'Thermodynamics', 'Material properties', 'Design process'],
4: ['Cell structure', 'Genetics', 'Evolution', 'Metabolism', 'Ecology', 'Homeostasis'],
5: ['Supply and demand', 'Elasticity', 'Market structures', 'Financial statements', 'Cost behavior', 'Budgets'],
6: ['Research methods', 'Social norms', 'Learning', 'Memory', 'Culture', 'Inequality'],
7: ['Thesis statements', 'Close reading', 'Historical context', 'Rhetorical appeals', 'Form and style', 'Citations'],
8: ['Patient assessment', 'Pharmacology', 'Infection control', 'Care planning', 'Leadership', 'Delegation'],
9: ['Constitutional law', 'Federalism', 'Due process', 'Policy analysis', 'Classroom management', 'Ethics'],
10: ['Audience analysis', 'Message design', 'Media effects', 'Persuasion', 'Interviewing', 'Public speaking'],
11: ['Key terms', 'Main ideas', 'Worked examples', 'Common mistakes', 'Practice questions', 'Review']
};
const cap1 = t => t.charAt(0).toUpperCase() + t.slice(1);
function docTopics(d) {
if (d.topics && d.topics.length) return d.topics;
const info = docSid(d) === 'um' ? COURSE_INFO[d.code] : null;
if (info && info.tags.length >= 3) return info.tags.map(cap1);
const c = courseOfDoc(d);
return CAT_TOPICS[c ? catOfCourse(c) : d.code ? catOfCode(d.code) : 11] || CAT_TOPICS[11];
}
/* grey text lines; the last line of a paragraph runs short */
function skl(seed, n, lo, hi) { const r = rng(seed); return Array.from({ length: n }, (_, i) => `<i class="sk" style="width:${i === n - 1 && n > 1 ? Math.round(28 + r() * 40) : Math.round((lo || 72) + r() * ((hi || 100) - (lo || 72)))}%"></i>`).join(''); }
const skp = (seed, n, lo, hi) => `<span class="sk-p">${skl(seed, n, lo, hi)}</span>`;
function genPage(d, n) {
const seed = hsh(d.id + ':' + n), T = docTopics(d), c = courseOfDoc(d), code = esc(d.code || ''), course = c ? esc(c.title) : '';
const t = i => esc(cap1(T[(i + (n - 1) * 3) % T.length]));
const head = n === 1 ? '' : `<p class="t-nh">${esc(d.title)} <span class="t-date">p. ${n}</span></p>`;
const kind = d.kind === 'hand' || d.kind === 'hand2' ? 'mcq' : d.kind;
if (kind === 'mcq') {
const q0 = n === 1 ? 1 : (n - 1) * 4 + 1;
const qs = Array.from({ length: n === 1 ? 3 : 4 }, (_, i) => `<li>${skp(seed + i, 2, 70)}<ol class="t-alpha">${Array.from({ length: 4 }, (_, k) => `<li>${skl(seed + i * 7 + k, 1, 26, 62)}</li>`).join('')}</ol></li>`).join('');
return { cls: 'f-arial', html: (n === 1 ? `<p class="t-rt">${code} ${esc(d.type)}</p><p class="t-ra">${course}${course ? ' · ' : ''}Name <i class="pg-blank s"></i></p><p class="pg-formula">Choose the best answer. Topics: ${t(0)}, ${t(1)} and ${t(2)}.</p>` : head) + `<ol start="${q0}">${qs}</ol>` };
}
if (kind === 'notes') {
const sec = k => `<p class="t-nh">${t(k)} <span class="t-date">${n === 1 && k === 0 ? 'Week ' + (1 + seed % 9) : ''}</span></p><ul>${Array.from({ length: 4 }, (_, i) => `<li>${skl(seed + k * 11 + i, 1, 48, 92)}</li>`).join('')}<li>${skl(seed + k * 13, 1, 40, 70)}<ul>${Array.from({ length: 2 }, (_, i) => `<li>${skl(seed + k * 17 + i, 1, 30, 64)}</li>`).join('')}</ul></li></ul>`;
return { cls: 'f-arial', html: (n === 1 ? `<p class="t-ra">${code}${course ? ' · ' + course : ''}</p>` : head) + sec(0) + sec(1) + sec(2) };
}
if (kind === 'ws') {
const rows = Array.from({ length: 4 }, (_, i) => `<tr><td>${i + 1}</td><td></td><td></td></tr>`).join('');
return { cls: '', html: (n === 1 ? `<p class="pg-title">${esc(d.title)}</p><p class="pg-formula">${skl(seed, 2, 80)}</p>` : head) + `<p><b>${(n - 1) * 3 + 1}.</b> ${t(0)}</p>${skp(seed + 1, 2)}<table class="pg-table t-narrow"><thead><tr><th>Trial</th><th>Value</th><th>Notes</th></tr></thead><tbody>${rows}</tbody></table><p><b>${(n - 1) * 3 + 2}.</b> ${t(1)}</p>${skp(seed + 2, 3)}<div class="pg-lines"><i></i><i></i></div><p><b>${(n - 1) * 3 + 3}.</b> ${t(2)}</p>${skp(seed + 3, 2)}<div class="pg-lines"><i></i><i></i></div>` };
}
if (kind === 'report') {
return { cls: '', html: (n === 1 ? `<p class="t-rt">${esc(d.title)}</p><p class="t-ra">${code}${course ? ' · ' + course : ''}</p><p><b>Abstract</b></p>${skp(seed, 4)}` : head + `<p><b>${t(0)}</b></p>${skp(seed, 4)}`) + `<div class="t-fig"><span style="height:${40 + seed % 45}%"></span><span style="height:${30 + (seed >>> 4) % 50}%"></span></div><p class="t-cap">Figure ${n}. ${t(1)}</p><p><b>${n === 1 ? 'Methods' : 'Discussion'}</b></p>${skp(seed + 5, 5)}` };
}
if (kind === 'defs') {
const items = Array.from({ length: 7 }, (_, i) => `<li>${t(i)} - <span class="t-red">${skl(seed + i, 1, 40, 80)}</span></li>`).join('');
return { cls: '', html: (n === 1 ? `<p class="pg-title">${esc(d.title)}</p>` : head) + `<ol class="dp-defs" start="${(n - 1) * 7 + 1}">${items}</ol>` };
}
/* study guides and anything else */
const sec = k => `<p><b>${n === 1 ? 'Part ' + (k + 1) + ': ' : ''}${t(k)}</b></p><ul>${Array.from({ length: 3 }, (_, i) => `<li>${skl(seed + k * 9 + i, 1, 50, 95)}</li>`).join('')}<li>${skl(seed + k * 5, 1, 40, 70)}<ul>${Array.from({ length: 2 }, (_, i) => `<li>${skl(seed + k * 15 + i, 1, 30, 60)}</li>`).join('')}</ul></li></ul>`;
return { cls: 'f-arial', html: (n === 1 ? `<p class="t-nh">${esc(d.title)}</p><p class="t-ra">${code}${course ? ' · ' + course : ''}</p>` : head) + sec(0) + sec(1) + sec(2) };
}
function firstPage(d) {
const c = courseOfDoc(d);
if (d.id === 'mitotic') return { cls: '', html: PAPER_HEAD(d, c) + MITOTIC[1]() };
if (OWN_PAGE[d.id]) return OWN_PAGE[d.id];
if (/^BIL|^BIO/.test(d.code || '') && FIRST_PAGE[d.kind] && !d.gen) return FIRST_PAGE[d.kind];
return genPage(d, 1);
}
function paperHtml(d, n, locked, mini) {
let inner, cls = '';
if (d.id === 'mitotic' && MITOTIC[n]) inner = PAPER_HEAD(d, courseOfDoc(d)) + MITOTIC[n]();
else { const f = n === 1 ? firstPage(d) : genPage(d, n); inner = f.html; cls = f.cls; }
return `<div class="paper${locked ? ' locked' : ''}"${mini ? '' : ` data-page="${n}"`}${locked || mini ? ' aria-hidden="true"' : ''}><div class="pg ${cls}">${inner}${mini ? '' : `<div class="pg-foot">${n}</div>`}</div></div>`;
}
const DP_STEP = 3;
const moreBtn = (shown, total) => `<div class="dp-more"><span>Page ${shown} of ${total}</span><button type="button" class="btn btn-ghost btn-sm" data-act="dp-more" data-shown="${shown}">${icon('i-down', 'sm')}Show ${Math.min(DP_STEP, total - shown) === 1 ? 'the last page' : `the next ${Math.min(DP_STEP, total - shown)} pages`}</button></div>`;
const thumbPaperD = d => paperHtml(d, 1, false, true);
const isOpenDoc = d => !d.pro || S.plan === 'pro' || S.unlocked.includes(d.id);
function voteCounts(d) { const v = S.votes[d.id]; return { up: d.up + (v === 'up' ? 1 : 0), down: d.down + (v === 'down' ? 1 : 0), mine: v || null }; }
function ratingCard(d) {
const c = voteCounts(d), t = c.up + c.down, pct = Math.round(c.up / Math.max(1, t) * 100);
const b = dir => { const on = c.mine === dir, n = dir === 'up' ? c.up : c.down; return `<button type="button" class="vote ${dir}${on ? ' on' : ''}" data-act="dp-vote" data-dir="${dir}" aria-pressed="${on}" aria-label="${dir === 'up' ? 'Helpful' : 'Not helpful'} (${n})">${icon(dir === 'up' ? 'i-thumb' : 'i-thumb-down')}<span class="tnum">${n}</span></button>`; };
return `<p class="rc-label">Student rating</p><p class="rc-big"><b>${pct}%</b>found this helpful</p><div class="rc-bar"><i style="width:${pct}%"></i></div><p class="rc-sub">${c.up} thumbs up, ${c.down} thumbs down · ${t} ratings</p><div class="rc-ask"><span>Was it helpful to you?</span><div class="vote-row">${b('up')}${b('down')}</div></div>`;
}
function dpRelated(d) {
const s = schoolOfDoc(d), c = courseOfDoc(d);
const same = c ? courseDocs(s.id, c.code).filter(x => x.id !== d.id) : [];
const pool = same.length ? same : TRENDING.map(ensureDoc).concat(UM_POPULAR.map(p => docByTitle(p.title)).filter(Boolean)).filter(x => x.id !== d.id);
const similar = pool.filter(x => x.hub === d.hub).concat(pool.filter(x => x.hub !== d.hub)).slice(0, 6);
const more = same.slice(0, 4);
const cat = c ? catOfCourse(c) : 11;
const rel = s ? coursesOf(s).filter(x => !c || x.code !== c.code).sort((a, b) => (catOfCourse(b) === cat) - (catOfCourse(a) === cat) || b.docs - a.docs).slice(0, 5) : [];
return { similar, more, rel, c, s };
}
const hcard = d => `<a class="hcard" href="#${tokDoc(d.id)}"><div class="hthumb">${thumbPaperD(d)}</div><div class="hcard-body"><span class="dcard-course">${esc(d.code || '')} · ${esc(d.type)}</span><span class="hcard-title">${esc(d.title)}</span><span class="hcard-meta"><span class="tnum">${plural(d.pages, 'page')}</span><span class="mini-rate">${icon('i-thumb', 'xs')}${docRate(d)}%</span>${d.pro ? `<span class="lock-mini">${icon('i-lock', 'xs')}Pro</span>` : '<span class="free-mini">Free</span>'}</span></div></a>`;
const crowC = (sid, x) => `<a class="crow" href="#${tokCourse(sid, x.code)}"><span class="crow-glyph">${catGlyph(catOfCourse(x), 40)}</span><span class="crow-code">${esc(x.code)}</span><span class="crow-main"><span class="crow-name">${esc(x.title)}</span><span class="crow-sub">${esc(CH_CATS[catOfCourse(x)] ? CH_CATS[catOfCourse(x)].name : '')}</span></span><span class="crow-count">${plural(x.docs, 'document')}</span>${icon('i-right', 'sm')}</a>`;
PAGES.doc = {
cls: () => 'pg-doc',
render(r) {
const d = DOCS[r.id] || lazyDoc(r.id);
if (!S.viewed.includes(d.id)) { S.viewed.push(d.id); save(); }
const { similar, more, rel, c, s } = dpRelated(d);
const open = isOpenDoc(d), total = d.pages || 1, saved = S.saved.includes(d.id);
const shown = open ? Math.min(DP_STEP, total) : 1;
const left = unlocksLeft();
const typeWord = (d.type || 'document').toLowerCase();
const crumbParts = [['documents', 'Documents']];
if (s) crumbParts.push([tokSchool(s.id), s.name]);
if (c) crumbParts.push([tokCourse(s.id, c.code), `${c.code} · ${c.title}`]);
crumbParts.push([TOKEN, d.title]);
const summary = d.summary || `${d.type} for ${d.code || 'this course'}${c ? ' · ' + c.title : ''}. ${plural(total, 'page')}, rated helpful by ${docRate(d)}% of students who used it.`;
const topics = docTopics(d).slice(0, 6);
const pagesHtml = open
? Array.from({ length: shown }, (_, i) => paperHtml(d, i + 1, false)).join('') + (total > shown ? moreBtn(shown, total) : '')
: paperHtml(d, 1, false) + `<div class="lock-band"><span class="lb-ic">${icon('i-lock')}</span><p><b>${total > 2 ? `Pages 2 to ${total} are` : 'Page 2 is'} included with Pro</b>${d.id === 'mitotic' ? 'Phase indices for all 12 controls, then the treatment vs. control analysis.' : `The rest of this ${esc(typeWord)}, plus every premium document in ${esc(d.code || 'your courses')}.`}</p><button type="button" class="btn btn-ghost btn-sm" data-act="dp-gate">See what’s inside</button></div>` + paperHtml(d, 2, true);
return `<div class="page-in">${crumbs(crumbParts)}
<div class="wrap wrap-b">
<header class="doc-head-b">
<h1 class="doc-title">${esc(d.title)}</h1>
<div class="meta">${d.pro ? (open ? `<span class="tag tag-free">${icon('i-check', 'xs')}Unlocked</span>` : `<span class="tag tag-pro">${icon('i-lock', 'xs')}Premium</span>`) : '<span class="tag tag-free">Free</span>'}
<span>${esc(d.type)}</span><i class="sep"></i><span>${plural(total, 'page')}</span><i class="sep"></i><span>Added ${esc(d.added || 'Sep 14, 2026')}</span><i class="sep"></i><span class="tnum">${fmt(d.views || 400)} views</span><i class="sep"></i><span class="mini-rate">${icon('i-thumb', 'xs')}<span>${Math.round(voteCounts(d).up / Math.max(1, voteCounts(d).up + voteCounts(d).down) * 100)}% helpful</span></span></div>
</header>
<div class="b-grid">
<div class="viewer" id="viewer-rail">
<div class="vbar"><div class="vbar-l"><span class="pageno">Page <b id="dp-pn">1</b> of ${total}</span><span class="vsep"></span><div class="zoom"><button type="button" data-act="dp-zoom" data-dir="-1" aria-label="Zoom out">${icon('i-minus', 'sm')}</button><span id="dp-zoom">${DPV.zoom}%</span><button type="button" data-act="dp-zoom" data-dir="1" aria-label="Zoom in">${icon('i-plus', 'sm')}</button></div></div>
<div class="vbar-r"><button type="button" class="chip-btn" data-act="dp-focus">${icon('i-max', 'sm')}Focus</button><button type="button" class="chip-btn ai" data-act="dp-ai" aria-haspopup="menu" aria-expanded="false">${icon('i-sparkles', 'sm')}AI tools</button></div></div>
<div class="desk" data-zoom-host style="--pw:calc(min(680px, 100%) * ${DPV.zoom / 100})"><div class="pages">${pagesHtml}</div></div>
</div>
<aside class="b-rail" aria-label="About this document">
<div class="rail-actions">
<button type="button" class="btn btn-ghost" data-act="dp-save" aria-pressed="${saved}">${icon('i-bookmark', 'sm')}<span>${saved ? 'Saved' : 'Save'}</span></button>
<button type="button" class="btn btn-ghost" data-act="dp-notes">${icon('i-notebook', 'sm')}To notes</button>
<button type="button" class="btn btn-ghost btn-icon" data-act="dp-share" aria-label="Copy link">${icon('i-share', 'sm')}</button>
</div>
<div class="rcard">${ratingCard(d)}</div>
${open ? '' : `<div class="gate gate-b" id="gate-b">
<div class="gate-b-top"><span class="tag tag-pro">${icon('i-sparkles', 'xs')}Pro</span><span class="price"><b>$9.99</b> / month</span></div>
<h3>Unlock the full ${esc(typeWord)}</h3>
<div><p class="prog-label"><span>You’ve read</span><span class="tnum">1 of ${plural(total, 'page')}</span></p><div class="progress3" aria-hidden="true" style="--n:${Math.min(total, 6)}">${Array.from({ length: Math.min(total, 6) }, (_, i) => `<i class="${i === 0 ? 'on' : ''}"></i>`).join('')}</div></div>
${d.id === 'mitotic' ? `<ol class="locked-list"><li><span class="pg-no">2</span><span>Phase index table for the 12 controls</span>${icon('i-lock', 'xs')}</li><li><span class="pg-no">3</span><span>Treatment vs. control ranking and analysis</span>${icon('i-lock', 'xs')}</li></ol>` : ''}
<p class="small">Pro also includes Ask Hoot, flashcards and quizzes, 1,500 AI credits a month and structured notes.</p>
<button type="button" class="btn btn-primary btn-block btn-lg" data-act="pro">Unlock with Pro</button>
${S.plan !== 'pro' && left > 0 ? `<button type="button" class="btn btn-ghost btn-block" data-act="dp-unlock">${icon('i-unlock', 'sm')}Use a free unlock · ${left} left this month</button>` : `<p class="small" style="text-align:center">No free unlocks left this month. They reset ${resetDay()}.</p>`}
<p class="trust" style="justify-content:center"><span>${icon('i-rotate', 'xs')}Cancel anytime</span><span>${icon('i-shield', 'xs')}Secure checkout</span></p>
</div>`}
<div class="rcard about"><p class="rc-label">${icon('i-sparkles', 'xs')}Summary</p><p>${esc(summary)}</p>${topics.length ? `<div class="topics">${topics.map(t => `<span class="topic">${esc(t)}</span>`).join('')}</div>` : ''}<button type="button" class="link" data-act="hoot-open" data-q="Summarize ${esc(d.title)}">Ask Hoot to explain it</button></div>
${c ? `<a class="rcard course-mini" href="#${tokCourse(s.id, c.code)}"><span class="cm-glyph">${catGlyph(catOfCourse(c), 52)}</span><span class="cm-body"><span class="rc-label" style="letter-spacing:.06em">From the course</span><b>${esc(c.code)} · ${esc(c.title)}</b><span class="small">${esc(s.name)} · ${plural(courseDocs(s.id, c.code).total || c.docs, 'document')}</span></span>${icon('i-right', 'sm')}</a>` : ''}
</aside>
</div>
<section class="block" data-tabgroup>
<div class="block-head"><div><h2>Keep studying</h2><p>${c ? `Picked from ${esc(c.code)} and nearby courses at ${esc(s.short || s.name)}` : 'Popular with students right now'}</p></div></div>
<div class="tabs" role="tablist">
<button type="button" class="tab" role="tab" data-act="dp-tab" data-tab="similar" aria-selected="true">Similar documents <span class="count">${similar.length}</span></button>
${c ? `<button type="button" class="tab" role="tab" data-act="dp-tab" data-tab="more" aria-selected="false">More from ${esc(c.code)} <span class="count">${more.length}</span></button>` : ''}
${rel.length ? `<button type="button" class="tab" role="tab" data-act="dp-tab" data-tab="courses" aria-selected="false">Related courses <span class="count">${rel.length}</span></button>` : ''}
</div>
<div data-panel="similar"><div class="list-b">${similar.map(hcard).join('') || '<p class="muted">Nothing similar yet.</p>'}</div></div>
${c ? `<div data-panel="more" hidden><div class="list-b">${more.map(hcard).join('')}</div></div>` : ''}
${rel.length ? `<div data-panel="courses" hidden><div class="crow-list">${rel.map(x => crowC(s.id, x)).join('')}</div></div>` : ''}
</section>
</div></div>`;
},
onScroll() {
const ps = $$('#page .pages .paper[data-page]'); if (!ps.length) return;
let cur = 1; const mid = innerHeight * 0.5, first = ps[0].getBoundingClientRect().top;
ps.forEach(p => { const t = p.getBoundingClientRect().top; if (t < mid && t > first + 40) cur = Math.max(cur, +p.dataset.page); });
const n = $('#dp-pn'); if (n) n.textContent = cur;
}
};
$('#page').addEventListener('scroll', () => { const P = PAGES[R.page]; if (P && P.onScroll) { if (P._raf) return; P._raf = requestAnimationFrame(() => { P._raf = 0; P.onScroll(); }); } }, { passive: true });
function setFocusMode(on) {
const app = $('#app'); app.classList.toggle('focus', on);
let b = $('#exit-focus');
if (on && !b) { b = document.createElement('button'); b.type = 'button'; b.id = 'exit-focus'; b.className = 's-btn exit-focus'; b.innerHTML = icon('i-x', 'sm') + 'Exit focus'; b.addEventListener('click', () => { setFocusMode(false); const f = $('[data-act="dp-focus"]'); if (f) f.focus(); }); document.body.appendChild(b); }
if (!on && b) b.remove();
const fb = $('[data-act="dp-focus"]'); if (fb) fb.setAttribute('aria-pressed', on);
layoutVars();
if (on) toast('Focus mode on. Press Esc to exit.');
}
Object.assign(ACT, {
'dp-vote': el => {
const d = DOCS[R.id], dir = el.dataset.dir;
if (S.votes[d.id] === dir) delete S.votes[d.id]; else S.votes[d.id] = dir;
save(); const rc = el.closest('.rcard'); rc.innerHTML = ratingCard(d);
const c = voteCounts(d), pct = Math.round(c.up / Math.max(1, c.up + c.down) * 100);
toast(S.votes[d.id] === 'up' ? `Thanks. ${pct}% of students now rate this helpful.` : S.votes[d.id] === 'down' ? `Thanks. Your rating helps other ${d.code || ''} students choose.` : 'Rating removed');
const b = $(`.vote.${dir}`, rc); if (b) b.focus();
},
'dp-save': () => { const on = toggleSaved(R.id); rerender(); toast(on ? 'Saved. Find it in My Room under Documents.' : 'Removed from your saved documents'); },
'dp-notes': () => { addNote('Read: ' + DOCS[R.id].title); toast(`Added to your notes`); },
'dp-share': () => { const url = 'https://studyboost.com/doc/' + R.id; try { navigator.clipboard.writeText(url).then(() => toast('Link copied'), () => toast('Copying is blocked here. The link is ' + url)); } catch (e) { toast(url); } },
'dp-unlock': () => { if (unlocksLeft() <= 0) return; S.unlocksUsed++; S.unlocked.push(R.id); save(); rerender(); toast(`Unlocked. You have ${unlocksLeft()} free ${unlocksLeft() === 1 ? 'unlock' : 'unlocks'} left this month.`); },
'dp-gate': () => { const g = $('#gate-b'); if (!g) return; g.scrollIntoView({ behavior: reduceMotion() ? 'auto' : 'smooth', block: 'center' }); g.classList.remove('pulse'); void g.offsetWidth; g.classList.add('pulse'); const b = $('.btn-primary', g); if (b) b.focus({ preventScroll: true }); },
'dp-zoom': el => { const Z = [80, 90, 100, 115, 130]; let i = Z.indexOf(DPV.zoom); i = Math.max(0, Math.min(Z.length - 1, i + +el.dataset.dir)); DPV.zoom = Z[i]; const h = $('[data-zoom-host]'); if (h) h.style.setProperty('--pw', `calc(min(680px, 100%) * ${DPV.zoom / 100})`); $('#dp-zoom').textContent = DPV.zoom + '%'; },
'dp-focus': () => setFocusMode(!$('#app').classList.contains('focus')),
'dp-more': el => {
const d = DOCS[R.id], from = +el.dataset.shown, total = d.pages || 1, to = Math.min(total, from + DP_STEP);
const box = el.closest('.dp-more');
box.insertAdjacentHTML('beforebegin', Array.from({ length: to - from }, (_, i) => paperHtml(d, from + i + 1, false)).join(''));
if (to < total) { box.outerHTML = moreBtn(to, total); const nb = $('[data-act="dp-more"]'); if (nb) nb.focus({ preventScroll: true }); } else box.remove();
},
'dp-tab': el => { const g = el.closest('[data-tabgroup]'); $$('[data-act="dp-tab"]', g).forEach(b => b.setAttribute('aria-selected', b === el)); $$(':scope > [data-panel]', g).forEach(p => { p.hidden = p.dataset.panel !== el.dataset.tab; }); },
'dp-ai': el => {
const d = DOCS[R.id], w = (d.type || 'document').toLowerCase(), pro = S.plan !== 'pro';
const item = (q, ic, t, m) => `<button type="button" class="pm-item" role="menuitem" data-act="hoot-open" data-q="${esc(q)}"><span class="pm-ic">${icon(ic, 'sm')}</span><span class="pm-txt"><b>${t}</b><small>${m}</small></span>${pro ? '<span class="tag tag-pro">Pro</span>' : ''}</button>`;
openPop(el, `<div class="pop-menu-in"><p class="pm-head">${icon('i-sparkles', 'sm')}AI tools for this ${esc(w)}</p>
${item('Help me with ' + d.title, 'i-chat', 'Ask the tutor', 'Help with any table or question')}
${item('Make flashcards from ' + d.title, 'i-cards', 'Flashcards', '12 cards from the key terms')}
${item('Quiz me on ' + d.title, 'i-quiz', 'Practice quiz', '8 questions from this ' + w)}
${item('Summarize ' + d.title, 'i-text', 'Summary', 'A one-page recap for your notes')}
${pro ? '<p class="pm-foot">Included with Pro, along with 1,500 AI credits a month. Hoot answers a sample here.</p>' : ''}</div>`, { cls: 'pg-doc dp-ai-pop', role: 'menu', label: 'AI tools' });
}
});
/* ===== 78a-study-data.js ===== */
/* ================= Study Room data: originals, community games and tools, question sets ================= */
/* Thumbnails and art are carried over from the Idea Builder, Studio and Community prototypes. */
const SR_G = 'linear-gradient(145deg,#1f3d4b,#0d1b24)';
const SR_ORIG = [
{ id:'trivia', kind:'game', name:'Trivia Challenge', cat:'Quick play', ai:SR_G, glyph:'grid', thumb:`<div class="t-in"><div class="t-q">Which molecule carries amino acids to the ribosome?</div><div class="t-opt"><span>A</span>mRNA</div><div class="t-opt bad"><span>B</span>rRNA<i>✕</i></div><div class="t-opt good"><span>C</span>tRNA<i>✓</i></div><div class="t-opt"><span>D</span>DNA</div></div>` },
{ id:'hangman', kind:'game', name:'Vocab Hangman', cat:'Quick play', ai:SR_G, glyph:'flag', thumb:`<div class="t-in" style="flex-direction:row;gap:5cqi"><svg viewBox="0 0 40 60" style="width:24cqi;flex:none" fill="none" stroke="#7fe3d2" stroke-width="1.6" stroke-linecap="round"><path d="M4 58h24M10 58V4h18v8"/><circle cx="28" cy="17" r="4"/><path d="M28 21v14M28 26l-6 5M28 26l6 5M28 35l-5 9M28 35l5 9"/></svg><div style="display:flex;flex-direction:column;gap:2.4cqi;flex:1"><div class="t-eye">Guess · Medium</div><div class="t-q" style="font-size:3.6cqi">A protein that speeds up a chemical reaction without being used up.</div><div class="t-tiles" style="justify-content:flex-start"><span class="t-tile">E</span><span class="t-tile">N</span><span class="t-tile">Z</span><span class="t-tile">Y</span><span class="t-tile">M</span><span class="t-tile">E</span></div><span class="t-pill" style="align-self:flex-start">Solved · Enzyme</span></div></div>` },
{ id:'tiles', kind:'game', name:'Letter Tile Game', cat:'Quick play', ai:SR_G, glyph:'grid', thumb:`<div class="t-in" style="align-items:center;justify-content:center;inset:0;gap:3cqi"><div class="t-eye">Definition · 6 cells</div><div class="t-q" style="font-size:4.8cqi">A single particle of light.</div><div class="t-tiles"><span class="t-tile">P</span><span class="t-tile">H</span><span class="t-tile">O</span><span class="t-tile">T</span><span class="t-tile">O</span><span class="t-tile">N</span></div><div class="t-tiles"><span class="t-tile ghost">O</span><span class="t-tile ghost">T</span><span class="t-tile ghost">N</span><span class="t-tile ghost">P</span><span class="t-tile ghost">H</span></div><span class="t-pill">Lock answer</span></div>` },
{ id:'memory', kind:'game', name:'Memory Game', cat:'Quick play', ai:SR_G, glyph:'cards', thumb:`<div class="t-in" style="flex-direction:row;gap:3cqi;inset:6cqi 6cqi 6cqi"><div class="t-card" style="flex:1;border:.5cqi solid var(--stage-accent);background:#0f2a33;font:700 5cqi/1 var(--body)">Rubisco</div><div class="t-card" style="flex:1;border:.5cqi solid var(--stage-accent);background:#f4ecd8;color:#16302c;font:600 3.3cqi/1.3 var(--body)">Enzyme that fixes carbon dioxide by attaching it to a sugar.</div><div class="t-card" style="flex:.8;background:repeating-linear-gradient(45deg,#0f242b 0 1.2cqi,#12303a 1.2cqi 2.4cqi)"></div></div>` },
{ id:'crossword', kind:'game', name:'Crossword Puzzle', cat:'Word games', ai:SR_G, glyph:'grid', thumb:`<div class="t-in" style="flex-direction:row;gap:3cqi;inset:5cqi"><div class="t-grid" style="grid-template-columns:repeat(7,1fr);flex:1.3">${'ATOM###M#AS#S#ION###G#E#N#E'.padEnd(35,'#').split('').map((c,i)=>`<span class="t-cell ${c==='#'?'x':''} ${i===16?'gold':''}">${c==='#'?'':c}</span>`).join('')}</div><div style="flex:1;display:flex;flex-direction:column;gap:1.6cqi"><span class="t-pill" style="align-self:stretch">Across</span><div class="t-line" style="font-size:2.6cqi">1 Smallest unit of an element</div><div class="t-line" style="font-size:2.6cqi;color:var(--stage-accent)">5 A charged particle</div><div class="t-line" style="font-size:2.6cqi">7 Unit of heredity</div></div></div>` },
{ id:'wordsearch', kind:'game', name:'Word Search', cat:'Word games', ai:SR_G, glyph:'search', thumb:`<div class="t-in" style="flex-direction:row;gap:3cqi;inset:4cqi 5cqi"><div class="t-grid" style="grid-template-columns:repeat(8,1fr);flex:1.5">${'ADXPTLAVSYNAPSECGLIARTMOPAXONFERWDENDRIT'.split('').map((c,i)=>`<span class="t-cell ${i>=8&&i<15?'hl':''} ${i>=25&&i<29?'alt':''}" style="${i>=25&&i<29?'background:#2c5aa0;color:#fff':''}">${c}</span>`).join('')}</div><div style="flex:1;display:flex;flex-direction:column;gap:1.8cqi;font:700 2.6cqi/1 var(--body)"><span style="color:var(--stage-accent)">✓ SYNAPSE</span><span style="color:var(--stage-gold)">✓ AXON</span><span style="color:#56717a">• • • • • •</span><span style="color:#56717a">• • • • • • • •</span></div></div>` },
{ id:'blank', kind:'game', name:'Fill-in-the-Blank', cat:'Word games', ai:SR_G, glyph:'file', thumb:`<div class="t-in" style="justify-content:center;inset:6cqi"><div class="t-box" style="gap:3cqi"><div class="t-eye">Fill the blank · Card 1 / 10</div><div class="t-q" style="font-size:4.2cqi">The <span style="border:1px solid var(--stage-accent);border-radius:1cqi;padding:.4cqi 1.6cqi;color:var(--stage-accent)">nucleus</span> of a cell holds its genetic material.</div><span class="t-pill" style="align-self:flex-start;background:rgba(47,208,184,.16);color:var(--stage-accent)">Correct · recall locked in</span></div></div>` },
{ id:'typing', kind:'game', name:'Typing Game', cat:'Word games', ai:SR_G, glyph:'monitor', thumb:`<div class="t-in" style="inset:5cqi"><div class="t-box"><div class="t-mono" style="color:#6fbfb2">Photosynthesis converts light energy into chemical energy stored in glucose, releasing oxygen as a by-product.</div></div><div class="t-box" style="background:#06100f"><div class="t-mono" style="color:#e9f2f1">Photosynthesis converts light energy into chemical energy stored in glu<span style="background:var(--stage-gold);color:#000">c</span></div></div></div>` },
{ id:'siege', kind:'game', name:'Misconception Siege', cat:'Strategy', ai:'linear-gradient(145deg,#4a2a1c,#1d120c)', glyph:'shield', thumbClass:'ember', thumb:`<div class="t-in" style="flex-direction:row;gap:3cqi;inset:5cqi"><div class="t-card" style="flex:1;border:.5cqi solid #c79a4a;background:#3a1512;align-items:start;justify-items:start;text-align:left;padding:3cqi"><span style="font:600 2.3cqi/1 var(--body);letter-spacing:.16em;color:#e8b8a0">MISCONCEPTION</span><span class="t-serif" style="font-size:3.6cqi;color:#f3e3cf;line-height:1.3">“Water only evaporates once it reaches boiling point.”</span></div><div class="t-card" style="flex:1;border:.5cqi solid #c79a4a;background:#3a1512;align-items:start;justify-items:start;text-align:left;padding:3cqi"><span style="font:600 2.3cqi/1 var(--body);letter-spacing:.16em;color:#e8b8a0">MISCONCEPTION</span><span class="t-serif" style="font-size:3.6cqi;color:#f3e3cf;line-height:1.3">“Clouds are made of invisible water vapour gas.”</span></div></div>` },
{ id:'courses', kind:'tool', name:'AI Courses', cat:'Understand your material', ai:'linear-gradient(145deg,#8c6cf7,#5a3fd6)', glyph:'cap', thumbClass:'violet', thumb:`<div class="t-in"><div class="t-eye">AI Course · 4 modules</div><div class="t-box"><div class="t-line"><span class="t-num">1</span><b style="font-size:3.7cqi">Introduction to Photosynthesis</b></div><div class="t-line"><span class="t-dot done"></span>Overview of Photosynthesis</div><div class="t-line"><span class="t-dot open"></span>Chloroplasts and Chlorophyll</div></div><div class="t-box"><div class="t-line"><span class="t-num dim">2</span><b style="font-size:3.7cqi">Stages of Photosynthesis</b></div></div></div>` },
{ id:'summarize', kind:'tool', name:'Summarize', cat:'Understand your material', ai:'linear-gradient(145deg,#25b39b,#0f8070)', glyph:'file', thumb:`<div class="t-in"><div class="t-eye">Analyzer · Photosynthesis Notes.pdf</div><div class="t-box" style="gap:2.6cqi"><span style="font:700 2.3cqi/1 var(--mono);letter-spacing:.16em;color:var(--stage-muted)">KEY POINTS</span><div class="t-line" style="font-size:3.8cqi"><span class="t-dot done"></span>Chlorophyll absorbs red and blue</div><div class="t-line" style="font-size:3.8cqi"><span class="t-dot done"></span>Rubisco fixes CO₂ into sugar</div><div class="t-line" style="font-size:3.8cqi"><span class="t-dot done"></span>Light, heat and water set the rate</div></div></div>` },
{ id:'guide', kind:'tool', name:'Study Guide Generator', cat:'Understand your material', ai:'linear-gradient(145deg,#5a6cf0,#3443c4)', glyph:'book', thumb:`<div class="t-in"><div class="t-eye">Study guide · 5 sections</div><div class="t-box"><div class="t-line"><span class="t-num" style="background:var(--primary)">1</span><b style="font-size:3.7cqi">Introduction</b></div><div class="t-line"><span class="t-dot done"></span>Light energy becomes glucose</div></div><div class="t-box"><div class="t-line"><span class="t-num dim" style="background:#1a2d33;color:#8fb0ad">2</span><b>Chloroplasts</b></div></div><div class="t-box"><div class="t-line"><span class="t-num dim" style="background:#1a2d33;color:#8fb0ad">3</span><b>The Calvin cycle</b></div></div></div>` },
{ id:'ask', kind:'tool', name:'Ask Documents', cat:'Understand your material', ai:'linear-gradient(145deg,#1fb59f,#0b7c6c)', glyph:'message', thumb:`<div class="t-in"><div class="t-eye">Ask documents</div><div class="t-bubble">Why does the Calvin cycle need light?</div><div class="t-box" style="max-width:86%"><div class="t-line" style="font-size:3.4cqi">It runs on the ATP and NADPH the light reactions make.</div></div><span style="font-size:2.2cqi;color:var(--stage-muted)">1 source · Photosynthesis Notes.pdf</span></div>` },
{ id:'flash', kind:'tool', name:'Flashcards', cat:'Practice and apply', ai:'linear-gradient(145deg,#ef5d70,#c43246)', glyph:'cards', thumb:`<div class="t-in"><div style="display:flex;justify-content:space-between;align-items:center"><span class="t-eye">Flashcards · 12 cards</span><b style="font-size:4cqi">4<span style="font-size:2.4cqi;color:var(--stage-muted)">/12</span></b></div><div class="t-bar" style="--w:33%"><i></i></div><div class="t-box" style="flex:1;align-items:center;justify-content:center;text-align:center;margin-bottom:5cqi"><div class="t-q" style="font-size:4.4cqi">What are the main structures inside a chloroplast?</div></div></div>` },
{ id:'scenario', kind:'tool', name:'Scenario Simulator', cat:'Practice and apply', ai:'linear-gradient(145deg,#f0a43a,#d27716)', glyph:'users', thumbClass:'warm', thumb:`<div class="t-in" style="inset:0"><div style="padding:3cqi 5cqi;background:#2a1d10;font:700 3.2cqi/1 var(--body);color:#f2c35b">You are: Plant Biologist</div><div style="margin:0 5cqi;border-left:.6cqi solid #f2c35b;padding:2.6cqi 3cqi;background:rgba(255,255,255,.03);border-radius:1.4cqi;display:flex;flex-direction:column;gap:2cqi"><span style="font:700 2cqi/1 var(--body);letter-spacing:.14em;color:#e8c27a">FEEDBACK</span><span style="font-size:3.6cqi;line-height:1.3">Cooling reopens the stomata and protects rubisco.</span><span class="t-pill" style="align-self:flex-start;background:rgba(47,208,184,.16);color:var(--stage-accent)">Score: 10 / 10</span></div></div>` },
];
const SR_T = {
q:(eye, q, good, bad, cls='') => `<div class="t-in"><div class="t-eye">${eye}</div><div class="t-q">${q}</div><div class="t-opt good"><span>A</span>${good}<i>✓</i></div><div class="t-opt"><span>B</span>${bad}</div></div>`,
};
const SR_ITEMS = [
{ id:'lyso', kind:'game', name:'Lysosome Defense', by:'priya', course:'BIOL 1510', plays:12400, up:1190, down:50, remixes:214, from:'cellwars', checked:3, desc:'A tower defense where you protect the cell by answering organelle questions. Every right answer fires a lysosome at an incoming pathogen.', docs:['Cell Structure Lecture 4.pdf','BIOL 1510 Lab Manual, Unit 2.pdf','Organelles Study Guide.docx'], thumb:'lyso' },
{ id:'cellwars', kind:'game', name:'Cell Wars', by:'marcus', course:'BIL 150', plays:8900, up:640, down:41, remixes:96, checked:2, desc:'The original cell-defense game. Block viruses by naming the organelle that stops them.', docs:['BIL 150 Cell Notes.pdf','Chapter 6 Outline.docx'], thumb:SR_T.q('Wave 3 · Cell Wars','Which organelle packages proteins for export?','Golgi apparatus','Lysosome') },
{ id:'supply', kind:'game', name:'Supply Shock', by:'jordan', course:'ECO 211', plays:1800, up:164, down:16, remixes:12, checked:2, desc:'Market events hit and you call the shift before the price moves.', docs:['ECO 211 Unit 2 Notes.pdf','Midterm Review.docx'], thumb:SR_T.q('Market event · 4 / 8','A frost wipes out half the orange crop. Price?','Rises, quantity falls','Falls, quantity rises') },
{ id:'pret', kind:'game', name:'Preterite Showdown', by:'diego', course:'SPAN 1412', plays:3200, up:310, down:10, remixes:41, checked:1, desc:'Two tenses, one clock. Pick preterite or imperfect before your opponent does.', docs:['Preterite vs Imperfect.pdf'], thumb:`<div class="t-in" style="align-items:center;justify-content:center;inset:0;gap:3cqi"><div class="t-eye">Conjugate · 0:12 left</div><div class="t-q" style="font-size:5cqi">Ella ____ (comer) a las ocho.</div><div class="t-tiles"><span class="t-tile">C</span><span class="t-tile">O</span><span class="t-tile">M</span><span class="t-tile">I</span><span class="t-tile">Ó</span></div></div>`, cls:'warm' },
{ id:'hand', kind:'game', name:'Bones of the Hand', by:'aisha', course:'ANAT 2401', plays:2200, up:186, down:14, remixes:23, checked:1, desc:'Label all 27 bones of the hand against the clock.', docs:['ANAT 2401 Lab 3.pdf'], thumb:`<div class="t-in"><div class="t-eye">Label · 27 bones</div><div class="t-box"><div class="t-line"><span class="t-dot done"></span>Scaphoid</div><div class="t-line"><span class="t-dot done"></span>Lunate</div><div class="t-line"><span class="t-dot open"></span>Triquetrum</div></div></div>` },
{ id:'krebs', kind:'game', name:'Krebs Escape', by:'tessa', course:'BIL 150', plays:3100, up:282, down:15, remixes:18, checked:2, desc:'Each lock opens with a step of the Krebs cycle. Eight locks to freedom.', docs:['Cellular Respiration Notes.pdf','Lecture 9 Slides.pdf'], thumb:SR_T.q('Lock 5 of 8','Which step makes FADH₂?','Succinate → fumarate','Citrate → isocitrate'), cls:'violet' },
{ id:'sn2', kind:'game', name:'SN2 Speedway', by:'leo', course:'CHM 2210', plays:1100, up:92, down:8, remixes:6, from:'lyso', checked:1, desc:'A remix of Lysosome Defense for reaction mechanisms. Each lane is a substrate.', docs:['CHM 2210 Mechanisms.pdf'], thumb:SR_T.q('Lane 2 · Remix','Strong nucleophile, primary carbon, one step.','SN2','SN1'), fresh:true },
{ id:'plant', kind:'game', name:'Organelle Defense: Plant Edition', by:'hannah', course:'BSC 2010', plays:740, up:61, down:3, remixes:4, from:'lyso', checked:1, desc:'Lysosome Defense with chloroplasts, a cell wall and a central vacuole.', docs:['Plant Cells Lecture.pdf'], thumb:SR_T.q('Wave 2 · Remix','Which organelle runs photosynthesis?','Chloroplast','Vacuole'), fresh:true },
{ id:'pretimp', kind:'game', name:'Showdown: Imperfect Edition', by:'carlos', course:'SPN 2200', plays:420, up:38, down:2, remixes:1, from:'pret', checked:1, desc:'Preterite Showdown focused on imperfect triggers like siempre and mientras.', docs:['Imperfect Triggers.pdf'], thumb:`<div class="t-in" style="align-items:center;justify-content:center;inset:0;gap:3cqi"><div class="t-eye">Remix · Imperfect</div><div class="t-q" style="font-size:5cqi">Siempre ____ (leer) antes de dormir.</div><span class="t-pill">leía</span></div>`, cls:'warm', fresh:true },
{ id:'citation', kind:'tool', name:'Citation Fixer', by:'ana', course:'ENC 1102', plays:6700, up:802, down:16, remixes:58, checked:1, desc:'Paste a messy citation. It fixes it to APA 7 or MLA 9 and explains each change.', docs:['APA 7 Quick Guide.pdf'], thumb:`<div class="t-in"><div class="t-eye">APA 7 · 3 fixes</div><div class="t-box"><div class="t-mono">Rivera, M. (2023). <span style="color:#f2c35b">Sleep and memory.</span> Journal of Cognition, 6(2), 41–58.</div></div><span class="t-pill" style="align-self:flex-start">Fixed 3 issues</span></div>` },
{ id:'dosage', kind:'tool', name:'Dosage Calc Drill', by:'kayla', course:'NUR 3220', plays:4300, up:430, down:18, remixes:33, checked:2, desc:'Med math drills with the formula shown after every answer.', docs:['Pharm Math Notes.pdf','Dosage Practice Set.docx'], thumb:SR_T.q('Dosage · Card 3 / 12','Order: 500 mg. On hand: 250 mg per 5 mL. Give?','10 mL','2.5 mL'), cls:'violet' },
{ id:'labgrader', kind:'tool', name:'Lab Report Grader', by:'sam', course:'CHEM 1211K', plays:2900, up:240, down:24, remixes:19, checked:1, desc:'Checks your lab report against the rubric before you submit it.', docs:['CHEM 1211K Lab Rubric.pdf'], thumb:`<div class="t-in"><div class="t-eye">Rubric · 7 checks</div><div class="t-box"><div class="t-line"><span class="t-dot done"></span>Hypothesis is testable</div><div class="t-line"><span class="t-dot done"></span>Units on every value</div><div class="t-line"><span class="t-dot open"></span>Error analysis missing</div></div></div>` },
{ id:'formula', kind:'tool', name:'Flash Formula Sheet', by:'nina', course:'PHY 101', plays:1600, up:142, down:9, remixes:11, from:'citation', checked:1, desc:'Your formula sheet, with one part hidden at a time.', docs:['PHY 101 Formula Sheet.pdf'], thumb:`<div class="t-in" style="justify-content:center"><div class="t-box"><div class="t-mono" style="font-size:4.4cqi;color:#fff">v² = v₀² + <span style="color:var(--stage-accent)">?</span></div></div></div>`, fresh:true },
];
function lysoSVG(play) {
const pg = [[400,70],[452,150],[410,232],[470,96],[440,206]];
return `<svg viewBox="0 0 520 300" preserveAspectRatio="xMidYMid slice" aria-hidden="true">
<defs><radialGradient id="cyto" cx="45%" cy="45%" r="60%"><stop offset="0" stop-color="#1b5560"/><stop offset="1" stop-color="#0c2a31"/></radialGradient></defs>
<rect width="520" height="300" fill="#071219"/>
<g opacity=".18" stroke="#2fd0b8">${Array.from({ length:12 }, (_, i) => `<path d="M${i*46} 0v300"/>`).join('')}</g>
<circle cx="150" cy="150" r="128" fill="url(#cyto)" stroke="#2fd0b8" stroke-width="3"/><circle cx="150" cy="150" r="136" fill="none" stroke="#2fd0b8" stroke-width="1.5" opacity=".5"/>
<circle cx="130" cy="140" r="40" fill="#5a4bb0"/><circle cx="138" cy="132" r="12" fill="#8c7bf0"/>
<g fill="#e07b52"><ellipse cx="200" cy="90" rx="26" ry="12" transform="rotate(-20 200 90)"/><ellipse cx="96" cy="222" rx="24" ry="11" transform="rotate(30 96 222)"/></g>
<g fill="none" stroke="#f0c157" stroke-width="2"><path d="M188 92q12-8 24 0" transform="rotate(-20 200 90)"/></g>
<g stroke="#7fd6c8" stroke-width="5" fill="none" opacity=".7"><path d="M196 196q16-10 32 0q16 10 32 0"/><path d="M200 214q14-8 28 0q14 8 28 0"/></g>
${[[268,104],[276,150],[266,198]].map(([x,y],i) => `<g><circle cx="${x}" cy="${y}" r="15" fill="#f2c35b"/><circle cx="${x}" cy="${y}" r="8" fill="#fff1c4"/><circle cx="${x}" cy="${y}" r="21" fill="none" stroke="#f2c35b" stroke-width="2" opacity=".5"/></g>`).join('')}
${pg.map(([x,y],i) => `<g class="pg" data-pg="${i}" style="transform-origin:${x}px ${y}px"><circle cx="${x}" cy="${y}" r="14" fill="#d9475b"/>${[0,60,120,180,240,300].map(a => `<path d="M${(x+14*Math.cos(a*Math.PI/180)).toFixed(1)} ${(y+14*Math.sin(a*Math.PI/180)).toFixed(1)}L${(x+22*Math.cos(a*Math.PI/180)).toFixed(1)} ${(y+22*Math.sin(a*Math.PI/180)).toFixed(1)}" stroke="#d9475b" stroke-width="3" stroke-linecap="round"/>`).join('')}<circle cx="${x-4}" cy="${y-4}" r="3" fill="#ffb1bc"/></g>`).join('')}
${play ? pg.map(([x,y],i) => `<path class="zap" data-zap="${i}" d="M${[268,276,266][i%3]+12} ${[104,150,198][i%3]}L${x} ${y}"/>`).join('') : `<path d="M286 150L452 150" stroke="#f2c35b" stroke-width="3" stroke-dasharray="6 6"/>`}
</svg>`;
}
const golemSVG = () => `<svg class="golem" viewBox="0 0 220 210" aria-hidden="true">
<ellipse cx="110" cy="200" rx="72" ry="8" fill="#000" opacity=".35"/>
<path d="M72 148 62 194h34l4-42z" fill="#2b3a43"/><path d="M148 148l10 46h-34l-4-42z" fill="#2b3a43"/>
<path d="M50 76c-30 20-30 46-27 68 2 16 12 20 22 19 15-2 19-12 19-22-1-26 9-38 16-50z" fill="#35464f"/>
<path d="M170 76c30 20 30 46 27 68-2 16-12 20-22 19-15-2-19-12-19-22 1-26-9-38-16-50z" fill="#35464f"/>
<path d="M60 60c34-24 66-24 100 0l14 64c-2 32-30 40-64 42-34-2-62-10-64-42z" fill="#425661"/>
<path d="M84 20c22-14 30-14 52 0l8 32c-36 14-50 14-68 0z" fill="#4e6571"/>
<rect class="g-eye" x="91" y="33" width="13" height="6" rx="2" fill="#7ff0dc"/><rect class="g-eye" x="116" y="33" width="13" height="6" rx="2" fill="#7ff0dc"/>
<path d="M110 82l18 10v21l-18 10-18-10V92z" fill="#e8b23c"/><path d="M110 90l11 6v13l-11 6-11-6V96z" fill="#fff1c4"/>
<path class="g-crack" d="M70 72l14 12-6 14M150 70l-12 16 8 12M94 142l12-10 12 8" stroke="#f2c35b" stroke-width="2" fill="none" opacity=".45"/>
</svg>`;
/* ---------- the people behind the community games ---------- */
const SR_P = {
priya: { n: 'Priya Raman', s: 'Georgia Tech', av: 'PR', c: '#7a4fd0' }, marcus: { n: 'Marcus Lee', s: 'Miami', av: 'ML', c: '#2f7d6d' }, jordan: { n: 'Jordan Reyes', s: 'Miami', av: 'JR', c: '#a8842a' },
diego: { n: 'Diego Álvarez', s: 'Texas State', av: 'DÁ', c: '#c0612b' }, aisha: { n: 'Aisha Bello', s: 'Georgia State', av: 'AB', c: '#2a6fb0' }, leo: { n: 'Leo Martins', s: 'FIU', av: 'LM', c: '#3d7d3a' },
tessa: { n: 'Tessa Chen', s: 'Miami', av: 'TC', c: '#b0436a' }, ana: { n: 'Ana Lima', s: 'FIU', av: 'AL', c: '#2a6fb0' }, kayla: { n: 'Kayla Brooks', s: 'Miami', av: 'KB', c: '#8a4fd0' },
sam: { n: 'Sam Patel', s: 'Georgia Tech', av: 'SP', c: '#2f7d6d' }, hannah: { n: 'Hannah Kim', s: 'UF', av: 'HK', c: '#b0436a' }, carlos: { n: 'Carlos Ruiz', s: 'Texas State', av: 'CR', c: '#a8842a' }, nina: { n: 'Nina Okafor', s: 'Miami', av: 'NO', c: '#c0612b' },
sb: { n: 'StudyBoost team', s: 'StudyBoost Originals', av: 'SB', c: '#0f9483' }
};
const SR_LB = [['priya', 48200, 12, 12], ['marcus', 31500, 8, 8], ['ana', 22900, 6, 6], ['diego', 18400, 9, 8], ['kayla', 15100, 5, 5]];
/* Priya's remix of Maya's own Mitosis Sprint (the notification in the top bar points here) */
SR_ITEMS.splice(6, 0, { id: 'mitoboss', kind: 'game', name: 'Mitosis Boss Battle', by: 'priya', course: 'BIOL 1510', plays: 640, up: 71, down: 3, remixes: 2, from: 'm-mitosis', checked: 2, desc: 'A remix of Maya’s Mitosis Sequence Sprint: ten minutes, three rounds and a boss that gets angrier as you go.', docs: ['Cell Division Lecture 7.pdf', 'BIOL 1510 Lab 5.pdf'], thumb: SR_T.q('Round 3 · Boss', 'Sister chromatids pull apart. Which phase?', 'Anaphase', 'Metaphase'), cls: 'ember', fresh: true });
const SR_PLAYSET = { lyso: 'cell', cellwars: 'cell', plant: 'cell', sn2: 'chem', pret: 'span', pretimp: 'span', supply: 'econ', hand: 'anat', krebs: 'krebs', mitoboss: 'bio', citation: 'cite', dosage: 'dose', labgrader: 'lab', formula: 'formula',
trivia: 'cell', hangman: 'transport', tiles: 'bio', memory: 'psy', crossword: 'chem', wordsearch: 'anat', blank: 'transport', typing: 'krebs', siege: 'econ', courses: 'transport', summarize: 'stoich', guide: 'psy', ask: 'transport', flash: 'stoich', scenario: 'dose' };
const SR_FIELD = new Set(['lyso', 'cellwars', 'plant', 'sn2']);
SR_ITEMS.forEach(it => { it.set = SR_PLAYSET[it.id]; });
SR_ORIG.forEach(o => { o.set = SR_PLAYSET[o.id]; o.by = 'sb'; o.orig = true; });
const SR_ORIG_DESC = {
trivia: 'Four answers, one right. Quick rounds on any course, with the page each answer came from.', hangman: 'Guess the term from its definition before the drawing is done.', tiles: 'Unscramble the letters to spell the term.',
memory: 'Flip cards to match each term with its meaning.', crossword: 'A crossword built from the key terms in your course.', wordsearch: 'Find the course terms hidden in the grid.',
blank: 'Fill in the missing word in sentences from your notes.', typing: 'Type passages from your notes as fast and as accurately as you can.', siege: 'Defend the castle by knocking down the misconceptions.',
courses: 'Upload a syllabus or notes and get lessons, practice quizzes and a study plan.', summarize: 'Key points from any document, in seconds.', guide: 'Turns a document into a study guide with sections and checks.',
ask: 'Ask questions and get answers from your own documents, with sources.', flash: 'Flashcards from any document, with spaced review.', scenario: 'Role-play a real situation and get feedback on each choice.'
};
const SR_ORIG_TAGS = { trivia: ['quiz'], hangman: ['word', 'quiz'], tiles: ['word'], memory: ['relaxing'], crossword: ['word'], wordsearch: ['word', 'relaxing'], blank: ['word', 'quiz'], typing: ['relaxing'], siege: ['strategy'], courses: ['understand'], summarize: ['understand'], guide: ['understand'], ask: ['understand'], flash: ['practice'], scenario: ['practice'] };
/* ---------- question sets: every game and tool in the prototype plays from one of these ---------- */
const SR_SETS = {
bio: { T: 'Mitosis', topic: 'the phases of mitosis', seq: ['Prophase', 'Metaphase', 'Anaphase', 'Telophase', 'Cytokinesis'], facts: ['Prophase: chromosomes condense, spindle forms', 'Metaphase: chromosomes line up in the middle', 'Anaphase: sister chromatids pull apart'], qs: [
{ q: 'Sister chromatids are pulled to opposite poles.', o: ['Anaphase', 'Metaphase', 'Prophase', 'Telophase'], a: 0, p: 'p. 5', h: 'Think “A” for apart.', why: 'In anaphase the spindle fibers shorten and pull each pair of sister chromatids apart, toward opposite poles.' },
{ q: 'Chromosomes line up along the cell’s middle.', o: ['Telophase', 'Metaphase', 'Anaphase', 'Prophase'], a: 1, p: 'p. 4', h: '“Meta” sounds like middle.', why: 'At metaphase the chromosomes line up on the metaphase plate, so each sister chromatid can attach to a spindle fiber.' },
{ q: 'Nuclear envelopes re-form around each set of chromosomes.', o: ['Prophase', 'Anaphase', 'Telophase', 'Metaphase'], a: 2, p: 'p. 6', h: '“Telo” means end.', why: 'Telophase reverses prophase: the chromosomes relax and a nuclear envelope forms around each set.' },
{ q: 'Chromatin condenses and the spindle starts to form.', o: ['Metaphase', 'Prophase', 'Interphase', 'Anaphase'], a: 1, p: 'p. 3', h: '“Pro” means before.', why: 'Prophase comes first: chromatin coils into visible chromosomes and the spindle begins to form.' }] },
transport: { T: 'Cell Transport', topic: 'osmosis and diffusion', facts: ['Diffusion: particles move from high to low concentration', 'Osmosis: water diffuses across a membrane', 'Active transport uses ATP to move against the gradient'], qs: [
{ q: 'A cell sits in a hypertonic solution. What happens?', o: ['It shrinks as water leaves', 'It swells and may burst', 'Nothing changes', 'It takes in salt'], a: 0, p: 'p. 4', h: 'Water moves toward the higher solute concentration.', why: 'Hypertonic means more solute outside, so water leaves the cell by osmosis and the cell shrinks.' },
{ q: 'Which of these needs ATP?', o: ['Simple diffusion', 'Osmosis', 'The sodium-potassium pump', 'Facilitated diffusion'], a: 2, p: 'p. 7', h: 'It pumps against the gradient.', why: 'The Na⁺/K⁺ pump moves ions against their gradients, which takes energy from ATP. The other three are passive.' },
{ q: 'Glucose crosses through a carrier protein, down its gradient. This is:', o: ['Active transport', 'Facilitated diffusion', 'Endocytosis', 'Osmosis'], a: 1, p: 'p. 6', h: 'Down the gradient means no energy is needed.', why: 'A protein helps it across, but it still moves from high to low concentration, so it is facilitated diffusion.' },
{ q: 'Water moving across a membrane is called:', o: ['Exocytosis', 'Osmosis', 'Active transport', 'Phagocytosis'], a: 1, p: 'p. 3', h: 'A special name for water diffusion.', why: 'Osmosis is the diffusion of water through a selectively permeable membrane.' }] },
cell: { T: 'Organelles', topic: 'organelles and what they do', facts: ['Lysosomes digest worn-out parts and invaders', 'Ribosomes make proteins', 'Mitochondria make most of the cell’s ATP'], qs: [
{ q: 'Which organelle digests worn-out parts and invading bacteria?', o: ['Ribosome', 'Lysosome', 'Golgi apparatus', 'Nucleolus'], a: 1, p: 'Lecture 4, p. 6', h: 'It is full of digestive enzymes.', why: 'Lysosomes hold hydrolytic enzymes that break down old organelles and anything the cell takes in.' },
{ q: 'Where are most of the cell’s proteins made?', o: ['Ribosomes', 'Lysosomes', 'Central vacuole', 'Cell membrane'], a: 0, p: 'Lecture 4, p. 3', h: 'Small, and found on the rough ER.', why: 'Ribosomes translate mRNA into protein, free in the cytoplasm or bound to the rough ER.' },
{ q: 'Which organelle makes most of the cell’s ATP?', o: ['Golgi apparatus', 'Smooth ER', 'Mitochondria', 'Lysosome'], a: 2, p: 'Lab Manual, Unit 2', h: 'The powerhouse.', why: 'Mitochondria run cellular respiration, which makes most of the cell’s ATP.' }] },
krebs: { T: 'Krebs Cycle', topic: 'the Krebs cycle', facts: ['Acetyl-CoA joins oxaloacetate to make citrate', 'Runs in the mitochondrial matrix', 'Each turn: 3 NADH, 1 FADH₂, 1 ATP'], qs: [
{ q: 'Acetyl-CoA joins which molecule to start the cycle?', o: ['Oxaloacetate', 'Pyruvate', 'Citrate', 'Glucose'], a: 0, p: 'p. 6', h: 'It has 4 carbons and is rebuilt at the end of every turn.', why: 'Acetyl-CoA (2 carbons) joins oxaloacetate (4 carbons) to make citrate (6 carbons). Citrate is the product, so it can’t be the starting partner.' },
{ q: 'Where in the cell does the Krebs cycle run?', o: ['Cytoplasm', 'Mitochondrial matrix', 'Inner mitochondrial membrane', 'Nucleus'], a: 1, p: 'p. 4', h: 'Inside the mitochondrion, not on its membranes.', why: 'The cycle runs in the mitochondrial matrix. Glycolysis runs in the cytoplasm, and the electron transport chain sits on the inner membrane.' },
{ q: 'How many CO₂ molecules leave per turn of the cycle?', o: ['1', '3', '2', '6'], a: 2, p: 'p. 7', h: 'Two carbons come in with acetyl-CoA.', why: 'Two carbons enter as acetyl-CoA and two carbons leave as CO₂, so the cycle stays balanced each turn.' },
{ q: 'Which step makes FADH₂?', o: ['Citrate → isocitrate', 'Malate → oxaloacetate', 'α-ketoglutarate → succinyl-CoA', 'Succinate → fumarate'], a: 3, p: 'p. 9', h: 'The enzyme is also Complex II.', why: 'Succinate dehydrogenase passes electrons to FAD when succinate becomes fumarate.' },
{ q: 'Per turn, the Krebs cycle makes:', o: ['3 NADH, 1 FADH₂, 1 ATP', '2 NADH, 2 FADH₂, 2 ATP', '1 NADH, 1 FADH₂, 3 ATP', '4 NADH, 0 FADH₂, 1 ATP'], a: 0, p: 'p. 10', h: 'NADH is made three times.', why: 'Each turn makes 3 NADH, 1 FADH₂ and 1 ATP (or GTP). Double it for one glucose.' }] },
stoich: { T: 'Stoichiometry', topic: 'stoichiometry', facts: ['Balance the equation first', 'Grams to moles with the molar mass', 'Use the mole ratio, then convert back'], qs: [
{ q: 'How many moles of O₂ react with 4.0 mol of H₂ in 2H₂ + O₂ → 2H₂O?', o: ['1.0 mol', '2.0 mol', '4.0 mol', '8.0 mol'], a: 1, p: 'Practice Set, p. 1', h: 'The ratio of H₂ to O₂ is 2 to 1.', why: 'The balanced equation uses 2 H₂ for every O₂, so 4.0 mol of H₂ needs 2.0 mol of O₂.' },
{ q: 'What is the molar mass of CO₂?', o: ['28.0 g/mol', '32.0 g/mol', '44.0 g/mol', '12.0 g/mol'], a: 2, p: 'Practice Set, p. 2', h: 'C is 12.0 and each O is 16.0.', why: '12.0 + 2 × 16.0 = 44.0 g/mol.' },
{ q: '3.0 mol Al meets 4.0 mol Cl₂ in 2Al + 3Cl₂ → 2AlCl₃. Which runs out first?', o: ['Al', 'Cl₂', 'Neither', 'Both at once'], a: 1, p: 'Limiting Reagents, p. 1', h: 'Work out what each one would need.', why: '4.0 mol of Cl₂ only reacts with 2.67 mol of Al, so Cl₂ is the limiting reagent.' },
{ q: 'Actual yield 18.2 g, theoretical yield 20.0 g. What is the percent yield?', o: ['91%', '110%', '9%', '82%'], a: 0, p: 'Practice Set, p. 3', h: 'Actual ÷ theoretical × 100.', why: '18.2 ÷ 20.0 × 100 = 91%.' }] },
psy: { T: 'Classical Conditioning', topic: 'classical conditioning', facts: ['A neutral stimulus is paired with an unconditioned one', 'After pairing it becomes a conditioned stimulus', 'Extinction: the response fades when pairing stops'], qs: [
{ q: 'In Pavlov’s study, the food is the:', o: ['Conditioned stimulus', 'Unconditioned stimulus', 'Conditioned response', 'Neutral stimulus'], a: 1, p: 'Notes, p. 1', h: 'It works without any learning.', why: 'Food makes the dog salivate without training, so it is the unconditioned stimulus.' },
{ q: 'After training, salivating to the tone alone is the:', o: ['Unconditioned response', 'Conditioned response', 'Extinction', 'Generalization'], a: 1, p: 'Notes, p. 1', h: 'It had to be learned.', why: 'Salivating to the tone was learned through pairing, so it is a conditioned response.' },
{ q: 'The tone plays again and again with no food, and the response fades. This is:', o: ['Acquisition', 'Spontaneous recovery', 'Extinction', 'Discrimination'], a: 2, p: 'Notes, p. 2', h: 'The learning goes away.', why: 'When the conditioned stimulus keeps appearing without the unconditioned one, the response weakens. That is extinction.' },
{ q: 'Which study design can show cause and effect?', o: ['Case study', 'Survey', 'Experiment', 'Naturalistic observation'], a: 2, p: 'Research Methods Quiz', h: 'It needs random assignment.', why: 'Only an experiment manipulates one variable with random assignment, so only an experiment can show cause.' }] },
chem: { T: 'SN1 vs SN2', topic: 'SN1 vs SN2 reactions', facts: ['SN2: one step, backside attack, inversion', 'SN1: carbocation first, then the nucleophile', 'Tertiary carbons favor SN1'], qs: [
{ q: 'Which mechanism is one step with backside attack?', o: ['SN1', 'SN2', 'E1', 'Radical'], a: 1, p: 'p. 3', h: 'One step means bonds break and form together.', why: 'SN2 is concerted: the nucleophile attacks from the back as the leaving group leaves.' },
{ q: 'SN1 reactions go through which intermediate?', o: ['A carbocation', 'A carbanion', 'A radical', 'None'], a: 0, p: 'p. 6', h: 'The leaving group leaves first.', why: 'In SN1 the leaving group leaves first, which leaves a carbocation for the nucleophile to attack.' },
{ q: 'What happens to stereochemistry in SN2?', o: ['It is kept', 'It becomes racemic', 'It inverts', 'It disappears'], a: 2, p: 'p. 4', h: 'Like an umbrella flipping in the wind.', why: 'Backside attack flips the carbon’s configuration, so SN2 gives inversion.' }] },
econ: { T: 'Supply and Demand', topic: 'supply and demand shifts', facts: ['A change in price moves along a curve', 'Anything else shifts the whole curve', 'A price held below equilibrium causes a shortage'], qs: [
{ q: 'A frost destroys half the orange crop. What shifts?', o: ['Demand shifts right', 'Supply shifts left', 'Supply shifts right', 'Nothing shifts'], a: 1, p: 'p. 2', h: 'Sellers can offer less at every price.', why: 'Fewer oranges can be sold at every price, so the whole supply curve shifts left.' },
{ q: 'Incomes rise and coffee is a normal good. Coffee demand…', o: ['shifts right', 'shifts left', 'stays put', 'becomes supply'], a: 0, p: 'p. 2', h: 'People buy more normal goods when they earn more.', why: 'Higher income raises demand for normal goods at every price, so demand shifts right.' },
{ q: 'A price ceiling below equilibrium causes…', o: ['a surplus', 'no change', 'a shortage', 'lower demand'], a: 2, p: 'p. 4', h: 'More buyers want it than sellers offer.', why: 'At the capped price, quantity demanded is higher than quantity supplied, which is a shortage.' }] },
anat: { T: 'Bones of the Hand', topic: 'the bones of the hand', facts: ['8 carpals in the wrist', '5 metacarpals in the palm', '14 phalanges in the fingers'], qs: [
{ q: 'How many carpal bones are in each wrist?', o: ['5', '14', '8', '27'], a: 2, p: 'Lab 3', h: 'Two rows of four.', why: 'The wrist has eight carpals, in a proximal and a distal row of four.' },
{ q: 'Which bones form the palm?', o: ['Metacarpals', 'Carpals', 'Phalanges', 'Metatarsals'], a: 0, p: 'Lab 3', h: '“Meta” means beyond the carpals.', why: 'The five metacarpals form the palm, between the carpals and the phalanges.' },
{ q: 'How many phalanges does the thumb have?', o: ['3', '2', '1', '4'], a: 1, p: 'Lab 3', h: 'It has no middle phalanx.', why: 'The thumb has a proximal and a distal phalanx only, so two.' }] },
span: { T: 'Preterite vs Imperfect', topic: 'the preterite and the imperfect', facts: ['Preterite: finished actions at a set time', 'Imperfect: habits and background', 'Triggers: ayer for preterite, siempre for imperfect'], qs: [
{ q: 'Ayer yo ____ (comer) tacos.', o: ['comía', 'comí', 'como', 'comeré'], a: 1, p: 'p. 1', h: 'Ayer points to a finished action.', why: '“Ayer” marks one finished action in the past, so the preterite comí fits.' },
{ q: 'Cuando era niño, ____ (jugar) todos los días.', o: ['jugué', 'jugaba', 'juego', 'jugó'], a: 1, p: 'p. 2', h: 'Every day is a habit.', why: 'A repeated habit in the past takes the imperfect, jugaba.' }] },
cite: { T: 'APA Citations', topic: 'APA 7 citations', facts: ['Author, year, title, source', 'Sentence case for article titles', 'Three or more authors: first author et al.'], qs: [
{ q: 'Which APA 7 reference is formatted correctly?', o: ['Rivera, M. (2023). Sleep and memory. Journal of Cognition, 6(2), 41–58.', 'Rivera, Maria. 2023. “Sleep and Memory.” Journal of Cognition.', 'M. Rivera (2023) Sleep and memory, Journal of Cognition 6.2', 'Rivera M 2023 Sleep and memory'], a: 0, p: 'APA Guide, p. 2', h: 'Initials, year in parentheses.', why: 'APA 7 uses the surname and initials, the year in parentheses, a sentence-case title and the italic source with volume and pages.' },
{ q: 'A source has three authors. How does an APA 7 in-text citation start?', o: ['All three last names', 'First author, then “et al.”', '“Anonymous”', 'The title'], a: 1, p: 'APA Guide, p. 4', h: 'APA 7 shortened this.', why: 'For three or more authors, APA 7 cites the first author followed by “et al.” every time.' }] },
dose: { T: 'Dosage Calculation', topic: 'dosage calculations', facts: ['Desired ÷ have × quantity', 'Check the units match first', 'Round the way your program asks'], qs: [
{ q: 'Order: 500 mg. On hand: 250 mg per 5 mL. How many mL do you give?', o: ['2.5 mL', '5 mL', '10 mL', '20 mL'], a: 2, p: 'Pharm Math, p. 3', h: 'Desired ÷ have × quantity.', why: '500 ÷ 250 × 5 mL = 10 mL.' },
{ q: 'Order: 0.25 mg. On hand: 0.5 mg tablets. How many tablets?', o: ['2', '½', '1', '4'], a: 1, p: 'Pharm Math, p. 4', h: 'The order is smaller than one tablet.', why: '0.25 ÷ 0.5 = ½ tablet.' }] },
lab: { T: 'Lab Reports', topic: 'lab report writing', facts: ['A testable hypothesis', 'Units on every value', 'An error analysis section'], qs: [
{ q: 'Which hypothesis is testable?', o: ['Chemistry is interesting', 'Raising the temperature increases the reaction rate', 'Reactions happen', 'Catalysts are good'], a: 1, p: 'Rubric, item 1', h: 'You could measure it.', why: 'Only this one names something you can change and something you can measure.' },
{ q: 'Your data table says “mass: 2.31”. What is missing?', o: ['A title', 'Units', 'Color', 'Nothing'], a: 1, p: 'Rubric, item 4', h: 'Grams? Kilograms?', why: 'Every measured value needs its unit, here grams.' }] },
formula: { T: 'Kinematics', topic: 'the kinematics formulas', facts: ['v = v₀ + at', 'x = x₀ + v₀t + ½at²', 'v² = v₀² + 2aΔx'], qs: [
{ q: 'v = v₀ + ?', o: ['at', '½at²', '2aΔx', 'a/t'], a: 0, p: 'Sheet p. 1', h: 'Velocity changes by a every second.', why: 'After t seconds of constant acceleration, velocity has changed by a·t.' },
{ q: 'x = x₀ + v₀t + ?', o: ['at', '½vt', '½at²', '2at²'], a: 2, p: 'Sheet p. 1', h: 'The area of a triangle.', why: 'The extra distance from speeding up is ½at², the triangle under a velocity-time graph.' },
{ q: 'v² = v₀² + ?', o: ['at²', '2aΔx', 'aΔx', '½aΔx'], a: 1, p: 'Sheet p. 2', h: 'No time in this one.', why: 'Eliminating t from the first two equations gives v² = v₀² + 2aΔx.' }] }
};
/* topic words → question set, so a typed topic still finds questions */
const SR_TOPIC_KEYS = [[/mitosis|phase|chromosom|cell div/i, 'bio'], [/osmo|diffus|transport|membrane|tonic/i, 'transport'], [/krebs|citric|respiration/i, 'krebs'], [/stoich|mole|limiting|yield|reagent|titrat/i, 'stoich'],
[/condition|pavlov|memory|learning|psych|research method/i, 'psy'], [/organelle|lysosome|ribosome|mitochondri/i, 'cell'], [/sn1|sn2|mechanism|substitution|organic/i, 'chem'], [/supply|demand|market|price|econ/i, 'econ'],
[/bone|hand|carpal|anatom/i, 'anat'], [/preterit|imperfect|spanish|conjug/i, 'span'], [/citation|apa|mla/i, 'cite'], [/dosage|dose|pharm|med math/i, 'dose'], [/lab report|rubric|hypothes/i, 'lab'], [/kinemat|formula|velocity|physics/i, 'formula']];
const srTopicSet = t => { const hit = SR_TOPIC_KEYS.find(([re]) => re.test(t || '')); return hit ? hit[1] : null; };
/* ---------- the sentence: what it is, what it helps with, and the mechanic each pair makes ---------- */
const SR_GOALS = [
{ id: 'memorize', w: 'memorize', d: 'Lock facts and terms into memory' },
{ id: 'understand', w: 'understand', d: 'Get why it works, not just what' },
{ id: 'practice', w: 'practice problems on', d: 'Work through problems step by step' },
{ id: 'exam', w: 'get ready for an exam on', d: 'Exam-style questions and a plan' },
{ id: 'review', w: 'quickly review', d: 'A quick pass before class' }
];
const SR_LENS = [2, 5, 10, 20];
const SR_MECH = {
game: { memorize: ['Sequence Sprint', 'Put steps in order, then name each one fast', 'sprout'], understand: ['Misconception Duel', 'Pick the claim that holds up and see why the other fails', 'shield'], practice: ['Boss Battle', 'Every right answer lands a hit', 'flame'], exam: ['Mock Exam Run', 'Timed, exam-style questions with a score at the end', 'clock'], review: ['Lightning Round', 'Rapid-fire questions in a couple of minutes', 'bolt'] },
tool: { memorize: ['Self-quiz Cheat Sheet', 'Key facts on one side, quick checks on the other', 'file'], understand: ['Explain-it-back Tutor', 'You explain it, Hoot checks it against your notes', 'message'], practice: ['Step-by-step Problem Coach', 'One step at a time, with a check at each step', 'wrench'], exam: ['Exam Countdown Planner', 'Spreads review across your days, then checks you', 'calendar'], review: ['One-page Summary', 'The whole topic on one page, with a quick check', 'list'] }
};
const SR_REFINES = {
game: [
{ id: 'timer', label: 'Add a 20-second timer', reply: 'Done. Version {v} gives every question a 20-second timer. Running out of time counts as a miss.', tag: 'Timer 20s' },
{ id: 'easy', label: 'Make round 1 easier', reply: 'Done. Round 1 now shows a hint under each question. The later rounds stay the same.', tag: 'Round 1 hints' },
{ id: 'explain', label: 'Explain answers I get wrong', reply: 'Done. When you miss one, I’ll show why the right answer is right, with the page it comes from.', tag: 'Explanations' },
{ id: 'notes', label: 'Only use terms from my notes', reply: 'Done. I swapped the terms that only appear in the textbook for the ones used in your notes.', tag: 'Your terms' }],
tool: [
{ id: 'timer', label: 'Add a 20-second timer', reply: 'Done. Version {v} gives each check 20 seconds before it shows the answer.', tag: 'Timer 20s' },
{ id: 'explain', label: 'Explain answers I get wrong', reply: 'Done. Each miss now shows where the answer comes from, linked to your notes.', tag: 'Explanations' },
{ id: 'easy', label: 'Show a hint on every check', reply: 'Done. A hint now shows under every check so you can test yourself first.', tag: 'Hints' }]
};
/* ---------- what Maya has made (a brand-new student starts with none) ---------- */
const MADE_DEFAULT = [
{ id: 'm-mitosis', kind: 'game', name: 'Mitosis Sequence Sprint', mech: 'Sequence Sprint', set: 'bio', course: 'BIL 150', st: 'pub', plays: 312, up: 58, down: 4, remixes: 1, versions: 3,
sentence: 'Make me a game that helps me memorize the phases of mitosis using my BIL 150 · Midterm shelf in about 5 minutes.', idea: { kind: 'game', goal: 'memorize', topic: 'the phases of mitosis', src: 'shelf-bil', len: 5 },
desc: 'Put the phases of mitosis in order, then name each one fast.', docs: ['Worksheet on Mitotic Index and Phase Indices', 'Cell Cycle and Mitosis Lecture Notes'] },
{ id: 'm-stoich', kind: 'tool', name: 'Stoichiometry Problem Coach', mech: 'Step-by-step Problem Coach', set: 'stoich', course: 'CHM 113', st: 'draft', plays: 0, up: 0, down: 0, remixes: 0, versions: 2,
sentence: 'Make me a tool that helps me practice problems on stoichiometry using my CHM 113 · Unit 3 shelf in about 10 minutes.', idea: { kind: 'tool', goal: 'practice', topic: 'stoichiometry', src: 'shelf-chm', len: 10 },
desc: 'One step at a time, with a check at each step.', docs: ['Stoichiometry Practice Set', 'Limiting Reagents Worksheet'] },
{ id: 'm-cond', kind: 'game', name: 'Conditioning Misconception Duel', mech: 'Misconception Duel', set: 'psy', course: 'PSY 110', st: 'priv', plays: 4, up: 1, down: 0, remixes: 0, versions: 1,
sentence: 'Make me a game that helps me understand classical conditioning using my Finals Week shelf in about 5 minutes.', idea: { kind: 'game', goal: 'understand', topic: 'classical conditioning', src: 'shelf-fin', len: 5 },
desc: 'Pick the claim that holds up and see why the other fails.', docs: ['Classical Conditioning Notes', 'Research Methods Quiz'] }
];
/* ===== 78b-study-ui.js ===== */
/* ================= Study Room: the library (Community), the sentence (Idea Builder), Made by you ================= */
/* Idea Builder's sentence starts a build, Studio's chat refines it (78c), and it publishes into the Community library,
where anyone can remix it. Routes: #study (games), #study-tools, #study-mine, #study-create, #game-<id>. */
const SR = { proj: null, src: 'all', q: '' };
const hootProf = (w, cls) => `<svg class="hoot ${cls || ''}" viewBox="0 0 120 130" style="width:${w}px;height:${Math.round(w * 130 / 120)}px" aria-hidden="true"><use href="#hoot-prof"/></svg>`;
const hootSym = (w, cls) => `<svg class="hoot ${cls || ''}" viewBox="0 0 120 130" style="width:${w}px;height:${Math.round(w * 130 / 120)}px" aria-hidden="true"><use href="#hoot"/></svg>`;
const srPct = it => it.up + it.down ? Math.round(it.up / (it.up + it.down) * 100) : null;
function srPerson(by) {
if (by === 'you') return { n: S.persona === 'new' ? 'You' : PERSONA.full, s: S.mine && BY_ID[S.mine] ? BY_ID[S.mine].short : 'StudyBoost', av: S.persona === 'new' ? 'YOU' : PERSONA.initials, c: '#0f9483' };
return SR_P[by] || SR_P.sb;
}
/* ---------- one list of everything playable ---------- */
function srThumbInner(setKey, kind) {
const s = SR_SETS[setKey] || SR_SETS.bio, q = s.qs[0];
return kind === 'tool'
? `<div class="t-in"><div class="t-eye">${esc(s.T)} · Key points</div><div class="t-box">${s.facts.slice(0, 2).map(f => `<div class="t-line"><span class="t-dot done"></span>${esc(f)}</div>`).join('')}</div></div>`
: `<div class="t-in"><div class="t-eye">${esc(s.T)}</div><div class="t-q">${esc(q.q)}</div><div class="t-opt good"><span>A</span>${esc(q.o[q.a])}<i>✓</i></div><div class="t-opt"><span>B</span>${esc(q.o[(q.a + 1) % q.o.length])}</div></div>`;
}
function madeItem(m) {
return Object.assign({ by: 'you', mine: true, checked: 1, docs: [], desc: '', from: null, plays: 0, up: 0, down: 0, remixes: 0 }, m, { thumb: srThumbInner(m.set, m.kind) });
}
const srMade = () => (S.made || []).map(madeItem);
function studyItem(id) {
return SR_ITEMS.find(i => i.id === id) || SR_ORIG.find(i => i.id === id) || srMade().find(m => m.id === id) || null;
}
const srAll = () => srMade().filter(m => m.st === 'pub').concat(SR_ITEMS);
function studyCatalog() {
return srAll().concat(SR_ORIG).map(it => Object.assign({}, it, { by: it.orig ? null : it.by, byName: it.orig ? null : srPerson(it.by).n, course: it.course || '', cat: it.cat || (it.kind === 'tool' ? 'Student tool' : 'Student game'), wish: it.desc || SR_ORIG_DESC[it.id] || '' }));
}
/* votes on games live beside document votes, under a prefix */
const srVote = id => S.votes['g:' + id] || null;
function srCounts(it) { const v = srVote(it.id); return { up: it.up + (v === 'up' ? 1 : 0), down: it.down + (v === 'down' ? 1 : 0) }; }
/* ---------- tiles ---------- */
const srBadge = orig => orig ? `<span class="badge orig">${icon('i-check')}Original</span>` : `<span class="badge stud">${icon('i-users')}Student-made</span>`;
function srThumbHTML(it) {
if (it.thumb === 'lyso') return `<div class="thumb" style="background:#071219">${lysoSVG().replace('<svg', '<svg style="position:absolute;inset:0;width:100%;height:100%"')}</div>`;
return `<div class="thumb ${it.thumbClass || it.cls || ''}">${it.thumb}</div>`;
}
const srWithBadge = (html, orig) => html.replace(/(<div class="thumb[^>]*>)/, `$1${srBadge(orig)}`);
function srCtile(it) {
const p = srPerson(it.by), c = srCounts(it), pct = c.up + c.down ? Math.round(c.up / (c.up + c.down) * 100) + '%' : 'New';
return `<div class="tile ctile${it.mine ? ' fresh' : ''}"><a href="#game-${it.id}" class="tile" style="gap:14px"><div style="position:relative">${srWithBadge(srThumbHTML(it), false)}</div><div class="cmeta"><span class="av sm" style="--av:${p.c}">${esc(p.av)}</span><span class="tile-text"><span class="tile-name">${esc(it.name)}${it.mine ? '<span class="yours">Yours</span>' : ''}</span><span class="tile-sub">${esc(p.n)} · ${esc(p.s)} · ${esc(it.course)}</span><span class="cline"><span>${icon('i-play')}${compact(it.plays)}</span><span>${icon('i-thumb')}${pct}</span><span>${icon('i-shield')}Hoot-checked</span></span></span></div></a><button type="button" class="remix" data-act="sr-remix" data-id="${it.id}" aria-label="Remix ${esc(it.name)}">${icon('i-remix')}Remix</button></div>`;
}
function srOtile(c) {
return srWithBadge(`<a class="tile ctile" href="#game-${c.id}"><div class="thumb ${c.thumbClass || ''}">${c.thumb}</div><div class="tile-meta"><span class="appicon" style="--ai:${c.ai}">${icon('i-' + c.glyph)}</span><span class="tile-text"><span class="tile-name">${esc(c.name)}</span><span class="tile-sub">${esc(c.cat)}</span></span></div></a>`, true);
}
const srRow = (id, ic, title, sub, inner, extra) => `<section class="row" data-row="${id}"><div class="row-head"><div><h2>${icon('i-' + ic)}${title}</h2><p>${sub}</p></div>${extra || ''}<div class="row-arrows"><button type="button" data-act="sr-rail" data-dir="-1" aria-label="Scroll left">${icon('i-left', 'sm')}</button><button type="button" data-act="sr-rail" data-dir="1" aria-label="Scroll right">${icon('i-right', 'sm')}</button></div></div><div class="rail">${inner}</div></section>`;
/* ================= Idea Builder: the sentence and its blueprint ================= */
const SR_SHELF_SET = { 'BIL 150': 'transport', 'CHM 113': 'stoich', 'PSY 110': 'psy' };
function srSrcs() {
const list = (S.shelves || []).filter(s => !s.example && (s.docs || []).length).map(s => {
const set = SR_SHELF_SET[s.course] || srTopicSet(s.name + ' ' + (s.course || '')) || 'bio';
return { id: 'shelf-' + s.id, label: `${s.name} shelf`, short: `my ${s.name} shelf`, pages: s.docs.reduce((n, id) => n + ((DOCS[id] || {}).pages || 0), 0), course: s.course || '', set };
});
list.push({ id: 'lib', label: 'StudyBoost’s library', short: 'StudyBoost’s library', pages: 0, set: null, course: '' });
return list;
}
function srIdeas() {
const has = id => srSrcs().some(s => s.id === id), pick = id => has(id) ? id : 'lib';
return [
{ kind: 'game', goal: 'memorize', topic: 'the phases of mitosis', src: pick('shelf-bil'), len: 5 },
{ kind: 'tool', goal: 'practice', topic: 'stoichiometry', src: pick('shelf-chm'), len: 10 },
{ kind: 'game', goal: 'understand', topic: 'classical conditioning', src: pick('shelf-fin'), len: 5 },
{ kind: 'tool', goal: 'exam', topic: 'osmosis and diffusion', src: pick('shelf-bil'), len: 20 },
{ kind: 'game', goal: 'review', topic: 'limiting reagents', src: pick('shelf-chm'), len: 2 },
{ kind: 'game', goal: 'practice', topic: 'the Krebs cycle', src: 'lib', len: 10 }
];
}
const IB = {};
function ibReset(kind) {
const ideas = srIdeas(), i = kind === 'tool' ? 1 : 0;
Object.keys(IB).forEach(k => delete IB[k]);
Object.assign(IB, ideas[i], { alt: null, own: false, ideaN: i, say: 'Tap any colored word to change it.', changed: null, free: '' });
}
const ibGoal = id => SR_GOALS.find(g => g.id === id) || SR_GOALS[0];
const ibSrc = id => { const l = srSrcs(); return l.find(s => s.id === id) || l[l.length - 1]; };
const ibMech = () => IB.alt || SR_MECH[IB.kind][IB.goal];
const ibSet = () => srTopicSet(IB.topic) || ibSrc(IB.src).set || 'bio';
const ibSentence = () => `Make me a ${IB.kind} that helps me ${ibGoal(IB.goal).w} ${IB.topic} using ${ibSrc(IB.src).short} in about ${IB.len} minutes.`;
function ibSentenceHTML() {
const sw = id => IB.changed === id || IB.changed === 'all' ? ' swap' : '';
const tk = (id, val, cls) => `<button type="button" class="tok ${cls || ''}${sw(id)}" data-act="ib-tok" data-tok="${id}" aria-haspopup="dialog" aria-expanded="false"><span class="tv">${esc(val)}</span></button>`;
return `Make me a ${tk('kind', IB.kind)} that helps me ${tk('goal', ibGoal(IB.goal).w)} ${tk('topic', IB.topic, 'topic')} using ${tk('src', ibSrc(IB.src).short)} in about ${tk('len', IB.len + ' minutes')}.`;
}
function ibBlueprint() {
const [name, desc, ic] = ibMech(), src = ibSrc(IB.src), set = SR_SETS[ibSet()];
const rounds = IB.len <= 2 ? 1 : IB.len <= 5 ? 2 : IB.len <= 10 ? 3 : 4;
const diff = IB.goal === 'exam' ? 'Exam level' : IB.goal === 'review' ? 'Quick' : 'Builds up';
const alts = Object.entries(SR_MECH[IB.kind]).filter(([g]) => g !== IB.goal).slice(0, 2).map(([, m]) => m);
return `<div class="bp-top"><span class="lbl">Blueprint</span><span class="kind">${IB.kind}</span></div>
<div class="bp-mech"><span class="appicon" style="--ai:${IB.kind === 'game' ? SR_G : 'linear-gradient(145deg,#25b39b,#0f8070)'}">${icon('i-' + ic)}</span><div><h3>${esc(name)}</h3><p>${esc(desc)}</p></div></div>
<dl class="bp-specs"><div><dt>${IB.kind === 'game' ? 'Rounds' : 'Sections'}</dt><dd>${rounds}</dd></div><div><dt>Length</dt><dd>${IB.len} min</dd></div><div><dt>Level</dt><dd>${diff}</dd></div></dl>
<div class="bp-src">${icon(src.id === 'lib' ? 'i-book' : 'i-file', 'sm')}<span>Built from: <b>${esc(src.id === 'lib' ? 'StudyBoost library · ' + set.T : src.label)}</b>${src.pages ? ' · ' + src.pages + ' pages' : ''}</span></div>
<div class="bp-alt"><span class="lbl">Or ${IB.kind === 'game' ? 'play' : 'use'} it as</span><div class="alts">${alts.map(a => `<button type="button" class="alt" data-act="ib-alt" data-alt="${esc(a[0])}" aria-pressed="${!!(IB.alt && IB.alt[0] === a[0])}">${esc(a[0])}<small>${esc(a[1])}</small></button>`).join('')}</div></div>
<button type="button" class="btn btn-primary btn-lg" data-act="ib-build">${icon('i-sparkle', 'sm')}Build it</button>`;
}
function ibBand() {
if (!IB.kind) ibReset();
return `<section class="ib-band" aria-labelledby="ib-title">
<div class="ib-card ib-sent">
<span class="peek" aria-hidden="true">${hootProf(84)}</span>
<p class="say" id="ib-say">${esc(IB.say)}</p>
<span class="eyebrow" id="ib-title">${icon('i-sparkle', 'xs')}Make your own <span class="new">New</span></span>
<p class="sentence" id="ib-sentence" aria-live="polite"${IB.own ? ' hidden' : ''}>${ibSentenceHTML()}</p>
<div class="ib-free"${IB.own ? '' : ' hidden'}><label for="ib-free" class="sr">Describe what to make</label><textarea id="ib-free">${esc(IB.free || ibSentence())}</textarea><span class="help">Say what it is, what it should help with, which notes to use and how long. Hoot fills in the rest.</span></div>
<div class="sent-foot"><button type="button" class="btn btn-outline btn-sm" data-act="ib-shuffle">${icon('i-dice', 'sm')}Shuffle idea</button><button type="button" class="linkish" data-act="ib-own">${IB.own ? 'Use the sentence builder' : 'Write it my own way'}</button><span class="help">${icon('i-lock', 'xs')}Private until you publish</span></div>
</div>
<aside class="ib-card bp" id="ib-bp" aria-live="polite">${ibBlueprint()}</aside>
</section>`;
}
function ibRepaint(changed, focusTok) {
IB.changed = changed || null;
const s = $('#ib-sentence'); if (s) s.innerHTML = ibSentenceHTML();
const bp = $('#ib-bp'); if (bp) bp.innerHTML = ibBlueprint();
IB.changed = null;
if (focusTok) { const b = $(`.tok[data-tok="${focusTok}"]`); if (b) b.focus({ preventScroll: true }); }
}
function ibSay(t) { IB.say = t; const s = $('#ib-say'); if (!s) return; s.classList.add('dim'); setTimeout(() => { s.textContent = t; s.classList.remove('dim'); }, reduceMotion() ? 0 : 150); }
function ibSetTok(tok, v) {
if (tok === 'kind') { IB.kind = v; IB.alt = null; }
if (tok === 'goal') { IB.goal = v; IB.alt = null; }
if (tok === 'topic') IB.topic = v;
if (tok === 'src') {
if (v === 'upload') { closePop(); openUpload(); return; }
IB.src = v; const s = ibSrc(v); if (s.set && !srTopicSet(IB.topic)) IB.topic = SR_SETS[s.set].topic; else if (s.set && srTopicSet(IB.topic) !== s.set && s.id !== 'lib') IB.topic = SR_SETS[s.set].topic;
}
if (tok === 'len') IB.len = +v;
closePop(); ibRepaint(tok, tok);
ibSay(tok === 'src' && v === 'lib' ? 'I’ll use trusted guides from the StudyBoost library and cite each one.' : `That makes it a ${ibMech()[0]}. Change anything else, or press Build it.`);
}
const SR_TOPICS = () => ['the phases of mitosis', 'osmosis and diffusion', 'organelles and what they do', 'the Krebs cycle', 'stoichiometry', 'limiting reagents', 'classical conditioning'];
Object.assign(ACT, {
'ib-tok': el => {
const t = el.dataset.tok;
const opt = (v, label, sub, on, ic) => `<button type="button" class="pop-opt" role="menuitemradio" data-act="ib-set" data-tok="${t}" data-v="${esc(v)}" aria-checked="${on}">${ic ? icon('i-' + ic, 'sm') : ''}<span>${esc(label)}${sub ? `<small>${esc(sub)}</small>` : ''}</span></button>`;
let html = '';
if (t === 'kind') html = `<span class="lbl">What to make</span>${opt('game', 'game', 'Something to play', IB.kind === 'game', 'gamepad')}${opt('tool', 'tool', 'Something to use while you study', IB.kind === 'tool', 'wrench')}`;
if (t === 'goal') html = `<span class="lbl">It should help you</span>${SR_GOALS.map(g => opt(g.id, g.w.replace(' on', ''), g.d, IB.goal === g.id)).join('')}`;
if (t === 'src') html = `<span class="lbl">Use these notes</span>${srSrcs().map(s => opt(s.id, s.id === 'lib' ? 'No notes, use StudyBoost’s library' : s.label, s.pages ? `${s.pages} pages · ${s.course}` : 'Hoot picks trusted guides on the topic', IB.src === s.id, s.id === 'lib' ? 'book' : 'file')).join('')}${opt('upload', 'Upload new notes', 'PDF, Word or a photo of your notes', false, 'upload')}`;
if (t === 'len') html = `<span class="lbl">About how long</span>${SR_LENS.map(l => opt(l, l + ' minutes', l <= 2 ? 'Between classes' : l <= 5 ? 'A quick session' : l <= 10 ? 'A focused session' : 'A real study block', IB.len === l)).join('')}`;
if (t === 'topic') html = `<span class="lbl">Topic</span><form class="pop-in" data-form="ib-topic"><input id="ib-topic-in" value="${esc(IB.topic)}" aria-label="Topic" maxlength="60" autocomplete="off"><button type="submit" class="btn btn-primary btn-sm">Set</button></form><span class="lbl">From your notes</span>${SR_TOPICS().map(x => opt(x, x, '', IB.topic === x)).join('')}`;
openPop(el, `<div class="sr-ib"><div class="tokpop" role="menu">${html}</div></div>`, { cls: 'pg-study sr-pop', label: 'Change this word', align: 'start', focus: t === 'topic' ? '#ib-topic-in' : '.pop-opt[aria-checked="true"]' });
},
'ib-set': el => ibSetTok(el.dataset.tok, el.dataset.v),
'ib-alt': el => { const m = Object.values(SR_MECH[IB.kind]).find(x => x[0] === el.dataset.alt); IB.alt = IB.alt && IB.alt[0] === m[0] ? null : m; ibRepaint(); ibSay(`${ibMech()[0]} it is.`); const b = $(`[data-act="ib-alt"][data-alt="${CSS.escape(m[0])}"]`); if (b) b.focus({ preventScroll: true }); },
'ib-shuffle': () => {
const ideas = srIdeas(), pool = ideas.map((x, i) => i).filter(i => i !== IB.ideaN);
IB.ideaN = pool[Math.floor(Math.random() * pool.length)];
Object.assign(IB, ideas[IB.ideaN], { alt: null });
if (IB.own) { IB.own = false; const f = $('.ib-free'), s = $('#ib-sentence'); if (f) f.hidden = true; if (s) s.hidden = false; const b = $('[data-act="ib-own"]'); if (b) b.textContent = 'Write it my own way'; }
ibRepaint('all'); ibSay(`Here’s one: a ${ibMech()[0]} on ${IB.topic}.`);
},
'ib-own': el => {
IB.own = !IB.own; const f = $('.ib-free'), s = $('#ib-sentence'), ta = $('#ib-free');
if (f) f.hidden = !IB.own; if (s) s.hidden = IB.own; el.textContent = IB.own ? 'Use the sentence builder' : 'Write it my own way';
if (IB.own && ta) { ta.value = ibSentence(); ta.focus(); ibSay('Write it however you like. I’ll read it the same way.'); } else ibSay('Tap any colored word to change it.');
},
'ib-build': () => {
let sentence = ibSentence();
if (IB.own) {
const t = ($('#ib-free') || {}).value || ''; IB.free = t.trim();
if (IB.free.length < 12) { ibSay('Tell me a little more: what it is and what it should help with.'); const ta = $('#ib-free'); if (ta) ta.focus(); return; }
if (/\btool\b/i.test(IB.free)) IB.kind = 'tool'; if (/\bgame\b/i.test(IB.free)) IB.kind = 'game';
const ts = srTopicSet(IB.free); if (ts) IB.topic = SR_SETS[ts].topic;
sentence = IB.free;
}
const [name] = ibMech(), setKey = ibSet(), set = SR_SETS[setKey], src = ibSrc(IB.src);
SR.proj = srNewProj({ kind: IB.kind, mech: name, setKey, title: `${set.T} ${name.replace(/^Self-quiz |^Step-by-step |^Explain-it-back |^Exam Countdown |^One-page /, '')}`, sentence, src, idea: { kind: IB.kind, goal: IB.goal, topic: IB.topic, src: IB.src, len: IB.len }, editing: IB.editing || null });
IB.editing = null;
go('study-create');
}
});
FORMS['ib-topic'] = f => { const v = $('#ib-topic-in', f).value.trim(); if (v) ibSetTok('topic', v); };
/* ---------- other pages start a build from what the student is looking at ---------- */
window.STUDY = {
fromShelf(s) {
ibReset('game'); SR.proj = null;
const src = srSrcs().find(x => x.id === 'shelf-' + s.id);
Object.assign(IB, { kind: 'game', goal: s.exam ? 'exam' : 'practice', src: src ? src.id : 'lib', len: 10, topic: src && src.set ? SR_SETS[src.set].topic : (s.name || 'my notes') });
IB.say = `I filled this in from your ${s.name} shelf. Change any word, then press Build it.`;
},
fromCourse(sid, code) {
ibReset('game'); SR.proj = null;
const src = srSrcs().find(x => x.course === code);
const set = SR_SHELF_SET[code] || srTopicSet(code) || null;
Object.assign(IB, { kind: 'game', goal: 'exam', src: src ? src.id : 'lib', len: 10, topic: set ? SR_SETS[set].topic : 'the key terms in ' + code });
IB.say = `A practice quiz for ${code}. Change any word, then press Build it.`;
}
};
/* ================= Community: the library home, for games or tools ================= */
function srClockHTML() {
const end = addDays((6 - TODAY.getDay() + 7) % 7 || 7); end.setHours(23, 59, 0, 0);
let ms = Math.max(0, end - Date.now());
const d = Math.floor(ms / 864e5); ms -= d * 864e5; const h = Math.floor(ms / 36e5); ms -= h * 36e5; const m = Math.floor(ms / 6e4); const s = Math.floor((ms - m * 6e4) / 1000);
return [[d, 'Days'], [h, 'Hrs'], [m, 'Min'], [s, 'Sec']].map(([v, l]) => `<div><b>${String(v).padStart(2, '0')}</b><span>${l}</span></div>`).join('');
}
setInterval(() => { const c = $('#sr-clock'); if (c && !document.hidden) c.innerHTML = srClockHTML(); }, 1000);
function srHome(kind) {
const all = srAll().filter(i => i.kind === kind), showS = SR.src !== 'orig', showO = SR.src !== 'stud', q = norm(SR.q);
const match = it => !q || norm(it.name + ' ' + (it.desc || SR_ORIG_DESC[it.id] || '') + ' ' + (it.course || '')).includes(q);
const studs = all.filter(match), origs = SR_ORIG.filter(c => c.kind === kind && match(c));
const school = S.mine && BY_ID[S.mine] ? BY_ID[S.mine] : null;
let h = '';
if (!q) {
if (showS && kind === 'game') {
const it = studyItem('lyso'), p = srPerson(it.by), c = srCounts(it);
h += `<section class="cm-hero" aria-labelledby="pickTitle"><div><span class="eyebrow">${icon('i-trophy', 'xs')}Community pick of the week</span><h2 id="pickTitle" class="h1">Lysosome Defense</h2><div class="badges" style="margin-top:12px"><span class="badge b-stud">${icon('i-users')}Student-made</span><span class="badge b-check">${icon('i-shield')}Hoot-checked · 3 sources</span></div><p class="d">${esc(it.desc)}</p>
<div class="who"><span class="av" style="--av:${p.c}">${p.av}</span><span><b>${p.n}</b><span>${p.s} · ${it.course}</span></span></div>
<div class="hstats"><div><b class="tnum">${compact(it.plays)}</b><span>plays</span></div><div><b class="tnum">${Math.round(c.up / (c.up + c.down) * 100)}%</b><span>thumbs up</span></div><div><b class="tnum">${it.remixes}</b><span>remixes</span></div></div>
<div class="hcta"><a class="btn btn-primary" href="#game-lyso" data-act="sr-autoplay">${icon('i-play', 'sm fill')}Play</a><button type="button" class="btn btn-on-stage" data-act="sr-remix" data-id="lyso">${icon('i-remix', 'sm')}Remix this</button><a class="btn btn-on-stage" href="#game-lyso">Details</a></div></div>
<div class="cm-art">${lysoSVG()}</div></section>`;
}
if (showS && kind === 'tool') {
const it = studyItem('citation'), p = srPerson(it.by), c = srCounts(it);
h += `<section class="cm-hero" aria-labelledby="pickTitle"><div><span class="eyebrow">${icon('i-trophy', 'xs')}Most used student tool this week</span><h2 id="pickTitle" class="h1">Citation Fixer</h2><div class="badges" style="margin-top:12px"><span class="badge b-stud">${icon('i-users')}Student-made</span><span class="badge b-check">${icon('i-shield')}Hoot-checked</span></div><p class="d">${esc(it.desc)}</p><div class="who"><span class="av" style="--av:${p.c}">${p.av}</span><span><b>${p.n}</b><span>${p.s} · ${it.course}</span></span></div>
<div class="hstats"><div><b class="tnum">${compact(it.plays)}</b><span>uses</span></div><div><b class="tnum">${Math.round(c.up / (c.up + c.down) * 100)}%</b><span>thumbs up</span></div><div><b class="tnum">${it.remixes}</b><span>remixes</span></div></div>
<div class="hcta"><a class="btn btn-primary" href="#game-citation" data-act="sr-autoplay">${icon('i-play', 'sm fill')}Open tool</a><button type="button" class="btn btn-on-stage" data-act="sr-remix" data-id="citation">${icon('i-remix', 'sm')}Remix this</button></div></div><div class="cm-art" style="display:grid;place-items:center;padding:20px"><div class="thumb" style="width:100%">${it.thumb}</div></div></section>`;
}
if (showS) h += `<section class="chal" aria-labelledby="chalTitle">${hootProf(82, 'hoot-bob')}<div><span class="eyebrow">${icon('i-flame', 'xs')}Weekly creator challenge</span><h2 id="chalTitle">Midterm Week: Chemistry</h2><p>Build a game or tool that helps CHM 113 students with stoichiometry. Professor Hoot and the StudyBoost team pick three winners on Saturday.</p><div class="chal-meta"><span>${icon('i-users', 'sm')}380 entries</span><span>${icon('i-trophy', 'sm')}Winners featured in the Study Room, plus 3 months of Pro</span></div></div>
<div class="chal-r"><div class="clock" id="sr-clock" aria-label="Time left">${srClockHTML()}</div><button type="button" class="btn btn-primary" data-act="sr-challenge">${icon('i-wand', 'sm')}Enter the challenge</button></div></section>`;
}
const cnt = arr => arr.length;
if (kind === 'game') {
const here = school && school.id !== 'um' ? school.short : 'Miami';
const trend = studs.filter(i => i.mine || srPerson(i.by).s === here);
if (showS && cnt(trend)) h += srRow('trend', 'flame', school ? `Trending at ${esc(school.name)}` : 'Trending this week', school ? 'What your classmates are playing this week' : 'What students are playing this week', trend.map(srCtile).join(''));
if (showO && cnt(origs)) h += srRow('orig', 'check', 'StudyBoost Originals', 'Built and maintained by the StudyBoost team', origs.map(srOtile).join(''));
const fresh = studs.filter(i => i.from || i.mine).sort((a, b) => (b.mine ? 1 : 0) - (a.mine ? 1 : 0));
if (showS && cnt(fresh)) h += srRow('remixes', 'remix', 'Fresh remixes', 'New takes on games that already work', fresh.map(srCtile).join(''));
const more = studs.filter(i => !i.from && !i.mine && !trend.includes(i));
if (showS && cnt(more)) h += srRow('all-s', 'users', 'More from students', 'Across every school on StudyBoost', more.map(srCtile).join(''));
} else {
if (showS && !q && AI_COURSES.length && S.persona !== 'new') h += `<section class="row" data-row="ai" id="ai-courses"><div class="row-head"><div><h2>${icon('i-cap')}Your AI courses</h2><p>Lessons and quizzes built from your own notes</p></div></div><div class="ai-row">${AI_COURSES.map(a => `<button type="button" class="ai-card" data-act="sr-ai" data-id="${a.id}"><span class="appicon" style="--ai:linear-gradient(145deg,#8c6cf7,#5a3fd6)">${icon('i-cap')}</span><span class="ai-t"><b>${esc(a.name)}</b><span>From ${esc(a.from)} · next: ${esc(a.next)}</span><span class="ai-bar" aria-hidden="true"><i style="width:${a.pct}%"></i></span></span><span class="ai-pct tnum">${a.pct}%</span></button>`).join('')}<button type="button" class="ai-card new" data-act="ai-course-make"><span class="appicon">${icon('i-plus')}</span><span class="ai-t"><b>New AI course</b><span>Upload a syllabus or notes</span></span><span class="s-pro">PRO</span></button></div></section>`;
if (showS && cnt(studs)) h += srRow('stools', 'users', 'Made by students', 'Tools students built around their own documents', studs.map(srCtile).join(''));
if (showO && cnt(origs)) h += srRow('otools', 'check', 'StudyBoost Originals', 'Built and maintained by the StudyBoost team', origs.map(srOtile).join(''));
if (showS && cnt(studs) > 2 && !q) h += srRow('mostremix', 'remix', 'Most remixed tools', 'The tools students build on most', [...studs].sort((a, b) => b.remixes - a.remixes).map(srCtile).join(''));
}
if (q && !studs.length && !origs.length) h += `<div class="sr-empty">${emptyBox(`Nothing matches “${esc(SR.q)}”`, 'Put it in the sentence above and Hoot will build it from your notes.', `<button type="button" class="s-btn sm primary" data-act="sr-make-q">${icon('i-wand', 'sm')}Make it yourself</button>`)}</div>`;
if (showS && !q) h += `<section class="row"><div class="row-head"><div><h2>${icon('i-trophy')}Top creators this month</h2><p>Ranked by plays. Every one of their games passed Hoot’s checks.</p></div></div><div class="lb">${SR_LB.map(([k, plays, made, checked], i) => { const p = SR_P[k]; return `<div class="lbc${i < 3 ? ' topc' : ''}"><span class="rk">${i + 1}</span><span class="av" style="--av:${p.c}">${p.av}</span><div><b>${p.n}</b><div class="s">${p.s}</div></div><dl><div><dt>Plays</dt><dd class="tnum">${compact(plays)}</dd></div><div><dt>Hoot-checked</dt><dd class="tnum">${checked} of ${made}</dd></div></dl></div>`; }).join('')}</div></section>`;
return h;
}
function srHomeTop(kind) {
const n = kind === 'game' ? '2,316' : '874';
return `<div class="cm-top"><div class="seg" role="group" aria-label="Who made it">${[['all', 'Everything', ''], ['orig', 'StudyBoost Originals', 'i-check'], ['stud', 'Made by students', 'i-users']].map(([v, l, ic]) => `<button type="button" data-act="sr-src" data-v="${v}" aria-pressed="${SR.src === v}">${ic ? icon(ic) : ''}${l}</button>`).join('')}</div>
<label class="sr-search">${icon('i-search', 'sm')}<span class="sr">Search the Study Room</span><input id="sr-q" type="search" value="${esc(SR.q)}" placeholder="Search ${kind === 'game' ? 'games' : 'tools'}" autocomplete="off"></label>
<span class="stat">${icon('i-users', 'sm')}<span><b>${n}</b> made by students this month</span></span></div>`;
}
/* ================= Made by you ================= */
const SR_ST = { pub: ['Published', 'i-globe'], draft: ['Draft', 'i-history'], priv: ['Private', 'i-lock'] };
function srMkCard(m) {
const c = srCounts(m), pct = c.up + c.down ? Math.round(c.up / (c.up + c.down) * 100) : null, st = SR_ST[m.st] || SR_ST.draft;
return `<article class="mkcard${m.fresh ? ' fresh' : ''}"><div class="thumb">${srThumbInner(m.set, m.kind)}</div><h3>${esc(m.name)}</h3><p class="wish">“${esc(m.sentence)}”</p>
<div class="mmeta"><span class="pill ${m.st}">${icon(st[1])}${st[0]}</span><span>${plural(m.plays, 'play')}</span>${pct != null ? `<span>${icon('i-thumb', 'xs')} ${pct}%</span>` : ''}${m.from && studyItem(m.from) ? `<span>${icon('i-remix', 'xs')} Remix of ${esc(studyItem(m.from).name)}</span>` : ''}</div>
<div class="mk-acts"><button type="button" class="btn btn-primary btn-sm" data-act="sr-open" data-id="${m.id}">${icon('i-wand', 'sm')}Open in Studio</button><button type="button" class="linkish" data-act="sr-edit" data-id="${m.id}">Edit the sentence</button>${m.st === 'pub' ? `<a class="linkish" href="#game-${m.id}">View in the library</a>` : ''}</div></article>`;
}
function srMine() {
const list = srMade();
return `<div class="sr-ib"><section class="mine-view"><div class="mine-head"><div><h1>Made by you</h1><p>Every game and tool keeps the sentence that made it. Change a word to make a new version.</p></div><a class="btn btn-primary" href="#study-create">${icon('i-plus', 'sm')}Make something new</a></div>
${list.length ? `<div class="mine-grid">${list.map(srMkCard).join('')}</div>` : `<div class="sr-empty">${emptyBox('Nothing here yet', 'Describe a game or tool in one sentence and Hoot builds it from your notes. It stays private until you publish it.', `<a class="s-btn sm primary" href="#study-create">${icon('i-wand', 'sm')}Make your first one</a>`)}</div>`}</section></div>`;
}
/* ================= a game's own page: play it, rate it, see where it came from, remix it ================= */
function srDetail(it) {
const p = srPerson(it.by), parent = it.from && studyItem(it.from), grand = parent && parent.from && studyItem(parent.from);
const kids = srAll().filter(x => x.from === it.id).slice(0, 3), isGame = it.kind === 'game', c = srCounts(it), pct = c.up + c.down ? Math.round(c.up / (c.up + c.down) * 100) : null;
const docs = it.orig ? ['StudyBoost library: trusted guides on each topic'] : (it.docs && it.docs.length ? it.docs : ['Your notes']);
const node = x => { const q = srPerson(x.by); return `<a class="node${x.id === it.id ? ' cur' : ''}" href="#game-${x.id}"><span class="av" style="--av:${q.c}">${q.av}</span>${esc(x.name)}</a>`; };
const stage = it.thumb === 'lyso' ? `<div class="cm-art">${lysoSVG()}</div>` : `<div class="cm-art" style="display:grid;place-items:center;padding:24px"><div class="thumb ${it.thumbClass || it.cls || ''}" style="width:min(560px,100%)">${it.thumb}</div></div>`;
return `<div class="sr-cm"><div class="dt"><div class="dstage" id="dStage">${stage}<div class="dplay"><button type="button" class="btn btn-primary" data-act="sr-play">${icon('i-play', 'sm fill')}${isGame ? 'Play' : 'Try it'}</button></div></div>
<div class="dside"><div class="badges">${it.orig ? `<span class="badge b-orig">${icon('i-check')}StudyBoost Original</span>` : `<span class="badge b-stud">${icon('i-users')}Student-made</span>`}<span class="badge b-check">${icon('i-shield')}Hoot-checked</span>${it.mine ? `<span class="badge b-stud">${icon('i-sparkle')}Yours</span>` : ''}</div>
<h1>${esc(it.name)}</h1><div class="dwho"><span class="av" style="--av:${p.c}">${p.av}</span><span><b>${esc(p.n)}</b><span>${esc(it.orig ? it.cat : p.s + ' · ' + it.course)}</span></span></div><p class="lede">${esc(it.desc || SR_ORIG_DESC[it.id] || '')}</p>
<div class="dact"><button type="button" class="btn btn-primary" data-act="sr-play">${icon('i-play', 'sm fill')}${isGame ? 'Play' : 'Try it'}</button>${it.mine ? `<button type="button" class="btn btn-outline" data-act="sr-open" data-id="${it.id}">${icon('i-wand', 'sm')}Open in Studio</button>` : `<button type="button" class="btn btn-outline" data-act="sr-remix" data-id="${it.id}">${icon('i-remix', 'sm')}Remix this</button>`}</div>
<div class="box"><h3>${icon('i-thumb')}Rating</h3><div class="rate-row"><span class="pct tnum" id="rPct">${pct != null ? pct + '%' : 'New'}</span><div class="rbar"><i id="rBar" style="width:${pct || 0}%"></i></div><span class="cnt tnum" id="rCnt">${fmt(c.up)} up · ${fmt(c.down)} down</span></div><span class="rmeta">${compact(it.plays)} ${isGame ? 'plays' : 'uses'}${it.orig ? '' : ` · remixed ${plural(it.remixes, 'time')}`}</span></div>
<div class="box"><h3>${icon('i-shield')}Hoot-checked</h3><span class="rmeta2">Every answer was traced to a page in ${it.orig ? 'the StudyBoost library' : plural(docs.length, 'document')}. Hoot re-checks a question whenever a player reports it.</span><ul class="docs">${docs.map(d => `<li>${icon('i-file')}${esc(d)}</li>`).join('')}</ul></div>
${it.orig ? '' : `<div class="box"><h3>${icon('i-remix')}Remix lineage</h3><div class="lin">${grand ? node(grand) + icon('i-right') : ''}${parent ? node(parent) + icon('i-right') : ''}${node(it)}${it.remixes ? `${icon('i-right')}<span class="rmeta">${plural(it.remixes, 'remix', 'remixes')}</span>` : ''}</div>
${kids.length ? `<div class="remixes">${kids.map(k => `<a href="#game-${k.id}" class="rm"><div class="thumb ${k.cls || ''}">${k.thumb}</div><b>${esc(k.name)}</b><span>${esc(srPerson(k.by).n)}${srPct(k) != null ? ' · ' + srPct(k) + '%' : ''}</span></a>`).join('')}</div>` : ''}</div>`}
<button type="button" class="report" data-act="sr-report">${icon('i-flag', 'sm')}Report a problem</button><div id="repBox"></div></div></div></div>`;
}
/* ---------- play (the Community player: lives, waves, the page each answer came from) ---------- */
const TD = {};
const tdHeart = lost => `<svg viewBox="0 0 24 24" class="${lost ? 'lost' : ''}"><use href="#i-heart"/></svg>`;
function tdStart(it, host) {
const set = SR_SETS[it.set] || SR_SETS.bio;
Object.assign(TD, { it, host, qs: set.qs.slice(0, 4), i: 0, lives: 3, score: 0, field: SR_FIELD.has(it.id), locked: false });
tdPaint();
}
const tdHud = () => TD.field ? `Wave ${TD.i + 1} of ${TD.qs.length} · ${TD.score} zapped` : `${TD.it.kind === 'tool' ? 'Check' : 'Question'} ${TD.i + 1} of ${TD.qs.length} · ${TD.score} right`;
function tdPaint() {
const q = TD.qs[TD.i], h = TD.host; if (!h || !document.contains(h)) return;
h.innerHTML = `<div class="td"><div class="td-hud"><span>${esc(TD.it.name)}</span><span class="lives">${[0, 1, 2].map(n => tdHeart(n >= TD.lives)).join('')}</span><span class="sc tnum">${tdHud()}</span></div>${TD.field ? `<div class="td-field">${lysoSVG(true)}</div>` : ''}
<div class="td-q"><h3>${esc(q.q)}</h3><div class="tdo">${q.o.map((o, n) => `<button type="button" data-act="td-a" data-a="${n}"><span class="k">${'ABCD'[n]}</span>${esc(o)}</button>`).join('')}</div><div id="tdfb" aria-live="polite"></div></div></div>`;
for (let k = 0; k < TD.score; k++) { const g = $(`[data-pg="${k}"]`, h); if (g) g.classList.add('zapped'); }
TD.locked = false;
const f = $('.tdo button', h); if (f) f.focus({ preventScroll: true });
}
function tdEnd() {
const won = TD.lives > 0, it = TD.it, v = srVote(it.id), h = TD.host;
h.innerHTML = `<div class="td"><div class="td-end"><span class="badge b-check">${icon('i-shield')}Hoot-checked</span><h3>${TD.field ? (won ? 'The cell holds' : 'The cell was overrun') : it.kind === 'tool' ? 'All checks done' : (won ? 'Run complete' : 'Out of lives')}</h3><p>${TD.field ? `${TD.score} of ${TD.qs.length} pathogens zapped${won ? ` with ${plural(TD.lives, 'life', 'lives')} left` : ''}` : `${TD.score} of ${TD.qs.length} right${won ? '' : '. Review the answers and try again'}`}.</p>
${it.preview ? '<p style="margin-top:6px">This is a preview. Tell Hoot what to change, or publish it.</p>' : `<p style="margin-top:6px">Did this help you study?</p><div class="vote"><button type="button" data-act="td-vote" data-v="up" aria-pressed="${v === 'up'}">${icon('i-thumb', 'sm')}Yes</button><button type="button" class="down" data-act="td-vote" data-v="down" aria-pressed="${v === 'down'}">${icon('i-thumb-down', 'sm')}Not really</button></div>`}
<div class="dact" style="justify-content:center;margin-top:8px"><button type="button" class="sbtn ghost" data-act="td-again">${icon('i-refresh', 'sm')}Play again</button>${it.mine ? '' : `<button type="button" class="sbtn ghost" data-act="sr-remix" data-id="${it.id}">${icon('i-remix', 'sm')}Remix it</button>`}</div></div></div>`;
const b = $('.vote button', h); if (b) b.focus({ preventScroll: true });
}
Object.assign(ACT, {
'sr-play': () => { const it = studyItem(R.id); const host = $('#dStage'); if (it && host) { tdStart(it, host); host.scrollIntoView({ block: 'nearest', behavior: reduceMotion() ? 'auto' : 'smooth' }); } },
'sr-autoplay': () => { SR.autoplay = true; },
'td-a': el => {
if (TD.locked) return; TD.locked = true;
const q = TD.qs[TD.i], pick = +el.dataset.a, ok = pick === q.a, h = TD.host;
$$('.tdo button', h).forEach(x => { x.disabled = true; if (+x.dataset.a === q.a) x.classList.add('good'); else if (x === el) x.classList.add('bad'); });
if (ok) { const z = $(`[data-zap="${TD.score}"]`, h), g = $(`[data-pg="${TD.score}"]`, h); if (z) z.classList.add('on'); setTimeout(() => g && g.classList.add('zapped'), reduceMotion() ? 0 : 180); TD.score++; }
else { TD.lives--; const g = $(`[data-pg="${TD.score}"]`, h); if (g) g.style.transform = 'translateX(-90px)'; }
$('.td-hud .lives', h).innerHTML = [0, 1, 2].map(n => tdHeart(n >= TD.lives)).join('');
$('.td-hud .sc', h).textContent = tdHud();
const last = TD.i === TD.qs.length - 1 || TD.lives <= 0;
$('#tdfb', h).innerHTML = `<div class="tdfb${ok ? '' : ' bad'}"><span><strong>${ok ? (TD.field ? 'Zapped.' : 'Right.') : (TD.field ? 'It got through.' : 'Not quite.')}</strong> ${ok ? '' : 'The answer is ' + esc(q.o[q.a]) + '. '}${esc(q.why || q.h || '')}</span><span class="cite">${icon('i-file')}${esc(TD.it.orig ? 'StudyBoost library' : (TD.it.docs && TD.it.docs[0] ? TD.it.docs[0].replace(/\.\w+$/, '') + ', ' : 'Notes, ') + q.p)}</span><button type="button" class="sbtn" data-act="td-next">${last ? 'Finish' : TD.field ? 'Next wave' : 'Next'}${icon('i-arrow', 'sm')}</button></div>`;
const nb = $('[data-act="td-next"]', h); if (nb) nb.focus({ preventScroll: true });
},
'td-next': () => { if (TD.i < TD.qs.length - 1 && TD.lives > 0) { TD.i++; tdPaint(); } else tdEnd(); },
'td-vote': el => {
const it = TD.it || studyItem(R.id), v = el.dataset.v, k = 'g:' + it.id;
if (S.votes[k] === v) delete S.votes[k]; else S.votes[k] = v;
save();
$$('[data-act="td-vote"]').forEach(b => b.setAttribute('aria-pressed', S.votes[k] === b.dataset.v));
const c = srCounts(it), pct = c.up + c.down ? Math.round(c.up / (c.up + c.down) * 100) : 0;
const rp = $('#rPct'); if (rp) { rp.textContent = pct + '%'; $('#rBar').style.width = pct + '%'; $('#rCnt').textContent = `${fmt(c.up)} up · ${fmt(c.down)} down`; }
toast(!S.votes[k] ? 'Rating removed' : v === 'up' ? `Thanks. ${it.orig ? 'The StudyBoost team' : srPerson(it.by).n.split(' ')[0]} will see the thumbs up.` : 'Thanks. Your rating helps other students choose.');
},
'sr-report': () => {
const box = $('#repBox'); if (!box) return;
box.innerHTML = `<form class="box rform" data-form="sr-report"><h3>${icon('i-flag')}What’s wrong?</h3><label class="sr" for="repKind">Problem type</label><select id="repKind"><option>An answer is wrong</option><option>A question is confusing</option><option>Inappropriate content</option><option>Something else</option></select><label class="sr" for="repText">Details</label><textarea id="repText" placeholder="Which question, and what should it say?"></textarea><div class="dact"><button type="submit" class="btn btn-primary btn-sm">Send report</button><button type="button" class="btn btn-ghost btn-sm" data-act="sr-report-x">Cancel</button></div></form>`;
$('#repKind').focus();
},
'sr-report-x': () => { const b = $('#repBox'); if (b) b.innerHTML = ''; const r = $('[data-act="sr-report"]'); if (r) r.focus(); },
'sr-rail': el => { const r = $('.rail, .ai-row', el.closest('.row')); if (r) r.scrollBy({ left: +el.dataset.dir * r.clientWidth * 0.75, behavior: reduceMotion() ? 'auto' : 'smooth' }); },
'sr-src': el => { SR.src = el.dataset.v; srRepaintHome(); },
'sr-challenge': () => {
ibReset('game');
Object.assign(IB, { kind: 'game', goal: 'practice', topic: 'stoichiometry', src: srSrcs().some(s => s.id === 'shelf-chm') ? 'shelf-chm' : 'lib', len: 10, say: 'You’re in the Midterm Week challenge. Change any word, then press Build it.' });
SR.challenge = true; SR.proj = null; go('study-create');
},
'sr-make-q': () => { const t = SR.q; ibReset(R.tab === 'tools' ? 'tool' : 'game'); if (t) { IB.topic = t; IB.say = 'I put your search in the sentence. Change any word, then press Build it.'; } SR.q = ''; SR.proj = null; go('study-create'); },
'sr-ai': el => { const a = AI_COURSES.find(x => x.id === el.dataset.id); if (window.HOOT) HOOT.open(`Continue my AI course: ${a.name}`); else toast(`${a.name}: resumes at ${a.next}.`); },
'sr-open': el => {
const m = S.made.find(x => x.id === el.dataset.id); if (!m) return;
const src = srSrcs().find(x => x.id === (m.idea || {}).src) || srSrcs().slice(-1)[0];
SR.proj = srNewProj({ kind: m.kind, mech: m.mech, setKey: m.set, title: m.name, sentence: m.sentence, src, idea: m.idea, editing: m.id, built: true, remixOf: m.from || null,
versions: Array.from({ length: m.versions || 1 }, (_, i) => ({ n: i + 1, label: i === 0 ? 'First build' : i === 1 ? 'Explanations' : 'Timer 20s', feat: i === 0 ? {} : i === 1 ? { explain: true } : { explain: true, timer: true } })) });
SR.proj.cur = SR.proj.versions.length - 1;
SR.proj.log = [{ t: 'me', html: esc(m.sentence) }, { t: 'ver', v: SR.proj.cur, text: `Welcome back to <b>${esc(m.name)}</b>. You’re on version ${SR.proj.versions.length}. Tell me what to change, or play it first.` }];
go('study-create');
},
'sr-edit': el => {
const m = S.made.find(x => x.id === el.dataset.id); if (!m) return;
ibReset(m.kind); Object.assign(IB, m.idea || {}, { alt: null, own: false, editing: m.id, say: `Here’s the sentence for ${m.name}. Change a word to make a new version.` });
IB.changed = 'all'; SR.proj = null; go('study-create');
},
'sr-remix': el => srRemixOpen(studyItem(el.dataset.id)),
'sr-keep': el => el.setAttribute('aria-pressed', el.getAttribute('aria-pressed') !== 'true'),
'sr-kind': el => { $$('[data-act="sr-kind"]').forEach(b => b.setAttribute('aria-pressed', b === el)); }
});
FORMS['sr-report'] = () => {
const it = studyItem(R.id), p = srPerson(it.by), box = $('#repBox');
box.innerHTML = `<div class="box"><h3>${icon('i-check')}Report sent</h3><span class="rmeta2">Hoot will re-check it against ${it.orig ? 'the StudyBoost library' : esc(p.n) + '’s sources, and ' + esc(p.n.split(' ')[0]) + ' gets a note to review it'}. Thanks for helping keep this accurate.</span></div>`;
};
function srRepaintHome() {
const host = $('#sr-home'); if (!host) return;
host.innerHTML = srHome(R.tab === 'tools' ? 'tool' : 'game');
$$('[data-act="sr-src"]').forEach(b => b.setAttribute('aria-pressed', b.dataset.v === SR.src));
}
/* ---------- remix: what should change, what to keep, and the original creator stays credited ---------- */
function srRemixOpen(it) {
if (!it) return;
const p = srPerson(it.by), mine = srSrcs().filter(s => s.id !== 'lib');
const prompt = it.id === 'lyso' ? 'Keep the tower-defense rules but use my CHM 113 notes on stoichiometry.' : it.id === 'citation' ? 'Keep how it explains each fix, but add Chicago style for my history papers.' : `Keep the rules of ${it.name} but use my own notes for ${mine[0] ? mine[0].course : 'my class'}.`;
openModal(`<div class="sr-cm sr-remix"><form data-form="sr-remix" data-id="${it.id}"><div class="dh"><div><span class="eyebrow">${icon('i-remix', 'xs')}Remix</span><h2>Remix ${esc(it.name)}</h2></div><button type="button" class="icon-btn" data-act="modal-close" aria-label="Close">${icon('i-x', 'sm')}</button></div>
<div class="src-card">${srThumbHTML(it)}<span><span class="kick">Remixing</span><b>${esc(it.name)}</b><span>by ${esc(p.n)} · ${esc(p.s)}</span></span></div>
<div class="fld"><label for="sr-remix-p">What should change?</label><textarea id="sr-remix-p" placeholder="Keep the rules, but use my notes on…">${esc(prompt)}</textarea></div>
<span class="doc">${icon('i-file')}${mine.length ? `Hoot will read ${esc(mine.map(s => s.label).join(', '))}` : 'Hoot will use trusted guides from the StudyBoost library'}</span>
<div class="fld"><span class="kick">What to keep</span><div class="keep">${[['rules', 'Rules'], ['art', 'Art style'], ['curve', 'Difficulty curve']].map(([k, l], i) => `<button type="button" data-act="sr-keep" data-keep="${k}" aria-pressed="${i < 2}">${icon('i-check')}${l}</button>`).join('')}</div></div>
${it.orig ? '' : `<div class="credit">${icon('i-users')}<span>${esc(p.n.split(' ')[0])} stays credited as the original creator, and your remix links back to ${esc(it.name)}.</span></div>`}
<div class="dfoot"><button type="button" class="btn btn-outline" data-act="modal-close">Cancel</button><button type="submit" class="btn btn-primary">${icon('i-wand', 'sm')}Build remix</button></div></form></div>`, { scope: 'pg-study', cls: 'sr-mcard', label: 'Remix ' + it.name, focus: '#sr-remix-p' });
}
FORMS['sr-remix'] = f => {
const it = studyItem(f.dataset.id), prompt = $('#sr-remix-p', f).value.trim();
if (prompt.length < 10) { toast('Tell Hoot a little more about what you want.'); $('#sr-remix-p', f).focus(); return; }
const keep = $$('[data-act="sr-keep"][aria-pressed="true"]', f).map(b => b.dataset.keep);
const setKey = srTopicSet(prompt) || it.set || 'bio', set = SR_SETS[setKey];
const src = srSrcs().find(s => s.set === setKey) || srSrcs().slice(-1)[0];
const name = setKey !== it.set ? `${set.T} ${/Defense|Wars/.test(it.name) ? 'Defense' : it.kind === 'tool' ? 'Fixer' : 'Showdown'}` : `${it.name}: Your Edition`;
SR.proj = srNewProj({ kind: it.kind, mech: it.kind === 'game' ? 'Boss Battle' : 'Self-quiz Cheat Sheet', setKey, title: name, sentence: prompt, src, remixOf: it.id, keep,
idea: { kind: it.kind, goal: it.kind === 'game' ? 'practice' : 'memorize', topic: set.topic, src: src.id, len: 10 } });
closeModal(true); go('study-create');
};
/* ================= the page ================= */
const SR_TABS = [['study', 'i-gamepad', 'Games', 'games'], ['study-tools', 'i-sprout', 'Tools', 'tools'], ['study-mine', 'i-layers', 'Made by you', 'mine'], ['study-create', 'i-wand', 'Create', 'create']];
function srTabs(tab) {
return `<nav class="sr-tabs" aria-label="Study Room sections">${SR_TABS.map(([t, ic, l, k]) => `<a href="#${t}" class="sr-tab"${tab === k ? ' aria-current="page"' : ''}>${icon(ic)}${l}${k === 'mine' && S.made.length ? `<span class="count">${S.made.length}</span>` : ''}${k === 'create' ? '<span class="new">New</span>' : ''}</a>`).join('')}</nav>`;
}
PAGES.study = {
cls: () => 'pg-study',
render(r) {
if (!IB.kind) ibReset();
if (r.tab === 'detail') {
const it = studyItem(r.id);
return `<div class="page-in">${crumbs([['study', 'Study Room'], [it.kind === 'tool' ? 'study-tools' : 'study', it.kind === 'tool' ? 'Tools' : 'Games'], [TOKEN, it.name]])}${srDetail(it)}</div>`;
}
const tab = r.tab;
const head = `<div class="sr-head"><h1>${tab === 'create' && SR.proj ? 'Study Room' : 'Study Room'}</h1><p>${tab === 'tools' ? 'AI tools that work on your own documents, and tools other students built.' : tab === 'mine' ? 'The games and tools you made, with the sentence behind each one.' : tab === 'create' ? 'Describe a game or tool in one sentence. Hoot builds it from your notes, and you refine it by talking.' : 'Games for quick recall, from the StudyBoost team and from students like you.'}</p></div>`;
let body = '';
if (tab === 'games' || tab === 'tools') {
const kind = tab === 'tools' ? 'tool' : 'game';
if (IB.kind !== kind) ibReset(kind);
body = `<div class="sr-ib">${ibBand()}</div><div class="sr-cm">${srHomeTop(kind)}<div id="sr-home">${srHome(kind)}</div></div>`;
} else if (tab === 'mine') body = srMine();
else if (tab === 'create') body = SR.proj ? srStudio(SR.proj) : srCreateHome();
return `<div class="page-in">${crumbs([['study', 'Study Room']].concat(tab === 'games' ? [] : [[TOKEN, { tools: 'Tools', mine: 'Made by you', create: 'Create' }[tab]]]))}${tab === 'create' && SR.proj ? '' : head}${srTabs(tab)}<div class="sr-body">${body}</div></div>`;
},
mount(r, page) {
const pin = $('.page-in', page);
const q = $('#sr-q', pin);
if (q) q.addEventListener('input', () => { SR.q = q.value.trim(); const host = $('#sr-home'); if (host) host.innerHTML = srHome(R.tab === 'tools' ? 'tool' : 'game'); });
if (r.tab === 'detail' && SR.autoplay) { SR.autoplay = false; setTimeout(() => ACT['sr-play'](), 60); }
if (r.tab === 'create' && SR.proj) stMount(SR.proj);
}
};
RESETS.push(() => { SR.proj = null; SR.src = 'all'; SR.q = ''; ibReset(); });
/* ===== 78c-studio.js ===== */
/* ================= Studio: Hoot builds from the sentence, the chat refines it, Publish sends it to the library ================= */
function srNewProj(o) { return Object.assign({ id: 'p' + Date.now(), versions: [{ n: 1, label: 'First build', feat: {} }], cur: 0, built: false, busy: false, log: [], remixOf: null, editing: null, keep: [] }, o); }
function srCreateHome() {
const mine = srMade();
const pill = m => `<span class="pill pill-${m.st === 'pub' ? 'pub' : m.st === 'draft' ? 'draft' : 'private'}">${icon(SR_ST[m.st][1])}${SR_ST[m.st][0]}</span>`;
return `<div class="sr-ib">${ibBand()}</div><div class="sr-st">
<section class="row"><div class="sec-head"><div><h2>${icon('i-layers')}Your creations</h2><p>Drafts stay private. Open one to keep working on it.</p></div></div>
${mine.length ? `<div class="mine">${mine.map(m => `<button type="button" class="mine-card" data-act="sr-open" data-id="${m.id}"><div class="thumb">${m.thumb}</div><span class="mine-tx"><b>${esc(m.name)}</b><span class="sub">${esc(m.course)} · ${m.kind === 'tool' ? 'Tool' : 'Game'}</span>${pill(m)}</span></button>`).join('')}</div>` : '<p class="row-empty">Nothing yet. Build your first one above.</p>'}</section>
<section class="row"><div class="sec-head"><div><h2>${icon('i-sparkle')}How it works</h2></div></div><ol class="how" style="list-style:none;margin:0;padding:0">${[['i-message', 'Describe', 'Say what you want in one sentence: game or tool, the topic, and which notes to use.'], ['i-play', 'Play-test', 'Hoot builds a first version in about a minute. Play it, then ask for changes in the chat.'], ['i-globe', 'Publish', 'Hoot checks every answer against your notes. Share it with your class or everyone.']].map(([ic, t, d], i) => `<li class="how-step"><span class="how-ic">${icon(ic)}</span><div><h3>${t} <span>${i + 1}</span></h3><p>${d}</p></div></li>`).join('')}</ol></section></div>`;
}
const stChip = (p, v) => `<button type="button" class="vchip${p.cur === v ? ' cur' : ''}" data-act="st-v" data-v="${v}"><span class="vn">v${p.versions[v].n}</span>${esc(p.versions[v].label)}${p.cur === v ? '<span class="viewing">Viewing</span>' : ''}</button>`;
function stMsg(m, p) {
if (m.t === 'me') return `<div class="msg me"><div class="bubble-me">${m.html}</div></div>`;
let body = m.html || '';
if (m.t === 'build') body = `<p class="msg-text">${m.text}</p><ul class="blog">${m.steps.map((s, i) => `<li class="${i < m.at ? 'done' : i === m.at && p.busy ? 'run' : ''}"><span class="st">${icon('i-check')}</span><span>${esc(s[0])}${s[1] ? `<span class="d">${esc(s[1])}</span>` : ''}</span></li>`).join('')}</ul>${m.at >= m.steps.length && m.v != null ? stChip(p, m.v) : ''}`;
if (m.t === 'ver') body = `<p class="msg-text">${m.text}</p>${stChip(p, m.v)}`;
return `<div class="msg"><span class="msg-av">${hootSym(30)}</span><div class="msg-body"><span class="msg-name">Professor Hoot</span>${body}</div></div>`;
}
const stRefine = p => { const feat = p.versions[p.versions.length - 1].feat; return SR_REFINES[p.kind].map(r => `<button type="button" class="chip rchip${feat[r.id] ? ' used' : ''}" data-act="st-ref" data-ref="${r.id}"${feat[r.id] || p.busy ? ' disabled' : ''}>${icon(feat[r.id] ? 'i-check' : 'i-sparkle')}${esc(r.label)}</button>`).join(''); };
const stVers = p => p.versions.map((v, i) => `<option value="${i}"${i === p.cur ? ' selected' : ''}>v${v.n} · ${esc(v.label)}</option>`).join('');
function stMeta(p) {
const v = p.versions[p.cur], par = p.remixOf && studyItem(p.remixOf);
return `<span class="pill pill-private">${icon('i-lock')}${p.editing ? 'In Made by you' : 'Private'}</span><span>${esc(p.src.id === 'lib' ? 'StudyBoost library' : p.src.label)}</span>${par ? `<span>${icon('i-remix', 'xs')} Remix of ${esc(par.name)}</span>` : ''}<span class="st-saved">${icon('i-check', 'sm')}Saved as v${v.n}</span>`;
}
function srStudio(p) {
return `<div class="sr-st"><div class="st-head"><a class="btn btn-outline btn-sm" href="#study-mine">${icon('i-back', 'sm')}Made by you</a><div class="st-title"><h1 id="stName">${esc(p.title)}</h1><div class="st-meta" id="stMeta">${stMeta(p)}</div></div></div>
<p class="st-sent">“${esc(p.sentence)}” <button type="button" class="linkish" data-act="st-edit">Edit the sentence</button></p>
<div class="studio"><section class="pane-chat" aria-label="Conversation with Professor Hoot"><div class="chat-log" id="chatLog" aria-live="polite"></div>
<div class="chat-foot"><div class="refine-wrap"><span class="lbl">Try a change</span><div class="refine" id="refine">${stRefine(p)}</div></div>
<form class="chat-form" data-form="st-chat" novalidate><label for="chatInput" class="sr">Tell Hoot what to change</label><input id="chatInput" autocomplete="off" placeholder="Tell Hoot what to change…"><button type="submit" class="send" aria-label="Send">${icon('i-arrow-up', 'sm')}</button></form></div></section>
<section class="pane-preview" aria-label="Live preview"><div class="pv-bar"><label class="ver">${icon('i-history', 'sm')}<span class="sr">Version</span><select id="verSel" data-act-change="st-ver">${stVers(p)}</select></label><button type="button" class="icon-btn boxed" data-act="st-restart" aria-label="Restart preview">${icon('i-refresh', 'sm')}</button><span class="pv-spacer"></span><span class="pv-live" id="pvLive"><i></i><span>Saved</span></span><button type="button" class="btn btn-primary btn-sm" data-act="st-publish">${icon('i-globe', 'sm')}<span>Publish</span></button></div>
<div class="stage"><div class="stage-top" id="stageTop"></div><div class="stage-center"><div class="device"><div class="arena sr-cm" id="arena"></div></div></div><div class="stage-foot"><span>Every answer cites ${p.src.id === 'lib' ? 'the StudyBoost library' : 'your notes'}</span><button type="button" data-act="st-report">${icon('i-flag')}Report a problem</button></div></div></section></div></div>`;
}
const stWait = ms => new Promise(r => setTimeout(r, reduceMotion() ? 30 : ms));
function stPaint(p) {
if (SR.proj !== p) return;
const l = $('#chatLog'); if (!l) return;
l.innerHTML = p.log.map(m => stMsg(m, p)).join('') + (p.typing ? '<div class="typing" aria-label="Hoot is typing"><i></i><i></i><i></i></div>' : ''); l.scrollTop = l.scrollHeight;
const rf = $('#refine'); if (rf) rf.innerHTML = stRefine(p);
const live = $('#pvLive'); if (live) { live.classList.toggle('building', !!p.busy); live.lastElementChild.textContent = p.busy ? 'Building' : 'Saved'; }
const pb = $('[data-act="st-publish"]'); if (pb) pb.disabled = !!p.busy;
const ci = $('#chatInput'); if (ci) ci.disabled = !!p.busy;
const meta = $('#stMeta'); if (meta) meta.innerHTML = stMeta(p);
const vs = $('#verSel'); if (vs) vs.innerHTML = stVers(p);
const t = $('#stageTop'), v = p.versions[p.cur];
if (t) t.innerHTML = p.busy ? `<span class="bld">● Building v${v.n}</span>` : `<span class="ok">● Live preview</span><span class="ftag">v${v.n}</span>${SR_REFINES[p.kind].filter(r => v.feat[r.id]).map(r => `<span class="ftag">${r.tag}</span>`).join('')}`;
}
function stPlay(p) {
const a = $('#arena'); if (!a || SR.proj !== p) return;
if (p.busy || !p.built) { a.innerHTML = `<div class="st-wait">${hootProf(70, 'hoot-bob')}<p>Hoot is building ${esc(p.title)}…</p></div>`; return; }
tdStart({ id: p.id, name: p.title, kind: p.kind, set: p.setKey, docs: [p.src.id === 'lib' ? 'StudyBoost library' : p.src.label], preview: true, mine: true }, a);
}
function stMount(p) { stPaint(p); if (!p.built && !p.busy) stBuild(p); else stPlay(p); }
async function stBuild(p) {
p.busy = true; const set = SR_SETS[p.setKey], src = p.src, par = p.remixOf && studyItem(p.remixOf);
p.log.push({ t: 'me', html: `<span>${esc(p.sentence)}</span>${src.id !== 'lib' ? `<span class="att">${icon('i-clip', 'xs')}${esc(src.label)}</span>` : ''}` });
const steps = [
par ? [`Copying the parts you kept from ${par.name}`, (p.keep || []).map(k => ({ rules: 'Rules', art: 'Art style', curve: 'Difficulty curve' })[k]).join(', ') || 'Starting fresh'] : [`Reading ${src.id === 'lib' ? 'trusted guides on ' + set.T : src.label}`, src.pages ? `${src.pages} pages` : '3 sources, each cited'],
[`Pulling the facts about ${set.topic}`, `${set.qs.length * 8 + 4} facts worth testing`],
[`Shaping it into a ${p.mech}`, p.kind === 'game' ? 'Rounds, scoring and feedback' : 'Sections, checks and feedback'],
['Writing questions from common mix-ups', 'Each one linked to a page'],
['Checking every answer against your notes', `${set.qs.length} of ${set.qs.length} match a page`]];
const m = { t: 'build', text: 'On it. Here’s what I’m doing, step by step.', steps, at: 0, v: null };
p.log.push(m); stPaint(p); stPlay(p);
for (let i = 0; i < steps.length; i++) { await stWait(620); m.at = i + 1; stPaint(p); }
p.busy = false; p.built = true;
p.log.push({ t: 'ver', v: 0, text: `<b>${esc(p.title)}</b> is ready. Play a round, then tell me what to change. Every version is saved, so you can always go back.` });
stPaint(p); stPlay(p);
}
async function stRefineRun(p, id, text) {
if (p.busy || !p.built) return;
const last = p.versions[p.versions.length - 1]; p.busy = true;
p.log.push({ t: 'me', html: esc(text) }); p.typing = true; stPaint(p);
await stWait(650); p.typing = false;
const r = SR_REFINES[p.kind].find(x => x.id === id), feat = Object.assign({}, last.feat);
let reply, label;
if (r && !last.feat[id]) { feat[id] = true; label = r.tag; reply = r.reply; }
else if (id === 'hard') { feat.hard = true; label = 'Harder: 2 hearts'; reply = 'Done. Version {v} starts you with 2 hearts instead of 3, so every miss counts.'; }
else if (r) { label = 'Re-tuned'; reply = 'That’s already in this version. I re-checked it against your notes and saved a fresh copy as version {v}.'; }
else { feat.note = true; label = 'Your note'; reply = `Got it. I saved your note in version {v} and applied what I could from it: “${esc(text)}”. If it isn’t right yet, tell me what to change.`; }
p.versions.push({ n: p.versions.length + 1, label, feat });
const v = p.versions.length - 1;
const m = { t: 'build', text: reply.replace('{v}', v + 1), steps: [[`Updating the ${p.kind}`, `Version ${v + 1}`], ['Re-checking answers against your notes', '']], at: 0, v };
p.log.push(m); stPaint(p);
await stWait(560); m.at = 1; stPaint(p); await stWait(560); m.at = 2;
p.busy = false; p.cur = v; stPaint(p); stPlay(p);
}
Object.assign(ACT, {
'st-ref': el => stRefineRun(SR.proj, el.dataset.ref, el.textContent.trim()),
'st-v': el => { const p = SR.proj; if (!p || p.busy) return; p.cur = +el.dataset.v; stPaint(p); stPlay(p); },
'change:st-ver': el => { const p = SR.proj; if (!p || p.busy) return; p.cur = +el.value; stPaint(p); stPlay(p); },
'st-restart': () => { if (SR.proj && !SR.proj.busy) stPlay(SR.proj); },
'st-report': () => toast('Thanks. Hoot will re-check this question and flag it for review.'),
'td-again': () => { if (TD.it) tdStart(TD.it, TD.host); },
'st-edit': () => { const p = SR.proj; ibReset(p.kind); if (p.idea) Object.assign(IB, p.idea); Object.assign(IB, { editing: p.editing, say: 'Change any word, then press Build it for a new version.' }); SR.proj = null; rerender(false); },
'st-publish': () => {
const p = SR.proj; if (!p || p.busy) return;
const course = p.src.course || 'your class';
openSheet('Publish to the Study Room', `<div class="sr-st"><form class="pub-form" data-form="st-pub" novalidate>
<div class="fld"><label for="pubName">Name</label><input class="inp" id="pubName" value="${esc(p.title)}" maxlength="48"></div>
<fieldset class="fld"><legend>Who can ${p.kind === 'game' ? 'play' : 'use'} it</legend><div class="vis">
<label><input type="radio" name="vis" value="me"><span class="vi">${icon('i-lock', 'sm')}</span><span><b>Only me</b><span class="s">Keep it in Made by you. You can share it later.</span></span></label>
<label><input type="radio" name="vis" value="class" checked><span class="vi">${icon('i-users', 'sm')}</span><span><b>My ${esc(course)} classmates</b><span class="s">Students in ${esc(course)} at your school.</span></span></label>
<label><input type="radio" name="vis" value="all"><span class="vi">${icon('i-globe', 'sm')}</span><span><b>Everyone on StudyBoost</b><span class="s">Listed in Made by students. Your notes stay private.</span></span></label></div></fieldset>
<div class="checks"><div class="checks-head">${hootSym(34)}<b>Hoot’s checks</b><span class="ok">All clear</span></div><ul>${[['Every answer traced to your notes', `${SR_SETS[p.setKey].qs.length} of ${SR_SETS[p.setKey].qs.length} questions link to a page`], ['No personal info found', 'No names, emails or student IDs in the content'], ['Plays well on phones', 'Tested at phone size'], ['Meets the community guidelines', 'Accurate, respectful and on topic']].map(c => `<li><span>${icon('i-check', 'sm')}</span><span>${c[0]}<small>${c[1]}</small></span></li>`).join('')}</ul><p class="note">${icon('i-flag')}Players can report a question. Hoot re-checks it and lets you know.</p></div>
<div class="sheet-foot"><button type="button" class="btn btn-outline" data-act="sheet-close">Cancel</button><button type="submit" class="btn btn-primary">${icon('i-globe', 'sm')}Publish</button></div></form></div>`, { scope: 'pg-study' });
}
});
FORMS['st-chat'] = f => {
const i = $('#chatInput', f), t = i.value.trim(); if (!t) return; i.value = '';
const k = /timer|time|second|clock/i.test(t) ? 'timer' : /eas|hint|simpl/i.test(t) ? 'easy' : /explain|why|wrong/i.test(t) ? 'explain' : /lecture|term|slide|notes/i.test(t) ? 'notes' : /hard|difficult|challeng/i.test(t) ? 'hard' : 'note';
stRefineRun(SR.proj, k, t);
};
FORMS['st-pub'] = f => {
const p = SR.proj, name = ($('#pubName', f).value || '').trim();
if (!name) { $('#pubName', f).focus(); toast('Give it a name so classmates can find it.'); return; }
const vis = $('input[name="vis"]:checked', f).value, par = p.remixOf && studyItem(p.remixOf);
const mech = Object.values(SR_MECH[p.kind]).find(x => x[0] === p.mech);
const e = { id: p.editing || 'm-' + Date.now().toString(36), kind: p.kind, name, mech: p.mech, set: p.setKey, course: p.src.course || (par ? par.course : 'StudyBoost'), st: vis === 'me' ? 'priv' : 'pub',
versions: p.versions.length, sentence: p.sentence, idea: p.idea, desc: mech ? mech[1] : (par ? par.desc : ''), docs: [p.src.id === 'lib' ? 'StudyBoost library' : p.src.label], from: p.remixOf || null, fresh: true };
const ex = S.made.find(m => m.id === e.id);
if (ex) Object.assign(ex, e); else S.made.unshift(Object.assign({ plays: 0, up: 0, down: 0, remixes: 0 }, e));
S.made.forEach(m => { if (m.id !== e.id) m.fresh = false; });
if (par && !ex) par.remixes++;
p.editing = e.id; save(); closeSheet(true);
toast(vis === 'me' ? `${name} is saved in Made by you` : `${name} is live in the Study Room`);
go(vis === 'me' ? 'study-mine' : 'game-' + e.id);
};
/* ===== 79-hoot.js ===== */
/* ================= Ask Hoot: the Documents hub button, with Study buddy's suggestions for the page you are on ================= */
window.HOOT = (() => {
const H = { open: false, busy: false, i: 0, timer: 0 };
const face = (cls) => `<svg class="hoot-f ${cls || ''}" viewBox="0 0 64 64" aria-hidden="true"><use href="#hoot64"/></svg>`;
function greet() { const h = new Date().getHours(); return h >= 23 || h < 5 ? ['Night owl? Me too.', 'I’m Mr. Hoot. What are we studying tonight?'] : h < 12 ? ['Good morning!', 'I’m Mr. Hoot. Ask me about your courses, or start with one of these.'] : h < 18 ? ['Good afternoon!', 'I’m Mr. Hoot. Ask me about your courses, or start with one of these.'] : ['Good evening!', 'I’m Mr. Hoot. Ask me about your courses, or start with one of these.']; }
function sugg(r) {
r = r || R;
if (r.page === 'shelf') { const s = shelfById(r.id) || {}; return [`Quiz me on ${s.course || 'this shelf'}`, `Make flashcards from ${s.name || 'this shelf'}`, 'Turn this shelf into a study plan']; }
if (r.page === 'course') return [`Quiz me on ${r.code}`, `What’s usually on the ${r.code} midterm?`, `Summarize the top ${r.code} study guide`];
if (r.page === 'doc') { const d = DOCS[r.id] || {}; return [`Summarize ${d.title || 'this document'}`, `Quiz me on this ${(d.type || 'document').toLowerCase()}`, 'Explain the hardest part simply']; }
if (r.page === 'docs' || r.page === 'campus') return ['Find BIL 150 notes at Miami', 'Which school near me has the most documents?', 'Look up a course code'];
if (r.page === 'study') return ['Make me a game from my notes', 'Which game helps me memorize terms?', 'Quiz me on stoichiometry'];
if (r.page === 'prefs') return ['How do I replay the tour?', 'Which theme is easiest on the eyes?'];
return S.persona === 'new' ? ['Find my school', 'How does StudyBoost work?', 'Quiz me on a topic'] : ['Plan my week for the BIL 150 midterm', 'Quiz me on osmosis and diffusion', 'What’s due this week?'];
}
const ANS = [
[/plan|week|due/i, () => { const t = allTasks().filter(x => x.st !== 'done' && x.day >= 0 && x.day < 7).sort((a, b) => a.day - b.day).slice(0, 3); return t.length ? `This week: ${t.map(x => `${x.t} (${dueTxt(x.day).toLowerCase()})`).join('; ')}. Want me to turn that into a day-by-day plan?` : 'Nothing is due this week. Want me to build a review plan for your next exam?'; }],
[/quiz|flashcard|game/i, () => 'Let’s do it. I’ll ask five questions from your notes, one at a time, and show the page each answer comes from. In the Study Room I can also turn this into a game you keep.'],
[/summar|explain|hardest/i, () => 'Here’s the short version: five key points, the terms in bold, and one example for the trickiest idea. With Pro I answer straight from the document you have open.'],
[/school|near|code|find/i, () => 'University of Miami has 1,300 documents across 175 courses, and BIL 150 is one of the busiest. Want me to open the course?'],
[/tour|replay/i, () => 'Open your account menu, then Preferences. Under Help and tutorials, press Replay the tour.'],
[/theme|eyes/i, () => 'Dark or Lo-fi Midnight are easiest at night. Pick one from the palette button in the top bar.']
];
const answer = q => { const hit = ANS.find(([re]) => re.test(q)); return hit ? hit[1]() : 'In the live app I’d answer that from your notes and documents. This prototype shows how I react.'; };
function launcher() {
const s = sugg();
return `<div class="hb" role="group" aria-label="Ask Hoot"><button type="button" class="hb-face" data-act="hoot-toggle" aria-label="Open the Ask Hoot chat" aria-expanded="${H.open}">${face()}</button><button type="button" class="hb-main" data-act="hoot-sugg"><span class="hb-k">Ask Hoot</span><span class="hb-q" id="hb-q">${esc(s[H.i % s.length])}</span>${icon('i-arrow', 'sm')}</button></div>`;
}
function panel() {
const g = greet();
return `<div class="hp" role="dialog" aria-label="Ask Hoot" id="hoot-panel"><div class="hp-h"><span class="hp-av">${face('reactive')}</span><div class="hp-n"><b>Mr. Hoot</b><span id="hp-sub">Your StudyBoost study buddy</span></div><button type="button" class="s-ib sm" data-act="hoot-close" aria-label="Close chat">${icon('i-x', 'sm')}</button></div>
<div class="hp-b" id="hp-b"><div class="hp-hello"><b>${g[0]}</b><p>${g[1]}</p><div class="hp-chips">${sugg().map(q => `<button type="button" data-act="hoot-ask" data-q="${esc(q)}">${esc(q)}</button>`).join('')}</div></div><div class="hp-thread" id="hp-thread" aria-live="polite"></div></div>
<div class="hp-tools">${[['Summarize', 'i-file'], ['Flashcards', 'i-cards'], ['Quiz me', 'i-quiz'], ['Study plan', 'i-calendar']].map(([t, ic]) => `<button type="button" data-act="hoot-ask" data-q="${t}">${icon(ic, 'sm')}${t}</button>`).join('')}</div>
<form class="hp-f" data-form="hoot"><label for="hp-in" class="sr">Ask Mr. Hoot</label><input id="hp-in" autocomplete="off" placeholder="Ask anything about your studies…"><button type="submit" class="s-ib sm" aria-label="Send">${icon('i-arrow-up', 'sm')}</button></form></div>`;
}
function paint() { const root = $('#hoot-root'); if (!root) return; root.innerHTML = launcher() + (H.open ? panel() : ''); }
async function send(q) {
q = (q || '').trim(); if (!q || H.busy) return; H.busy = true;
const th = $('#hp-thread'), b = $('#hp-b'), hello = $('.hp-hello'), sub = $('#hp-sub'), f = $('.hp .hoot-f.reactive');
if (hello) hello.hidden = true;
th.insertAdjacentHTML('beforeend', `<div class="hm me">${esc(q)}</div><div class="hm bot typing"><i></i><i></i><i></i></div>`); b.scrollTop = b.scrollHeight;
if (sub) sub.textContent = 'Hoot is thinking…'; if (f) f.classList.add('think');
await new Promise(r => setTimeout(r, reduceMotion() ? 50 : 1000));
const ty = $('.hm.typing', th); if (ty) ty.remove();
th.insertAdjacentHTML('beforeend', '<div class="hm bot"></div>'); const out = th.lastElementChild;
if (sub) sub.textContent = 'Answering…';
const words = answer(q).split(' ');
for (let i = 0; i < words.length; i++) { if (!document.contains(out)) break; out.textContent += (i ? ' ' : '') + words[i]; b.scrollTop = b.scrollHeight; if (!reduceMotion()) await new Promise(r => setTimeout(r, 34)); }
if (sub) sub.textContent = 'Your StudyBoost study buddy'; if (f) f.classList.remove('think');
H.busy = false;
}
function open(q) { H.open = true; paint(); const i = $('#hp-in'); if (q) send(q); else if (i) i.focus(); }
function close() { H.open = false; H.busy = false; paint(); const b = $('.hb-face'); if (b) b.focus(); }
Object.assign(ACT, {
'hoot-toggle': () => H.open ? close() : open(),
'hoot-close': () => close(),
'hoot-ask': el => send(el.dataset.q),
'hoot-sugg': () => { const s = sugg(); open(s[H.i % s.length]); }
});
FORMS['hoot'] = f => { const i = $('#hp-in', f), v = i.value; i.value = ''; send(v); };
H.timer = setInterval(() => { if (H.open || document.hidden || reduceMotion()) return; const q = $('#hb-q'); if (!q || q.matches(':hover')) return; H.i++; const s = sugg(); q.classList.add('out'); setTimeout(() => { q.textContent = s[H.i % s.length]; q.classList.remove('out'); }, 220); }, 3800);
return { open, close, isOpen: () => H.open, route: () => { H.i = 0; if (!H.open) paint(); } };
})();
/* ===== 80-onboard.js ===== */
/* ================= First-run tour: the Welcome card (Hoot, then the three tabs). Replay lives in Preferences. ================= */
window.TOUR = (() => {
const P = [
{ ic: 'hoot', eyebrow: 'Welcome to StudyBoost', title: 'Hi, I’m Mr. Hoot.', body: 'StudyBoost is built around the three tabs in the sidebar. Here is what each one is for. It takes about a minute, and you can replay it later from Preferences.', chips: [] },
{ ic: 'i-home', eyebrow: 'My Room', title: 'Your desk for the semester', body: 'Keep a shelf for each class, pin quick notes and see what’s due. You start with a Getting started shelf you can edit, so the quickest way to learn it is to change it.', chips: ['Shelves', 'Pinboard', 'This week', 'Board, table and list views'] },
{ ic: 'i-search', eyebrow: 'Documents', title: 'A library of 2 million study guides', body: 'Notes, study guides and past exams shared by students at 27,000 schools in 200 countries. Find your school, then your course.', chips: ['Search by school', 'Browse by place', 'Save to a shelf'] },
{ ic: 'i-gamepad', eyebrow: 'Study Room', title: 'Games and tools for practice', body: 'Play games that turn your notes into quick recall, or describe one in a sentence and Hoot builds it for you.', chips: ['Make your own', 'AI Courses', 'Flashcards', 'Remix a game'] }];
let i = 0;
function art(p) {
if (p.ic === 'hoot') return `<div class="wc-hoot"><svg viewBox="0 0 64 64" aria-hidden="true"><use href="#hoot64"/></svg><div class="wc-plate"><b>Mr. Hoot</b><span>Your guide at StudyBoost</span></div></div>`;
return `<div class="wc-ic">${icon(p.ic, 'lg')}<b>${p.eyebrow}</b></div>`;
}
function paint() {
const p = P[i], card = $('.wc'); if (!card) return;
card.innerHTML = `<div class="wc-art">${art(p)}</div><div class="wc-main"><div class="wc-top"><nav class="wc-steps" aria-label="Tour steps">${P.map((x, k) => `<button type="button" data-act="tour-go" data-i="${k}" aria-current="${k === i ? 'step' : 'false'}" class="${k < i ? 'done' : ''}">${k === 0 ? 'Welcome' : x.eyebrow}</button>`).join('')}</nav><button type="button" class="s-ib sm" data-act="tour-end" aria-label="Close tour">${icon('i-x', 'sm')}</button></div>
<div class="wc-c"><p class="wc-eb">${p.eyebrow}</p><h2 id="wc-t">${p.title}</h2><p>${p.body}</p>${p.chips.length ? `<div class="wc-chips">${p.chips.map(c => `<span>${c}</span>`).join('')}</div>` : ''}</div>
${i === P.length - 1 ? `<p class="wc-note">${icon('i-sliders', 'sm')}You can replay this anytime from Preferences, under Help and tutorials.</p>` : ''}
<div class="wc-f">${i < P.length - 1 ? '<button type="button" class="s-btn quiet" data-act="tour-end">Skip tour</button>' : ''}<span style="flex:1"></span>${i > 0 ? '<button type="button" class="s-btn" data-act="tour-go" data-i="' + (i - 1) + '">Back</button>' : ''}<button type="button" class="s-btn primary" data-act="${i === P.length - 1 ? 'tour-end' : 'tour-go'}" data-i="${i + 1}">${i === P.length - 1 ? 'Start in My Room' : i === 0 ? 'Show me the tabs' : 'Next'}${icon('i-arrow', 'sm')}</button></div></div>`;
const f = $('.wc-f .primary'); if (f) f.focus({ preventScroll: true });
}
function start() { i = 0; openModal('<div class="wc"></div>', { scope: '', cls: 'wc-card', label: 'Welcome to StudyBoost', onClose: () => finish(true) }); paint(); }
function finish(fromClose) {
if (S.onboarded && fromClose) return;
S.onboarded = true; save();
if (!fromClose) closeModal(true);
if (R.page !== 'room') go('room');
toast('Tour saved to Preferences. Replay it anytime from your account menu.', { label: 'Show me', run: () => go('preferences') });
}
Object.assign(ACT, { 'tour-go': el => { i = Math.max(0, Math.min(P.length - 1, +el.dataset.i)); paint(); }, 'tour-end': () => finish(false) });
return { start };
})();
/* ===== 81-prefs.js ===== */
/* ================= Preferences: profile, appearance, plan, school, and Help and tutorials (the tour replay) ================= */
PAGES.prefs = {
cls: () => 'pg-prefs',
render() {
const s = S.mine && BY_ID[S.mine], left = unlocksLeft(), isNew = S.persona === 'new';
const row = (t, d, act) => `<div class="pf-row"><div class="pf-t"><b>${t}</b><span>${d}</span></div>${act || ''}</div>`;
return `<div class="page-in">${crumbs([['preferences', 'Preferences']])}<div class="pf"><h1>Preferences</h1>
<section class="pf-sec"><h2>Profile</h2>${row(isNew ? 'You' : PERSONA.full, isNew ? 'Signed in · add your school to get started' : `${esc(PERSONA.email)}${s ? ' · ' + esc(s.name) : ''}`, `<span class="s-av">${isNew ? 'YOU' : PERSONA.initials}</span>`)}</section>
<section class="pf-sec"><h2>Appearance</h2><p class="pf-d">Match your device, or pick a theme. Pro themes are free to preview.</p>${themeGrid()}
<div class="pf-row"><div class="pf-t"><b>Ambient motion</b><span>Slow background movement in Cloud Nine and Lo-fi Midnight</span></div><button type="button" role="switch" aria-checked="${S.motion}" data-act="motion" aria-label="Ambient motion"><span class="s-switch"><i></i></span></button></div></section>
<section class="pf-sec"><h2>Plan</h2>${row(S.plan === 'pro' ? 'Pro' : 'Free plan', S.plan === 'pro' ? 'Every premium document, Ask Hoot, AI Courses and the Pro themes' : `${left} of ${UNLOCKS} free unlocks left this month · they reset ${resetDay()}`, S.plan === 'pro' ? '' : '<button type="button" class="s-btn sm primary" data-act="pro">Upgrade</button>')}</section>
<section class="pf-sec"><h2>Your school</h2>${row(s ? esc(s.name) : 'No school yet', s ? `${fmt(s.docs)} documents · ${fmt(s.courses)} courses` : 'Find it in Documents and we’ll tailor search and Ask Hoot to it', `<a class="s-btn sm" href="#${s ? 'school-' + s.id : 'documents'}">${s ? 'Open Campus Hub' : 'Find my school'}</a>`)}</section>
<section class="pf-sec" id="help"><h2>Help and tutorials</h2>${row('Replay the tour', 'Mr. Hoot walks you through My Room, Documents and the Study Room again', '<button type="button" class="s-btn sm" data-act="tour">' + icon('i-play', 'sm') + 'Replay the tour</button>')}${row('Ask Hoot', 'Questions about StudyBoost or your courses', '<button type="button" class="s-btn sm" data-act="hoot-open" data-q="How does StudyBoost work?">Ask Hoot</button>')}</section>
<section class="pf-sec"><h2>Prototype</h2>${row('View as', isNew ? 'A first-time visitor' : 'Maya, a returning student', `<div class="pf-seg"><button type="button" class="s-btn sm${isNew ? '' : ' primary'}" data-act="persona" data-v="returning">Returning</button><button type="button" class="s-btn sm${isNew ? ' primary' : ''}" data-act="persona" data-v="new">New</button></div>`)}${row('Reset the demo', 'Clears everything you changed in this prototype', '<button type="button" class="s-btn sm" data-act="reset-demo">Reset</button>')}</section>
</div></div>`;
}
};
/* ===== 90-boot.js ===== */
/* ================= boot ================= */
loadState();
applyTheme(S.theme);
(() => {
const t = (location.hash || '').slice(1);
const r = parseToken(t);
TOKEN = r ? (t || 'room') : 'room'; R = r || { page: 'room' };
try { history.replaceState(null, '', '#' + TOKEN); } catch (e) { /* ignore */ }
addRecentFor(R);
render();
})();
</script>
</body></html>