6a4c972c47
- A quiz question can now carry the diagram it refers to: a Markdown image line anywhere inside the question block is parsed out of the text and rendered under it (referrerpolicy="no-referrer", lazy). The URL points at the course provider, so an offline install gets a note and a link instead of an empty box. - New optional SESSION_LIFETIME (30 days in the example config) replaces the browser-session cookie plus php.ini's 24-minute idle limit, which signed people out over a long pause. Configs that predate it behave as before. Behind a proxy trusted enough for CLIENT_IP_HEADER, the cookie is also marked Secure from X-Forwarded-Proto — TLS ending at the proxy no longer costs the flag. - Error responses no longer echo the exception message unless DEBUG: a PDOException names the DB user, host and tables, and that handler also runs for visitors who never signed in. - The .htaccess denials are anchored to a path segment rather than the root, so videos/ and includes/ stay unreachable under a prefix such as /coursehub/ instead of silently opening up. - A rescan takes a MySQL advisory lock; a second one answers "already running" rather than racing the first and flagging its rows missing. A file swapped in at the same path with a different size drops its cached duration so the probe reads the new length. - stream.php discards output buffering and zlib compression (both break the promised Content-Length) and answers HEAD with headers alone. - A progress save with no duration keeps the length already stored instead of nulling it, and the folder view builds its rollup once. - In the SPA, a view whose data arrives after the route moved on is discarded — previously a slow load could paint over a newer screen or start a heartbeat for a video nobody was watching. Also: the quiz submit button can't be double-fired, leaving a video during the autoplay countdown no longer jumps ahead later, the final position is always saved on 'ended', the playlist scrolls the current entry into its own box, the login field takes focus, views start at the top, and a stray "%" in the hash no longer throws. - Dropped the unused requireLogin(); registerUser() answers "taken" instead of a 500 when two people race for the same name. Claude-Session: https://claude.ai/code/session_01AXMrtWNUV7NU1pCUHBG4t1
1201 lines
36 KiB
CSS
1201 lines
36 KiB
CSS
/* CourseHub — "Course Library" design system.
|
|
* Light surface, near-black ink and chrome, orange primary accent,
|
|
* green success, red danger — all tuned to clear WCAG AA (4.5:1) as both
|
|
* text-on-white and white-text-on-fill, since each is used both ways.
|
|
* Tokens first, then layout, then components. No CSS framework underneath.
|
|
*/
|
|
|
|
:root {
|
|
/* Ground and surfaces — light and clean */
|
|
--paper: #F2F0EA;
|
|
--surface: #FFFFFF;
|
|
--card: #FFFFFF;
|
|
|
|
/* Dark chrome — sidebar + mobile tab bar only; the rest of the app stays light */
|
|
--chrome-bg: #14110D;
|
|
--chrome-bg-2: #201A13;
|
|
--chrome-line: #322B21;
|
|
--chrome-ink: #F7F5F1;
|
|
--chrome-ink-dim: #A79E8F;
|
|
--chrome-hover: #221C15;
|
|
/* Brighter orange for accent TEXT directly on dark chrome (mobile active
|
|
tab) — --accent itself is tuned for use on white and is too dark there. */
|
|
--chrome-accent: #FB923C;
|
|
|
|
/* Lines, from strongest to faintest */
|
|
--line-strong: #C7C0B5;
|
|
--line: #E1DCD2;
|
|
--line-soft: #EEEAE3;
|
|
|
|
/* Ink — near-black, each step still clears 4.5:1 on white */
|
|
--ink: #100F0D;
|
|
--ink-2: #3A3630;
|
|
--ink-3: #5C564D;
|
|
--ink-4: #756D5D;
|
|
|
|
/* Accent — orange, primary actions/links/selection/in-progress */
|
|
--accent: #C2410C;
|
|
--accent-hover: #9A3209;
|
|
--accent-soft: #FEF0E4;
|
|
--accent-tint: #FDF0E3;
|
|
--accent-line: #EFAE79;
|
|
|
|
/* Success — green, completed/correct/passed */
|
|
--success: #15803D;
|
|
--success-hover: #116430;
|
|
--success-soft: #E9F7EC;
|
|
--success-tint: #EBF7EE;
|
|
--success-line: #9AD6AC;
|
|
|
|
/* Danger — red, errors/reset/wrong */
|
|
--danger: #C7222A;
|
|
--danger-hover: #A11C22;
|
|
--danger-soft: #FCE6E5;
|
|
--danger-line: #F0AAAA;
|
|
|
|
/* Hovers */
|
|
--hover: #E9E4DA;
|
|
--row-hover: #F5F2EC;
|
|
|
|
/* Course colours — one is picked per course from its id. Darkened from the
|
|
original mutes so white overlay text on a thumbnail clears 4.5:1. */
|
|
--clay: #B85A2E;
|
|
--sage: #3F7A50;
|
|
--slate: #374B5C;
|
|
--sand: #96691C;
|
|
--plum: #7A4560;
|
|
--sky: #2E6A8C;
|
|
|
|
--shadow-card: 0 1px 2px rgba(16, 15, 13, .08);
|
|
--shadow-frame: 0 4px 16px rgba(16, 15, 13, .12);
|
|
--shadow-pop: 0 8px 24px rgba(16, 15, 13, .28);
|
|
|
|
/* One typeface everywhere — headings, body and numerals/paths all render
|
|
in Inter. --font-display/--font-mono are kept as separate variables (not
|
|
collapsed into --font-ui) so weight/size rules that reference them stay
|
|
meaningful call sites, even though all three resolve to the same family. */
|
|
--font-ui: 'Inter', system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
--font-display: 'Inter', system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
--font-mono: 'Inter', system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
|
|
--plyr-color-main: var(--accent);
|
|
--plyr-video-background: #100F0D;
|
|
}
|
|
|
|
*, *::before, *::after { box-sizing: border-box; }
|
|
|
|
/* Layout classes below set `display`, which would otherwise beat the UA's
|
|
`[hidden] { display: none }`. */
|
|
[hidden] { display: none !important; }
|
|
|
|
html, body { height: 100%; }
|
|
|
|
body {
|
|
margin: 0;
|
|
background: var(--paper);
|
|
color: var(--ink);
|
|
font-family: var(--font-ui);
|
|
font-size: 16px;
|
|
}
|
|
|
|
h1, h2, h3 { font-family: var(--font-display); font-weight: 700; letter-spacing: -0.015em; margin: 0; }
|
|
h1 { font-size: 36px; line-height: 1.1; }
|
|
h2 { font-size: 24px; line-height: 1.2; letter-spacing: -0.01em; }
|
|
h3 { font-family: var(--font-ui); font-size: 17px; line-height: 1.3; letter-spacing: 0; }
|
|
p { margin: 0; }
|
|
|
|
a { color: var(--accent); text-decoration-color: var(--accent-line); text-underline-offset: 2px; }
|
|
a:hover { color: var(--accent-hover); }
|
|
|
|
button, input { font-family: inherit; font-size: inherit; }
|
|
|
|
.mono { font-family: var(--font-mono); font-weight: 500; }
|
|
|
|
/* Small text utilities, so views don't hand-roll font sizes inline and drift
|
|
out of the scale above. */
|
|
.mono-sm { font: 500 14px/1 var(--font-mono); color: var(--ink-3); }
|
|
.hint-inline { font: 500 15px/1.4 var(--font-ui); color: var(--ink-3); }
|
|
|
|
/* --font-mono no longer points at an actual monospace face (everything is
|
|
Inter now), so durations/counts/paths would drift out of alignment as
|
|
digits change width — tabular-nums keeps every digit the same width
|
|
without needing a separate font. */
|
|
.mono, .mono-sm, .shelf-count, .thumb-dur, .row-num, .q-num, .path-row .path {
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
.h-section { font-size: 22px; }
|
|
|
|
::-webkit-scrollbar { width: 10px; height: 10px; }
|
|
::-webkit-scrollbar-thumb { background: var(--line-strong); border-radius: 999px; border: 3px solid var(--surface); }
|
|
::-webkit-scrollbar-track { background: transparent; }
|
|
|
|
@keyframes spin { to { transform: rotate(360deg); } }
|
|
@keyframes rise { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
|
|
|
|
/* ===========================================================================
|
|
Shell: sidebar + main column (+ mobile tab bar)
|
|
========================================================================= */
|
|
|
|
.app {
|
|
display: flex;
|
|
align-items: stretch;
|
|
min-height: 100vh;
|
|
background: var(--surface);
|
|
}
|
|
|
|
.sidebar {
|
|
width: 256px;
|
|
flex: none;
|
|
background: var(--chrome-bg);
|
|
border-right: 1px solid var(--chrome-line);
|
|
display: flex;
|
|
flex-direction: column;
|
|
padding: 16px 12px;
|
|
position: sticky;
|
|
top: 0;
|
|
height: 100vh;
|
|
}
|
|
|
|
.brand {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
padding: 6px 8px 18px;
|
|
text-decoration: none;
|
|
color: var(--chrome-ink);
|
|
}
|
|
.brand-mark {
|
|
width: 32px; height: 32px;
|
|
border-radius: 8px;
|
|
background: var(--accent);
|
|
color: var(--chrome-ink);
|
|
display: flex; align-items: center; justify-content: center;
|
|
flex: none;
|
|
}
|
|
.brand-name { font: 700 20px/1 var(--font-display); letter-spacing: -0.01em; }
|
|
|
|
.nav { display: flex; flex-direction: column; gap: 2px; }
|
|
|
|
.nav-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
padding: 9px 10px;
|
|
border: 0;
|
|
border-radius: 8px;
|
|
background: transparent;
|
|
color: var(--chrome-ink-dim);
|
|
text-align: left;
|
|
font: 600 16px/1 var(--font-ui);
|
|
cursor: pointer;
|
|
text-decoration: none;
|
|
}
|
|
.nav-item:hover { background: var(--chrome-hover); color: var(--chrome-ink); }
|
|
.nav-item.is-active { background: var(--accent); color: var(--chrome-ink); }
|
|
.nav-item.is-active:hover { background: var(--accent-hover); color: var(--chrome-ink); }
|
|
|
|
.sidebar-rule { height: 1px; background: var(--chrome-line); margin: 16px 4px; }
|
|
.sidebar-label {
|
|
font: 600 13px/1 var(--font-ui);
|
|
letter-spacing: .08em;
|
|
text-transform: uppercase;
|
|
color: var(--chrome-ink-dim);
|
|
padding: 4px 10px 10px;
|
|
}
|
|
|
|
.shelf-list { display: flex; flex-direction: column; gap: 2px; }
|
|
.shelf-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
padding: 7px 10px;
|
|
border: 0;
|
|
border-radius: 8px;
|
|
background: transparent;
|
|
text-align: left;
|
|
font: 500 16px/1 var(--font-ui);
|
|
color: var(--chrome-ink-dim);
|
|
cursor: pointer;
|
|
text-decoration: none;
|
|
}
|
|
.shelf-item:hover { background: var(--chrome-hover); color: var(--chrome-ink); }
|
|
.shelf-dot { width: 9px; height: 9px; border-radius: 3px; flex: none; background: var(--c, var(--chrome-ink-dim)); }
|
|
.shelf-item span:nth-child(2) { flex: 1; }
|
|
.shelf-count { font: 500 14px/1 var(--font-mono); color: var(--chrome-ink-dim); }
|
|
|
|
.sidebar-spacer { flex: 1; }
|
|
|
|
.scan-card {
|
|
border: 1px solid var(--chrome-line);
|
|
background: var(--chrome-bg-2);
|
|
border-radius: 12px;
|
|
padding: 12px;
|
|
}
|
|
.scan-card-head {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: baseline;
|
|
margin-bottom: 8px;
|
|
font: 600 14px/1 var(--font-ui);
|
|
color: var(--chrome-ink);
|
|
}
|
|
.scan-card-head .mono { font-size: 13px; color: var(--chrome-ink-dim); }
|
|
.scan-spin { display: flex; }
|
|
.is-scanning .scan-spin { animation: spin 1.4s linear infinite; }
|
|
|
|
.main { flex: 1; min-width: 0; display: flex; flex-direction: column; }
|
|
|
|
.topbar {
|
|
height: 72px;
|
|
flex: none;
|
|
border-bottom: 1px solid var(--line);
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 16px;
|
|
padding: 0 32px;
|
|
background: rgba(255, 255, 255, .85);
|
|
backdrop-filter: blur(12px);
|
|
position: sticky;
|
|
top: 0;
|
|
z-index: 20;
|
|
}
|
|
|
|
.search {
|
|
flex: 1;
|
|
max-width: 520px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
height: 44px;
|
|
padding: 0 14px;
|
|
background: var(--card);
|
|
border: 1px solid var(--line-strong);
|
|
border-radius: 8px;
|
|
}
|
|
.search:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
|
|
.search input {
|
|
flex: 1;
|
|
min-width: 0;
|
|
border: 0;
|
|
outline: none;
|
|
background: transparent;
|
|
font: 500 16px/1 var(--font-ui);
|
|
color: var(--ink);
|
|
}
|
|
.search input::-webkit-search-cancel-button { display: none; }
|
|
|
|
.topbar-end { margin-left: auto; display: flex; align-items: center; gap: 10px; }
|
|
.topbar-totals { font-size: 14px; color: var(--ink-3); }
|
|
|
|
.avatar {
|
|
width: 34px; height: 34px;
|
|
border: 0;
|
|
border-radius: 999px;
|
|
background: var(--plum);
|
|
color: var(--surface);
|
|
font: 700 14px/1 var(--font-ui);
|
|
cursor: pointer;
|
|
flex: none;
|
|
}
|
|
.avatar:hover { filter: brightness(1.06); }
|
|
|
|
.view { flex: 1; padding: 32px; }
|
|
.loading { display: flex; justify-content: center; padding: 64px 0; }
|
|
.spinner {
|
|
width: 22px; height: 22px;
|
|
border: 2px solid var(--line-strong);
|
|
border-top-color: var(--accent);
|
|
border-radius: 999px;
|
|
display: block;
|
|
animation: spin .8s linear infinite;
|
|
}
|
|
|
|
.tabbar { display: none; }
|
|
.scan-mobile { display: none; }
|
|
|
|
/* ===========================================================================
|
|
Buttons, chips, segmented controls
|
|
========================================================================= */
|
|
|
|
.btn {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 8px;
|
|
padding: 9px 14px;
|
|
border: 1px solid var(--line-strong);
|
|
border-radius: 8px;
|
|
background: var(--card);
|
|
color: var(--ink);
|
|
font: 600 15px/1 var(--font-ui);
|
|
cursor: pointer;
|
|
text-decoration: none;
|
|
white-space: nowrap;
|
|
}
|
|
.btn:hover { background: var(--row-hover); color: var(--ink); }
|
|
.btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
|
|
.btn-accent { background: var(--accent); border-color: var(--accent); color: var(--surface); }
|
|
.btn-accent:hover { background: var(--accent-hover); border-color: var(--accent-hover); color: var(--surface); }
|
|
|
|
.btn-danger { background: var(--card); border-color: var(--danger); color: var(--danger); }
|
|
.btn-danger:hover { background: var(--danger-soft); color: var(--danger); }
|
|
|
|
.btn-solid-danger { background: var(--danger); border-color: var(--danger); color: var(--surface); }
|
|
.btn-solid-danger:hover { background: var(--danger-hover); border-color: var(--danger-hover); color: var(--surface); }
|
|
|
|
.btn-light { background: var(--surface); border-color: transparent; color: var(--ink); }
|
|
.btn-light:hover { background: var(--line); }
|
|
|
|
.btn-ghost { background: transparent; border-color: rgba(250, 248, 245, .3); color: var(--surface); }
|
|
.btn-ghost:hover { background: rgba(250, 248, 245, .1); color: var(--surface); }
|
|
|
|
/* Text-only action (used on its own, not on top of .btn). */
|
|
.btn-quiet {
|
|
border: 0;
|
|
background: transparent;
|
|
padding: 0;
|
|
font: 600 15px/1 var(--font-ui);
|
|
color: var(--accent);
|
|
cursor: pointer;
|
|
text-decoration: none;
|
|
}
|
|
.btn-quiet:hover { color: var(--accent-hover); }
|
|
|
|
.btn-block { width: 100%; }
|
|
.btn-lg { padding: 12px 20px; font-size: 16px; }
|
|
.btn:disabled { opacity: .6; cursor: default; }
|
|
|
|
.icon-btn {
|
|
width: 34px; height: 34px;
|
|
flex: none;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
border: 0;
|
|
border-radius: 999px;
|
|
background: transparent;
|
|
color: var(--ink-3);
|
|
cursor: pointer;
|
|
padding: 0;
|
|
}
|
|
.icon-btn:hover { background: var(--line); color: var(--ink); }
|
|
|
|
.seg {
|
|
display: flex;
|
|
padding: 3px;
|
|
background: var(--paper);
|
|
border-radius: 9px;
|
|
gap: 2px;
|
|
}
|
|
.seg button {
|
|
padding: 7px 12px;
|
|
border: 0;
|
|
border-radius: 7px;
|
|
background: transparent;
|
|
color: var(--ink-3);
|
|
font: 600 15px/1 var(--font-ui);
|
|
cursor: pointer;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
.seg button.is-on { background: var(--card); color: var(--ink); box-shadow: var(--shadow-card); }
|
|
.seg .seg-icon { width: 38px; height: 34px; padding: 0; }
|
|
|
|
.chips { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 22px; }
|
|
.chip {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 7px;
|
|
padding: 7px 13px;
|
|
border: 1px solid var(--line-strong);
|
|
border-radius: 999px;
|
|
background: var(--card);
|
|
color: var(--ink-2);
|
|
font: 600 15px/1 var(--font-ui);
|
|
cursor: pointer;
|
|
text-decoration: none;
|
|
}
|
|
.chip:hover { background: var(--row-hover); color: var(--ink); }
|
|
.chip.is-on:hover { background: var(--ink); color: var(--surface); }
|
|
.chip .dot { width: 7px; height: 7px; border-radius: 999px; background: var(--c, var(--accent)); }
|
|
.chip.is-on { background: var(--ink); border-color: var(--ink); color: var(--surface); }
|
|
|
|
.pill {
|
|
font: 600 13px/1 var(--font-ui);
|
|
padding: 5px 9px;
|
|
border-radius: 999px;
|
|
background: var(--accent-soft);
|
|
color: var(--accent);
|
|
white-space: nowrap;
|
|
}
|
|
.pill-quiet { background: var(--line-soft); color: var(--ink-3); }
|
|
|
|
/* ===========================================================================
|
|
Page furniture
|
|
========================================================================= */
|
|
|
|
.page { max-width: 1120px; }
|
|
.page-narrow { max-width: 760px; }
|
|
.page-mid { max-width: 900px; }
|
|
|
|
.eyebrow {
|
|
font: 600 14px/1 var(--font-ui);
|
|
letter-spacing: .08em;
|
|
text-transform: uppercase;
|
|
color: var(--ink-3);
|
|
}
|
|
.eyebrow-sm { font-size: 13px; letter-spacing: .09em; }
|
|
|
|
.page-head {
|
|
display: flex;
|
|
align-items: flex-end;
|
|
justify-content: space-between;
|
|
gap: 20px;
|
|
margin-bottom: 20px;
|
|
flex-wrap: wrap;
|
|
}
|
|
.page-sub { font: 500 16px/1.5 var(--font-ui); color: var(--ink-2); margin-top: 6px; }
|
|
|
|
.section-head {
|
|
display: flex;
|
|
align-items: baseline;
|
|
justify-content: space-between;
|
|
gap: 12px;
|
|
margin-bottom: 14px;
|
|
}
|
|
|
|
.shelf-head {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
margin-bottom: 14px;
|
|
}
|
|
.shelf-head .swatch { width: 10px; height: 10px; border-radius: 3px; background: var(--c, var(--accent)); }
|
|
.shelf-head h2 { margin: 0; font-size: 22px; }
|
|
.shelf-head .rule { flex: 1; height: 1px; background: var(--line); }
|
|
.shelf-head .mono { font-size: 14px; color: var(--ink-3); }
|
|
|
|
.crumbs {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
flex-wrap: wrap;
|
|
font: 500 15px/1 var(--font-ui);
|
|
color: var(--ink-3);
|
|
margin-bottom: 14px;
|
|
}
|
|
.crumbs a { color: var(--ink-2); text-decoration: none; }
|
|
.crumbs a:hover { color: var(--accent); }
|
|
.crumbs .sep { color: var(--ink-4); }
|
|
.crumbs .here { color: var(--ink); font-weight: 600; }
|
|
|
|
.back-link {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
border: 0;
|
|
background: transparent;
|
|
padding: 0;
|
|
margin-bottom: 16px;
|
|
font: 600 15px/1 var(--font-ui);
|
|
color: var(--ink-2);
|
|
cursor: pointer;
|
|
text-decoration: none;
|
|
}
|
|
.back-link:hover { color: var(--accent); }
|
|
|
|
.empty {
|
|
border: 1px dashed var(--line-strong);
|
|
border-radius: 12px;
|
|
padding: 56px 24px;
|
|
text-align: center;
|
|
}
|
|
.empty h2 { font-size: 22px; margin-bottom: 8px; }
|
|
.empty p { font: 500 16px/1.5 var(--font-ui); color: var(--ink-2); margin: 0 0 16px; }
|
|
|
|
/* ===========================================================================
|
|
Course cards, thumbnails, progress
|
|
========================================================================= */
|
|
|
|
.grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; }
|
|
.grid-stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: 14px; margin-bottom: 32px; }
|
|
.stack { display: flex; flex-direction: column; gap: 8px; }
|
|
|
|
.card {
|
|
background: var(--card);
|
|
border: 1px solid var(--line);
|
|
border-radius: 12px;
|
|
padding: 16px;
|
|
}
|
|
|
|
.stat-label { display: flex; align-items: center; gap: 8px; margin-bottom: 10px; }
|
|
.stat-label .swatch { width: 8px; height: 8px; border-radius: 3px; background: var(--c, var(--accent)); }
|
|
.stat-label span { font: 600 14px/1 var(--font-ui); color: var(--ink-2); }
|
|
.stat-value { font: 700 32px/1 var(--font-display); letter-spacing: -0.01em; }
|
|
.stat-sub { font: 500 14px/1.4 var(--font-ui); color: var(--ink-3); margin-top: 6px; }
|
|
|
|
.course-card {
|
|
display: block;
|
|
background: var(--card);
|
|
border: 1px solid var(--line);
|
|
border-radius: 12px;
|
|
padding: 10px;
|
|
cursor: pointer;
|
|
text-decoration: none;
|
|
color: inherit;
|
|
}
|
|
.course-card:hover { box-shadow: var(--shadow-card); border-color: var(--line-strong); color: inherit; }
|
|
.course-card-body { padding: 10px 4px 4px; }
|
|
.course-title { font: 700 16px/1.3 var(--font-ui); }
|
|
.course-meta { font: 500 14px/1.4 var(--font-ui); color: var(--ink-3); margin-top: 3px; }
|
|
|
|
/* Colour block standing in for cover art: two soft blobs over a course hue. */
|
|
.thumb {
|
|
position: relative;
|
|
aspect-ratio: 16 / 10;
|
|
border-radius: 8px;
|
|
overflow: hidden;
|
|
background: var(--c, var(--slate));
|
|
}
|
|
.thumb::before, .thumb::after {
|
|
content: '';
|
|
position: absolute;
|
|
aspect-ratio: 1;
|
|
border-radius: 999px;
|
|
z-index: 0;
|
|
}
|
|
.thumb::before { top: -30%; right: -12%; width: 62%; background: rgba(255, 255, 255, .2); }
|
|
.thumb::after { left: -8%; bottom: -32%; width: 48%; background: rgba(16, 15, 13, .14); }
|
|
.thumb-cat {
|
|
position: absolute;
|
|
left: 10px; top: 8px;
|
|
font: 700 13px/1 var(--font-ui);
|
|
letter-spacing: .06em;
|
|
text-transform: uppercase;
|
|
color: rgba(255, 255, 255, .85);
|
|
text-shadow: 0 1px 3px rgba(16, 15, 13, .55);
|
|
z-index: 2;
|
|
}
|
|
.thumb-dur {
|
|
position: absolute;
|
|
right: 8px; bottom: 8px;
|
|
font: 500 13px/1 var(--font-mono);
|
|
color: var(--surface);
|
|
background: rgba(16, 15, 13, .55);
|
|
padding: 4px 7px;
|
|
border-radius: 999px;
|
|
z-index: 2;
|
|
}
|
|
.thumb-play {
|
|
position: absolute;
|
|
inset: 0;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
z-index: 2;
|
|
}
|
|
.thumb-play span {
|
|
width: 58px; height: 58px;
|
|
border-radius: 999px;
|
|
background: rgba(250, 248, 245, .92);
|
|
color: var(--ink);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
.thumb-bar {
|
|
position: absolute;
|
|
inset: auto 0 0 0;
|
|
height: 4px;
|
|
background: rgba(255, 255, 255, .28);
|
|
z-index: 2;
|
|
}
|
|
.thumb-bar span { display: block; height: 100%; background: var(--surface); }
|
|
|
|
.initial {
|
|
width: 42px; height: 42px;
|
|
flex: none;
|
|
border-radius: 8px;
|
|
background: var(--c, var(--slate));
|
|
color: var(--surface);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font: 700 17px/1 var(--font-display);
|
|
}
|
|
.initial-sm { width: 38px; height: 38px; font-size: 16px; }
|
|
.initial-lg { width: 50px; height: 50px; font-size: 20px; }
|
|
|
|
.bar { flex: 1; height: 5px; border-radius: 999px; background: var(--line); overflow: hidden; }
|
|
.bar span { display: block; height: 100%; border-radius: 999px; background: var(--c, var(--accent)); }
|
|
.bar-row { display: flex; align-items: center; gap: 8px; margin-top: 10px; }
|
|
.bar-row .mono { font-size: 13px; color: var(--ink-3); }
|
|
.bar-lg { height: 6px; }
|
|
|
|
/* ===========================================================================
|
|
Rows and lists
|
|
========================================================================= */
|
|
|
|
.rows {
|
|
background: var(--card);
|
|
border: 1px solid var(--line);
|
|
border-radius: 12px;
|
|
overflow: hidden;
|
|
}
|
|
.row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 14px;
|
|
padding: 0 16px;
|
|
min-height: 62px;
|
|
border-bottom: 1px solid var(--line-soft);
|
|
text-decoration: none;
|
|
color: inherit;
|
|
cursor: pointer;
|
|
}
|
|
.row:last-child { border-bottom: 0; }
|
|
.row:hover { background: var(--row-hover); color: inherit; }
|
|
.row-main { flex: 1; min-width: 0; }
|
|
.row-title {
|
|
font: 600 16px/1.3 var(--font-ui);
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
.row-sub {
|
|
font: 500 14px/1.3 var(--font-ui);
|
|
color: var(--ink-3);
|
|
margin-top: 2px;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
.row-fixed { width: 320px; flex: none; min-width: 0; }
|
|
.row-col { font: 500 14px/1 var(--font-ui); color: var(--ink-2); }
|
|
.row-num { font: 500 14px/1 var(--font-mono); color: var(--ink-3); }
|
|
.row-bar { flex: 1; display: flex; align-items: center; gap: 10px; min-width: 120px; }
|
|
.row-bar .mono { width: 38px; text-align: right; font-size: 13px; color: var(--ink-3); }
|
|
|
|
/* A row whose trailing control (reset) sits outside the link target. */
|
|
.row-link {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 14px;
|
|
flex: 1;
|
|
min-width: 0;
|
|
padding: 9px 0;
|
|
text-decoration: none;
|
|
color: inherit;
|
|
}
|
|
.row-link:hover { color: inherit; }
|
|
|
|
/* Free-standing rows (search hits, recent additions) sit apart from each other. */
|
|
.row-card {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 14px;
|
|
background: var(--card);
|
|
border: 1px solid var(--line);
|
|
border-radius: 10px;
|
|
padding: 12px 14px;
|
|
text-decoration: none;
|
|
color: inherit;
|
|
cursor: pointer;
|
|
}
|
|
.row-card:hover { background: var(--row-hover); color: inherit; }
|
|
|
|
.play-dot {
|
|
width: 30px; height: 30px;
|
|
flex: none;
|
|
border-radius: 999px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background: var(--line-soft);
|
|
color: var(--ink-3);
|
|
}
|
|
.play-dot.is-done { background: var(--success-soft); color: var(--success); }
|
|
.play-dot.is-partial { background: var(--accent-tint); color: var(--accent); }
|
|
|
|
/* ===========================================================================
|
|
Home hero
|
|
========================================================================= */
|
|
|
|
.hero {
|
|
display: flex;
|
|
gap: 20px;
|
|
background: var(--ink);
|
|
border-radius: 16px;
|
|
padding: 20px;
|
|
color: var(--surface);
|
|
margin-bottom: 14px;
|
|
text-decoration: none;
|
|
}
|
|
.hero:hover { color: var(--surface); }
|
|
.hero-art { width: 280px; flex: none; position: relative; border-radius: 10px; overflow: hidden; }
|
|
.hero-body {
|
|
flex: 1;
|
|
min-width: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: center;
|
|
gap: 8px;
|
|
}
|
|
.hero-eyebrow {
|
|
font: 600 13px/1 var(--font-ui);
|
|
letter-spacing: .1em;
|
|
text-transform: uppercase;
|
|
/* The hero card has a dark (var(--ink)) background, so its dim text needs
|
|
the light-on-dark token, not --ink-4 (tuned for dark-on-light). */
|
|
color: var(--chrome-ink-dim);
|
|
}
|
|
.hero-title { font: 700 28px/1.15 var(--font-display); letter-spacing: -0.01em; }
|
|
.hero-sub { font: 500 16px/1.5 var(--font-ui); color: var(--line-strong); }
|
|
.hero-progress { display: flex; align-items: center; gap: 12px; margin-top: 4px; }
|
|
.hero-progress .bar { max-width: 320px; height: 6px; background: rgba(250, 248, 245, .18); }
|
|
.hero-progress .bar span { background: var(--sage); }
|
|
.hero-progress .mono { font-size: 14px; color: var(--line-strong); }
|
|
.hero-actions { display: flex; gap: 10px; margin-top: 12px; flex-wrap: wrap; }
|
|
|
|
/* ===========================================================================
|
|
Watch screen
|
|
========================================================================= */
|
|
|
|
.watch { display: grid; grid-template-columns: minmax(0, 1fr) 360px; gap: 24px; align-items: start; }
|
|
.player-frame { background: var(--ink); border-radius: 16px; padding: 12px; }
|
|
.player-frame .plyr { border-radius: 10px; overflow: hidden; }
|
|
.watch-head {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
justify-content: space-between;
|
|
gap: 16px;
|
|
margin-top: 16px;
|
|
flex-wrap: wrap;
|
|
}
|
|
.watch-head h1 { font-size: 26px; }
|
|
.watch-actions { display: flex; gap: 8px; flex-wrap: wrap; }
|
|
.watch-note { font: 500 15px/1 var(--font-ui); color: var(--ink-3); margin-top: 10px; display: flex; align-items: center; gap: 6px; }
|
|
|
|
.playlist { background: var(--card); border: 1px solid var(--line); border-radius: 12px; overflow: hidden; }
|
|
.playlist-head {
|
|
padding: 12px 14px;
|
|
border-bottom: 1px solid var(--line);
|
|
font: 600 15px/1.3 var(--font-ui);
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
.playlist-scroll { max-height: 60vh; overflow-y: auto; }
|
|
.playlist .row { min-height: 58px; padding: 0 14px; }
|
|
.playlist .row.is-active { background: var(--accent-soft); }
|
|
.playlist .row.is-active .row-title { color: var(--accent); font-weight: 700; }
|
|
|
|
/* ===========================================================================
|
|
Quiz
|
|
========================================================================= */
|
|
|
|
.quiz-score {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 18px;
|
|
border-radius: 12px;
|
|
padding: 18px 20px;
|
|
margin-bottom: 22px;
|
|
background: var(--success-tint);
|
|
border: 1px solid var(--success-line);
|
|
flex-wrap: wrap;
|
|
}
|
|
.quiz-score.is-fail { background: var(--danger-soft); border-color: var(--danger-line); }
|
|
.quiz-score-num { font: 700 44px/1 var(--font-display); color: var(--success); }
|
|
.quiz-score.is-fail .quiz-score-num { color: var(--danger); }
|
|
.quiz-score-body { flex: 1; min-width: 180px; }
|
|
.quiz-score-head { font: 700 17px/1.3 var(--font-ui); color: var(--success); }
|
|
.quiz-score.is-fail .quiz-score-head { color: var(--danger); }
|
|
.quiz-score-sub { font: 500 15px/1.4 var(--font-ui); color: var(--ink-2); margin-top: 3px; }
|
|
|
|
.q-card {
|
|
background: var(--card);
|
|
border: 1px solid var(--line);
|
|
border-radius: 12px;
|
|
padding: 18px 20px;
|
|
}
|
|
.q-card.is-right { border-color: var(--success-line); }
|
|
.q-card.is-wrong { border-color: var(--danger-line); }
|
|
.q-head { display: flex; gap: 12px; margin-bottom: 14px; }
|
|
.q-num {
|
|
width: 28px; height: 28px;
|
|
flex: none;
|
|
border-radius: 999px;
|
|
background: var(--line-soft);
|
|
color: var(--ink-2);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font: 600 14px/1 var(--font-mono);
|
|
}
|
|
.q-card.is-right .q-num { background: var(--success-soft); color: var(--success); }
|
|
.q-card.is-wrong .q-num { background: var(--danger-soft); color: var(--danger); }
|
|
.q-text { font: 600 17px/1.4 var(--font-ui); text-wrap: pretty; }
|
|
.q-note { font: 500 15px/1.4 var(--font-ui); margin-top: 6px; color: var(--success); }
|
|
.q-note.is-wrong { color: var(--danger); }
|
|
|
|
/* The diagram a question refers to. It lives on the course provider's server,
|
|
so it may not load on an offline install — then the caption's link remains. */
|
|
.q-exhibit { margin: 12px 0 0; }
|
|
.q-exhibit img {
|
|
display: block; max-width: 100%; height: auto;
|
|
border: 1px solid var(--line); border-radius: 8px; background: var(--paper);
|
|
}
|
|
.q-exhibit figcaption { margin-top: 6px; font: 500 13px/1.4 var(--font-ui); color: var(--ink-4); }
|
|
.q-exhibit figcaption a { color: var(--ink-3); }
|
|
.q-exhibit.is-broken img { display: none; }
|
|
.q-exhibit.is-broken figcaption::before { content: 'Exhibit could not be loaded — '; color: var(--danger); }
|
|
|
|
.q-options { display: flex; flex-direction: column; gap: 8px; padding-left: 40px; }
|
|
|
|
.q-option {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 11px;
|
|
padding: 11px 13px;
|
|
border: 1px solid var(--line);
|
|
border-radius: 8px;
|
|
background: var(--card);
|
|
color: var(--ink);
|
|
text-align: left;
|
|
font: 500 16px/1.4 var(--font-ui);
|
|
cursor: pointer;
|
|
width: 100%;
|
|
}
|
|
.q-option:hover { background: var(--row-hover); }
|
|
.q-option .mark {
|
|
width: 18px; height: 18px;
|
|
flex: none;
|
|
border-radius: 999px;
|
|
border: 1.5px solid var(--ink-4);
|
|
background: transparent;
|
|
}
|
|
.q-option.multi .mark { border-radius: 5px; }
|
|
.q-option .label { flex: 1; }
|
|
.q-option .tag { font: 600 14px/1 var(--font-ui); color: var(--ink-3); }
|
|
|
|
.q-option.is-picked { background: var(--accent-soft); border-color: var(--accent); }
|
|
.q-option.is-picked .mark { border-color: var(--accent); background: var(--accent); }
|
|
.q-option.is-correct { background: var(--success-tint); border-color: var(--success); }
|
|
.q-option.is-correct .mark { border-color: var(--success); background: var(--success); }
|
|
.q-option.is-correct .tag { color: var(--success); }
|
|
.q-option.is-missed { background: var(--danger-soft); border-color: var(--danger); }
|
|
.q-option.is-missed .mark { border-color: var(--danger); background: var(--danger); }
|
|
.q-option.is-missed .tag { color: var(--danger); }
|
|
.q-option:disabled { cursor: default; }
|
|
|
|
.quiz-actions { display: flex; align-items: center; gap: 14px; margin: 22px 0 8px; flex-wrap: wrap; }
|
|
.quiz-actions .hint { font: 500 15px/1 var(--font-ui); color: var(--ink-3); }
|
|
|
|
/* ===========================================================================
|
|
Settings
|
|
========================================================================= */
|
|
|
|
.settings-section { padding: 20px; margin-bottom: 16px; }
|
|
.settings-section h3 { margin-bottom: 4px; }
|
|
.settings-section > p { font: 500 15px/1.5 var(--font-ui); color: var(--ink-3); margin-bottom: 14px; }
|
|
.settings-section.is-danger { border-color: var(--danger-line); }
|
|
.settings-section.is-danger h3 { color: var(--danger); }
|
|
.settings-section.is-danger > p { color: var(--ink-2); }
|
|
|
|
.path-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
padding: 10px 12px;
|
|
background: var(--surface);
|
|
border: 1px solid var(--line);
|
|
border-radius: 8px;
|
|
}
|
|
.path-row .path { flex: 1; min-width: 0; font: 500 15px/1.4 var(--font-mono); color: var(--ink-2); word-break: break-all; }
|
|
.path-row .count { font: 500 14px/1 var(--font-ui); color: var(--ink-3); white-space: nowrap; }
|
|
|
|
.toggle-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 14px;
|
|
padding: 12px 0;
|
|
border-top: 1px solid var(--line-soft);
|
|
}
|
|
.toggle-row .label { font: 600 16px/1.3 var(--font-ui); }
|
|
.toggle-row .sub { font: 500 15px/1.4 var(--font-ui); color: var(--ink-3); margin-top: 2px; }
|
|
.switch {
|
|
width: 42px; height: 24px;
|
|
flex: none;
|
|
border: 0;
|
|
border-radius: 999px;
|
|
padding: 3px;
|
|
cursor: pointer;
|
|
display: flex;
|
|
justify-content: flex-start;
|
|
background: var(--line-strong);
|
|
transition: background 180ms cubic-bezier(.2, 0, 0, 1);
|
|
}
|
|
.switch span {
|
|
width: 18px; height: 18px;
|
|
border-radius: 999px;
|
|
background: var(--card);
|
|
box-shadow: 0 1px 2px rgba(16, 15, 13, .2);
|
|
transition: transform 180ms cubic-bezier(.2, 0, 0, 1);
|
|
}
|
|
.switch.is-on { background: var(--accent); }
|
|
.switch.is-on span { transform: translateX(18px); }
|
|
|
|
.fact-row {
|
|
display: flex;
|
|
align-items: baseline;
|
|
justify-content: space-between;
|
|
gap: 12px;
|
|
padding: 10px 0;
|
|
border-top: 1px solid var(--line-soft);
|
|
font: 500 15px/1.4 var(--font-ui);
|
|
color: var(--ink-2);
|
|
}
|
|
.fact-row .mono { color: var(--ink-3); font-size: 15px; }
|
|
|
|
/* ===========================================================================
|
|
Toast, modal, account menu
|
|
========================================================================= */
|
|
|
|
.toasts {
|
|
position: fixed;
|
|
left: 24px;
|
|
bottom: 24px;
|
|
z-index: 60;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 10px;
|
|
}
|
|
.toast {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
background: var(--ink);
|
|
color: var(--surface);
|
|
padding: 13px 16px;
|
|
border-radius: 10px;
|
|
box-shadow: var(--shadow-pop);
|
|
animation: rise 180ms cubic-bezier(.2, 0, 0, 1);
|
|
font: 500 16px/1.35 var(--font-ui);
|
|
max-width: 380px;
|
|
}
|
|
.toast .glyph { flex: none; display: flex; color: #4ADE80; }
|
|
.toast.is-error .glyph { color: #F87171; }
|
|
|
|
.overlay {
|
|
position: fixed;
|
|
inset: 0;
|
|
background: rgba(16, 15, 13, .4);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
z-index: 70;
|
|
padding: 20px;
|
|
}
|
|
.dialog {
|
|
width: 420px;
|
|
max-width: 100%;
|
|
background: var(--card);
|
|
border-radius: 16px;
|
|
padding: 24px;
|
|
box-shadow: var(--shadow-pop);
|
|
animation: rise 160ms cubic-bezier(.2, 0, 0, 1);
|
|
}
|
|
.dialog h3 { font-family: var(--font-display); font-size: 22px; line-height: 1.2; margin-bottom: 8px; }
|
|
.dialog p { font: 500 16px/1.5 var(--font-ui); color: var(--ink-2); margin-bottom: 20px; }
|
|
.dialog-actions { display: flex; justify-content: flex-end; gap: 10px; }
|
|
|
|
.menu {
|
|
position: fixed;
|
|
z-index: 65;
|
|
min-width: 190px;
|
|
background: var(--card);
|
|
border: 1px solid var(--line);
|
|
border-radius: 12px;
|
|
box-shadow: var(--shadow-pop);
|
|
padding: 6px;
|
|
}
|
|
.menu-user {
|
|
padding: 8px 10px 10px;
|
|
border-bottom: 1px solid var(--line-soft);
|
|
margin-bottom: 6px;
|
|
}
|
|
.menu-user .who { font: 700 15px/1.3 var(--font-ui); }
|
|
.menu-user .what { font: 500 14px/1.3 var(--font-ui); color: var(--ink-3); margin-top: 2px; }
|
|
.menu button {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
width: 100%;
|
|
padding: 9px 10px;
|
|
border: 0;
|
|
border-radius: 8px;
|
|
background: transparent;
|
|
color: var(--ink-2);
|
|
font: 600 15px/1 var(--font-ui);
|
|
text-align: left;
|
|
cursor: pointer;
|
|
}
|
|
.menu button:hover { background: var(--row-hover); color: var(--ink); }
|
|
.menu button.is-danger { color: var(--danger); }
|
|
.menu button.is-danger:hover { background: var(--danger-soft); }
|
|
|
|
/* ===========================================================================
|
|
Auth
|
|
========================================================================= */
|
|
|
|
.auth {
|
|
min-height: 100vh;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 24px;
|
|
}
|
|
.auth-card {
|
|
width: 380px;
|
|
max-width: 100%;
|
|
background: var(--card);
|
|
border: 1px solid var(--line);
|
|
border-radius: 16px;
|
|
box-shadow: var(--shadow-frame);
|
|
padding: 28px;
|
|
}
|
|
.auth-brand { display: flex; align-items: center; gap: 10px; margin-bottom: 18px; }
|
|
.auth-card h1 { font-size: 26px; margin-bottom: 6px; }
|
|
.auth-card .sub { font: 500 16px/1.5 var(--font-ui); color: var(--ink-3); margin-bottom: 20px; }
|
|
.field { margin-bottom: 14px; }
|
|
.field label {
|
|
display: block;
|
|
font: 600 14px/1 var(--font-ui);
|
|
color: var(--ink-2);
|
|
margin-bottom: 6px;
|
|
}
|
|
.field input {
|
|
width: 100%;
|
|
height: 44px;
|
|
padding: 0 14px;
|
|
border: 1px solid var(--line-strong);
|
|
border-radius: 8px;
|
|
background: var(--surface);
|
|
color: var(--ink);
|
|
outline: none;
|
|
}
|
|
.field input:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
|
|
.auth-actions { display: flex; flex-direction: column; gap: 8px; margin-top: 18px; }
|
|
.alert {
|
|
background: var(--danger-soft);
|
|
border: 1px solid var(--danger-line);
|
|
color: var(--danger);
|
|
border-radius: 8px;
|
|
padding: 10px 12px;
|
|
font: 500 15px/1.4 var(--font-ui);
|
|
margin-bottom: 14px;
|
|
}
|
|
|
|
/* ===========================================================================
|
|
Responsive — the design's 390px frame informs everything below 900px
|
|
========================================================================= */
|
|
|
|
/* Larger type needs more room in the library list row: drop the state column
|
|
first, then narrow the title column, before the mobile rules take over. */
|
|
@media (max-width: 1280px) {
|
|
.row-col { display: none; }
|
|
}
|
|
|
|
@media (max-width: 1100px) {
|
|
.grid, .grid-stats { grid-template-columns: repeat(3, 1fr); }
|
|
.watch { grid-template-columns: minmax(0, 1fr); }
|
|
.playlist-scroll { max-height: 42vh; }
|
|
.row-fixed { width: 240px; }
|
|
}
|
|
|
|
@media (max-width: 900px) {
|
|
.app { flex-direction: column; }
|
|
.sidebar { display: none; }
|
|
.view { padding: 16px 20px 96px; }
|
|
|
|
.topbar { height: 64px; padding: 0 20px; gap: 10px; }
|
|
.search { max-width: none; border-radius: 999px; height: 44px; }
|
|
.topbar-totals { display: none; }
|
|
.scan-mobile {
|
|
display: flex;
|
|
width: 42px; height: 42px;
|
|
background: var(--accent);
|
|
color: var(--surface);
|
|
}
|
|
.scan-mobile:hover { background: var(--accent-hover); color: var(--surface); }
|
|
.is-scanning .scan-mobile svg { animation: spin 1.4s linear infinite; }
|
|
|
|
.grid, .grid-stats { grid-template-columns: 1fr 1fr; gap: 12px; }
|
|
h1 { font-size: 28px; }
|
|
.hero { flex-direction: column; padding: 12px; border-radius: 14px; }
|
|
.hero-art { width: 100%; }
|
|
.hero-title { font-size: 22px; }
|
|
.row-fixed, .row-col { display: none; }
|
|
.q-options { padding-left: 0; }
|
|
.toasts { left: 16px; right: 16px; bottom: 84px; }
|
|
.toast { max-width: none; }
|
|
|
|
.tabbar {
|
|
position: fixed;
|
|
left: 0; right: 0; bottom: 0;
|
|
z-index: 40;
|
|
display: flex;
|
|
padding: 8px 12px calc(8px + env(safe-area-inset-bottom));
|
|
border-top: 1px solid var(--chrome-line);
|
|
background: rgba(20, 17, 13, .94);
|
|
backdrop-filter: blur(12px);
|
|
}
|
|
.tab {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 5px;
|
|
border: 0;
|
|
background: transparent;
|
|
padding: 6px 0;
|
|
cursor: pointer;
|
|
font: 600 13px/1 var(--font-ui);
|
|
color: var(--chrome-ink-dim);
|
|
text-decoration: none;
|
|
}
|
|
.tab.is-active { color: var(--chrome-accent); }
|
|
}
|
|
|
|
@media (max-width: 560px) {
|
|
.grid-stats { grid-template-columns: 1fr 1fr; }
|
|
.page-head { align-items: flex-start; }
|
|
}
|
|
|
|
@media (prefers-reduced-motion: reduce) {
|
|
* { animation-duration: .001ms !important; animation-iteration-count: 1 !important; transition-duration: .001ms !important; }
|
|
.is-scanning .scan-spin, .is-scanning .scan-mobile svg { animation: spin 1.4s linear infinite !important; }
|
|
}
|
|
|
|
/* Boot failure report (index.php installs the handler that creates this).
|
|
Deliberately self-contained — no custom properties — because the whole
|
|
point is to stay readable when the app never started. */
|
|
#boot-error {
|
|
position: fixed;
|
|
inset: 16px;
|
|
z-index: 9999;
|
|
overflow: auto;
|
|
margin: 0;
|
|
padding: 20px;
|
|
border: 1px solid #B84A3C;
|
|
border-radius: 10px;
|
|
background: #FFF6F4;
|
|
color: #5A1E16;
|
|
font: 500 13px/1.6 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
white-space: pre-wrap;
|
|
word-break: break-word;
|
|
}
|