Sign-in page rewritten for non-technical students: Google-first when configured, plain-language email path with honest 30-day scope, recovery note, placeholders, full-width CTA. deploy/termux-start.sh sources .env for GOOGLE_* credentials; deploy/env.example documents the 5-minute Cloud Console setup; .gitignore covers secrets, DBs, pid/log files. bench scripts send a browser UA so Cloudflare bot checks stop 403ing local load tests. Landing bank counts cached 60s (five aggregate subselects no longer run per hit). gzip compression on all responses.
240 lines
14 KiB
CSS
240 lines
14 KiB
CSS
*{box-sizing:border-box;margin:0;padding:0;border-radius:0!important}
|
|
:root{--bg:#0b0b0b;--panel:#111111;--card:#141414;--input:#0e0e0e;--border:#2e2e2e;--border2:#3d3d3d;--edge:#626262;--txt:#e5e5e5;--body:#cccccc;--muted:#9a9a9a;--dim:#7d7d7d;--ok:#10b981;--cyan:#38bdf8;--amber:#f59e0b;--bad:#ff5555;--mono:'JetBrains Mono',ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
|
|
html{background:var(--bg)}
|
|
body{background:var(--bg);color:var(--body);font-family:var(--mono);font-size:14px;line-height:1.55;max-width:1180px;margin:0 auto;padding:0 16px 64px}
|
|
a{color:var(--cyan);text-decoration:none}
|
|
a:hover{text-decoration:underline}
|
|
h1{font-size:2.1rem;font-weight:600;letter-spacing:-.04em;color:#fff;line-height:1.15;margin:18px 0 10px}
|
|
h2{font-size:.76rem;font-weight:500;text-transform:uppercase;letter-spacing:.12em;color:var(--muted);margin:28px 0 12px;border-bottom:1px solid var(--border);padding-bottom:8px}
|
|
h3{color:#fff;font-size:1rem;margin:0 0 6px}
|
|
.topbar{display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid var(--border);padding:12px 0;position:sticky;top:0;background:rgba(11,11,11,.92);z-index:5}
|
|
.brand{color:#fff;font-weight:600;font-size:1rem;letter-spacing:-.02em}
|
|
.brand span{color:var(--dim);font-weight:400;font-size:.78rem;margin-left:10px}
|
|
.nav{display:flex;gap:14px;align-items:center;font-size:.8rem}
|
|
.btn{display:inline-flex;align-items:center;justify-content:center;padding:8px 18px;background:#161616;color:#ddd;border:1px solid var(--edge);font-size:.8rem;font-family:inherit;cursor:pointer;letter-spacing:.02em;transition:all .15s ease;text-decoration:none}
|
|
.btn:hover:not(:disabled){background:#242424;border-color:#555;color:#fff;text-decoration:none}
|
|
.btn:active:not(:disabled){background:#fff;color:#000;border-color:#fff}
|
|
.btn:disabled{opacity:.3;cursor:not-allowed}
|
|
.btn.primary{background:var(--cyan);border-color:var(--cyan);color:#001018;font-weight:600}
|
|
.btn.primary:hover{background:#7cd4fc;color:#000}
|
|
.btn.ok{border-color:var(--ok);color:var(--ok)}
|
|
.btn.danger{border-color:var(--bad);color:var(--bad)}
|
|
.grid2{display:grid;grid-template-columns:1fr 1fr;gap:14px}
|
|
.grid3{display:grid;grid-template-columns:repeat(3,1fr);gap:14px}
|
|
.card{background:var(--card);border:1px solid var(--border);padding:16px}
|
|
.card:hover{border-color:var(--border2)}
|
|
.meta{color:var(--muted);font-size:.78rem}
|
|
.dim{color:var(--dim);font-size:.78rem}
|
|
table{width:100%;border-collapse:collapse;font-size:.82rem}
|
|
th{color:var(--dim);text-transform:uppercase;letter-spacing:.1em;font-size:.68rem;text-align:left;padding:8px;border-bottom:1px solid var(--border)}
|
|
td{padding:8px;border-bottom:1px solid #1c1c1c;font-variant-numeric:tabular-nums}
|
|
input[type=text],input[type=email],input[type=number],select,textarea{background:var(--input);color:#e5e5e5;border:1px solid var(--edge);padding:9px 12px;font-family:inherit;font-size:.82rem;outline:none;width:100%}
|
|
input:focus,textarea:focus,select:focus{border-color:var(--cyan)}
|
|
input::placeholder{color:var(--dim)}
|
|
label{display:block;font-size:.75rem;color:var(--muted);margin:10px 0 4px;text-transform:uppercase;letter-spacing:.08em}
|
|
.chk{display:flex;gap:10px;align-items:flex-start;border:1px solid var(--edge);padding:10px 12px;margin:6px 0;background:var(--panel);cursor:pointer;font-size:.85rem}
|
|
.chk:hover{border-color:var(--border2)}
|
|
.chk input{width:auto;margin-top:3px;accent-color:var(--cyan)}
|
|
.palette{display:grid;grid-template-columns:repeat(5,1fr);gap:6px}
|
|
.pal{aspect-ratio:1;display:grid;place-items:center;border:1px solid var(--edge);background:var(--panel);color:var(--muted);font-size:.75rem;cursor:pointer;min-height:44px}
|
|
.pal.cur{border-color:var(--cyan);color:#fff}
|
|
.pal.ans{background:#12301f;border-color:var(--ok);color:var(--ok)}
|
|
.pal.rev{background:#2a1f05;border-color:var(--amber);color:var(--amber)}
|
|
.pal.skip{background:var(--panel)}
|
|
.opt{display:flex;gap:10px;align-items:flex-start;border:1px solid var(--edge);padding:10px 12px;margin:8px 0;background:var(--panel);cursor:pointer}
|
|
.opt:hover{border-color:var(--border2)}
|
|
.opt.sel{border-color:var(--cyan);background:#0d1b24}
|
|
.status-dot{width:6px;height:6px;background:#444;display:inline-block;flex:none}
|
|
.ok-t{color:var(--ok)}.bad-t{color:var(--bad)}.warn-t{color:var(--amber)}.cyan-t{color:var(--cyan)}
|
|
.bar{height:2px;background:#1c1c1c;overflow:hidden;margin-top:8px}
|
|
.bar i{display:block;height:100%;background:var(--ok)}
|
|
.seg{display:flex;height:10px;background:#1c1c1c;overflow:hidden}
|
|
.seg i{display:block;height:100%}
|
|
:focus-visible{outline:2px solid var(--cyan);outline-offset:2px}
|
|
.cbt{display:grid;grid-template-columns:1fr 220px;gap:14px}
|
|
.timer{font-variant-numeric:tabular-nums;color:var(--amber);font-weight:600}
|
|
.kv{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin:14px 0}
|
|
.kv .card b{display:block;font-size:1.5rem;color:#fff;font-variant-numeric:tabular-nums}
|
|
footer{margin-top:48px;border-top:1px solid var(--border);padding-top:16px;color:var(--dim);font-size:.75rem;display:flex;justify-content:space-between;flex-wrap:wrap;gap:8px}
|
|
/* Tablet: the CBT palette drops under the question instead of squeezing it. */
|
|
@media(max-width:1024px){.cbt{grid-template-columns:1fr}.palette{grid-template-columns:repeat(8,1fr)}}
|
|
/* Phone: one column throughout, a 2x2 metric grid and full-width palette taps. */
|
|
@media(max-width:700px){.grid2,.grid3{grid-template-columns:1fr}.kv{grid-template-columns:1fr 1fr}h1{font-size:1.6rem}.nav a.hidem{display:none}.palette{grid-template-columns:repeat(5,1fr)}.cbt{gap:10px}.topbar{padding:10px 0}.brand span{display:none}}
|
|
@media(max-width:420px){.kv{grid-template-columns:1fr}.palette{grid-template-columns:repeat(4,1fr)}.brand{font-size:.9rem}.btn{padding:10px 14px;min-height:44px}}
|
|
@media(hover:none){.pal{min-height:48px}}
|
|
|
|
/* ===== exam screen =====
|
|
The clock and the palette are the two things a candidate must never lose,
|
|
so both are pinned rather than left to scroll. */
|
|
.exam-bar{position:sticky;top:0;z-index:6;background:var(--bg);border-bottom:1px solid var(--border);padding:8px 0 0;margin:0 0 12px}
|
|
.exam-bar-inner{display:flex;align-items:center;justify-content:space-between;gap:10px;min-height:44px}
|
|
.exam-title{display:flex;flex-direction:column;min-width:0}
|
|
.exam-title b{color:#fff;font-size:.95rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
.exam-clock{text-align:right;flex:none}
|
|
.exam-progress{height:3px;background:#1c1c1c;margin-top:8px}
|
|
.exam-progress i{display:block;height:100%;background:var(--cyan);width:0;transition:width .2s ease}
|
|
.exam-bar.complete .exam-progress i{background:var(--ok)}
|
|
.exam-bar .timer{font-size:1.15rem;line-height:1.1}
|
|
.timer.urgent{color:var(--amber)}
|
|
.timer.critical{color:var(--bad)}
|
|
|
|
.cbt-side h2{margin-top:0}
|
|
.legend{list-style:none;display:flex;flex-wrap:wrap;gap:10px;margin:10px 0;font-size:.7rem;color:var(--dim)}
|
|
.legend li{display:flex;align-items:center;gap:5px}
|
|
.legend .sw{width:9px;height:9px;display:inline-block;border:1px solid var(--edge)}
|
|
.legend .sw.ans{background:#12301f;border-color:var(--ok)}
|
|
.legend .sw.rev{background:#2a1f05;border-color:var(--amber)}
|
|
.legend .sw.skip{background:var(--panel)}
|
|
.cbt-count{margin:0 0 10px;font-variant-numeric:tabular-nums}
|
|
.btn.block{display:block;width:100%;margin-top:8px}
|
|
.cbt-side [data-close-palette]{display:none}
|
|
button.pal-fab{display:none}
|
|
.pane-head{display:flex;justify-content:space-between;align-items:baseline;gap:10px;flex-wrap:wrap;font-size:.78rem}
|
|
.stem{margin:10px 0 6px;color:#fff;font-size:1rem;line-height:1.5}
|
|
.meta-line{margin-bottom:10px}
|
|
.opt-actions{display:flex;gap:8px;flex-wrap:wrap;margin-top:14px}
|
|
.opt-actions .btn{flex:1 1 auto;min-width:120px}
|
|
|
|
/* The palette is a drawer under the question: it was a wall of tiles that pushed
|
|
the submit button off a phone screen. */
|
|
@media(max-width:1024px){
|
|
/* A translated-off drawer is still in the tab order, so it is also hidden from
|
|
assistive tech and hit testing while closed. */
|
|
.cbt-side{position:fixed;left:0;right:0;bottom:0;background:var(--panel);border-top:1px solid var(--edge);padding:14px 16px calc(68px + env(safe-area-inset-bottom));max-height:78vh;overflow-y:auto;transform:translateY(100%);visibility:hidden;transition:transform .2s ease,visibility .2s;z-index:31}
|
|
.cbt-side.open{transform:translateY(0);visibility:visible}
|
|
.cbt-side h2{margin-top:0}
|
|
/* The button that opens the drawer cannot live inside the drawer, so it floats
|
|
above the tab bar where a thumb reaches it while reading a question. */
|
|
/* It sits above the tab bar, so the page reserves that strip; without it the
|
|
button lands on top of the last line of text. */
|
|
button.pal-fab{
|
|
display:flex;position:fixed;right:14px;z-index:25;
|
|
bottom:calc(60px + env(safe-area-inset-bottom));
|
|
margin:0;min-height:48px;padding:12px 16px;
|
|
background:var(--panel);border:1px solid var(--cyan);color:var(--cyan);
|
|
}
|
|
button.pal-fab[aria-expanded=true]{
|
|
/* While the drawer is open the button is redundant, so it leaves the
|
|
submit row entirely rather than covering it. */
|
|
display:none;
|
|
}
|
|
/* The drawer keeps its own close control at the top. */
|
|
.cbt-side [data-close-palette]{display:block}
|
|
.palette{grid-template-columns:repeat(6,1fr)}
|
|
/* Room for the drawer so the last option row is never hidden behind it. */
|
|
.cbt{padding-bottom:76px}
|
|
}
|
|
|
|
/* Phone: a smaller type step, tighter gaps, and touch-sized controls throughout. */
|
|
@media(max-width:700px){
|
|
body{font-size:15px;padding:0 14px 72px}
|
|
h1{font-size:1.55rem}
|
|
h2{font-size:.7rem;margin:22px 0 10px}
|
|
.exam-bar{padding-top:6px}
|
|
.exam-title b{font-size:.85rem}
|
|
.exam-bar .timer{font-size:1.35rem}
|
|
.pane-head{font-size:.75rem}
|
|
.stem{font-size:1.02rem;line-height:1.55}
|
|
.opt-actions .btn{min-width:0;flex:1 1 44%;padding:12px 10px;min-height:48px}
|
|
.palette{grid-template-columns:repeat(5,1fr);gap:8px}
|
|
.pal{min-height:48px;font-size:.8rem}
|
|
.kv .card b{font-size:1.3rem}
|
|
/* Every control on the exam screen gets a thumb-sized target. */
|
|
.btn{padding:11px 14px;min-height:44px}
|
|
.opt{padding:12px 12px;min-height:48px}
|
|
table{font-size:.78rem}
|
|
/* A data table cannot shrink to a phone, so it scrolls inside its own box
|
|
instead of pushing the whole page sideways. */
|
|
.table-wrap{overflow-x:auto;-webkit-overflow-scrolling:touch;margin:0 -14px;padding:0 14px}
|
|
}
|
|
|
|
@media(max-width:420px){
|
|
.palette{grid-template-columns:repeat(4,1fr)}
|
|
.opt-actions .btn{flex:1 1 100%}
|
|
}
|
|
|
|
/* Tablet: the palette drops under the question, so it needs the same drawer
|
|
treatment as a phone but with room for a wider grid. */
|
|
@media(max-width:1024px) and (min-width:701px){
|
|
.cbt{padding-bottom:0}
|
|
.cbt-side{max-height:70vh}
|
|
}
|
|
|
|
/* ===== phone navigation =====
|
|
The desktop link row has nowhere to go under 700px. Rather than hiding it,
|
|
the primary destinations move to a fixed bottom bar within thumb reach. */
|
|
.tabbar{display:none}
|
|
@media(max-width:700px){
|
|
.tabbar{
|
|
display:grid;grid-auto-flow:column;grid-auto-columns:1fr;
|
|
position:fixed;left:0;right:0;bottom:0;z-index:30;
|
|
background:var(--panel);border-top:1px solid var(--edge);
|
|
padding-bottom:env(safe-area-inset-bottom);
|
|
}
|
|
.tabbar a{
|
|
display:flex;align-items:center;justify-content:center;text-align:center;
|
|
min-height:52px;padding:6px 4px;font-size:.72rem;color:var(--muted);
|
|
border-right:1px solid var(--border);text-decoration:none;line-height:1.3;
|
|
}
|
|
.tabbar a:last-child{border-right:0}
|
|
.tabbar a:active{background:#1c1c1c;color:#fff}
|
|
.tabbar a:hover{text-decoration:none;color:#fff}
|
|
/* The bar is fixed, so the page reserves its height or the last row sits under it. */
|
|
body{padding-bottom:calc(72px + env(safe-area-inset-bottom))}
|
|
/* Clear the floating palette button in the exam view only. */
|
|
body.exam{padding-bottom:calc(122px + env(safe-area-inset-bottom))}
|
|
footer{margin-top:32px}
|
|
}
|
|
@media(min-width:701px){body{padding-bottom:64px}}
|
|
|
|
/* ===== tap targets =====
|
|
Measured at 35px on a pointer device and 13px on the chapter checkboxes,
|
|
both below the 44px minimum. Padding fixes the hit area without making the
|
|
control look heavy. */
|
|
.btn,a.btn{min-height:44px;padding:12px 18px;display:inline-flex;align-items:center;justify-content:center}
|
|
button.btn{line-height:1.2}
|
|
input[type=text],input[type=email],input[type=number],select{min-height:44px;padding:12px}
|
|
/* The native checkbox is small by design, so the label around it carries the target. */
|
|
.chk{min-height:44px}
|
|
.chk input[type=checkbox]{width:20px;height:20px;margin-top:2px;flex:none}
|
|
.opt{min-height:44px}
|
|
.opt input[type=radio]{width:20px;height:20px;flex:none}
|
|
|
|
/* ===== builder: subtopic picker =====
|
|
Chapters are the primary unit; subtopics expand inside them. */
|
|
.subject-section{margin-bottom:20px}
|
|
.subject-label{font-size:.7rem;letter-spacing:.08em;text-transform:uppercase;margin:0 0 8px}
|
|
.chapter-group{margin-bottom:8px}
|
|
.chapter-hd{display:flex;align-items:center;min-height:44px;background:var(--panel);padding:10px 12px;border:1px solid var(--border)}
|
|
.chapter-hd b{color:#fff}
|
|
.subtopic-list{padding:4px 12px 4px 32px;border:1px solid var(--border);border-top:0;display:none}
|
|
.chapter-group.expanded .subtopic-list{display:block}
|
|
.sub-chk{min-height:38px;padding:4px 0}
|
|
.settings-sticky{position:sticky;top:72px}
|
|
.picker-col,.settings-col{min-width:0}
|
|
.build-note{font-variant-numeric:tabular-nums}
|
|
.all-subj input[type=checkbox]{accent-color:var(--cyan)}
|
|
@media(max-width:700px){
|
|
.settings-sticky{position:static}
|
|
.chapter-hd{background:none;padding:10px 0}
|
|
.subtopic-list{padding:4px 0 4px 20px}
|
|
}
|
|
|
|
/* ===== per-subject timers on exam screen =====
|
|
Each strip shows for its subject window and hides when that window closes. */
|
|
.subj-timer-bar{display:flex;gap:12px;flex-wrap:wrap;margin-bottom:8px}
|
|
.subj-timer{display:flex;align-items:center;gap:8px;padding:6px 12px;background:var(--panel);border:1px solid var(--border);font-size:.8rem}
|
|
.subj-timer b{color:#fff;min-width:5ch;font-variant-numeric:tabular-nums}
|
|
.subj-timer.active{border-color:var(--cyan)}
|
|
.subj-timer.warn{border-color:var(--amber)}
|
|
.subj-timer.done{opacity:.4;text-decoration:line-through}
|
|
|
|
/* ===== mistake tagging ===== */
|
|
.mistake-row{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-top:10px;padding-top:10px;border-top:1px solid var(--border)}
|
|
.btn.sm{min-height:36px;padding:7px 12px;font-size:.72rem}
|
|
|
|
/* ===== sign-in ===== */
|
|
.signin-primary{max-width:420px}
|
|
.signin-or{display:flex;align-items:center;gap:12px;max-width:420px;margin:18px 0 4px;color:var(--dim);font-size:.75rem}
|
|
.signin-or::before,.signin-or::after{content:"";flex:1;height:1px;background:var(--border)}
|
|
.signin-or span{padding:0 2px}
|