diff --git a/static/css/base.css b/static/css/base.css new file mode 100644 index 0000000..77733e7 --- /dev/null +++ b/static/css/base.css @@ -0,0 +1,2120 @@ +/* ---------- Reusable check icon (SVG mask) ---------- */ +/* Source: /static/icons/check.svg. Inherits color from + currentColor so it adapts to surrounding text color + (green badges, white buttons, etc.). */ +.icon-check, +.icon-cross { + display: inline-block; + width: 1em; + height: 1em; + vertical-align: -0.15em; + background-color: currentColor; + flex-shrink: 0; +} +.icon-check { + -webkit-mask: url("/static/icons/check.svg") no-repeat center / + contain; + mask: url("/static/icons/check.svg") no-repeat center / contain; +} +.icon-cross { + -webkit-mask: url("/static/icons/cross.svg") no-repeat center / + contain; + mask: url("/static/icons/cross.svg") no-repeat center / contain; +} + +/* ---------- Local fonts (served from /static/fonts) ---------- */ +@font-face { + font-family: "Happy Headline"; + src: url("/static/fonts/Happy-Headline.woff2") format("woff2"); + font-weight: 400 900; + font-style: normal; + font-display: swap; +} +@font-face { + font-family: "JetBrains Mono"; + src: url("/static/fonts/JetBrainsMono-Regular.woff2") + format("woff2"); + font-weight: 400; + font-style: normal; + font-display: swap; +} +/* Lora italic, used for chapter intro epigraphs. + Two subsets: latin covers ASCII + Latin-1 Supplement, + latin-ext adds extended Latin diacritics and currency + symbols. Variable weight 400–700 from one file each. */ +@font-face { + font-family: "Lora"; + src: url("/static/fonts/lora/lora-italic-latin.woff2") + format("woff2"); + font-style: italic; + font-weight: 400 700; + font-display: swap; + unicode-range: + U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, + U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, + U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +@font-face { + font-family: "Lora"; + src: url("/static/fonts/lora/lora-italic-latin-ext.woff2") + format("woff2"); + font-style: italic; + font-weight: 400 700; + font-display: swap; + unicode-range: + U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, + U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, + U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, + U+2113, U+2C60-2C7F, U+A720-A7FF; +} +/* ---------- Theme tokens ---------- */ +:root { + /* GitHub Primer (light) */ + --color-background: #ffffff; + --color-surface: #f6f8fa; + --color-primary: #0969da; + --color-primary-muted: #0550ae; + --color-primary-rgb: 9, 105, 218; + --color-text: #1f2328; + --color-text-muted: #59636e; + --color-border: #d1d9e0; + --color-input-bg: #ffffff; + --color-input-border: #d1d9e0; + --color-success: #1a7f37; + --color-warning: #9a6700; + --color-error: #d1242f; + /* Brand pink, used for the headline color and for high-emphasis + destructive actions (e.g. the admin "Remove participant" + button) so the page reads with a single accent palette + instead of competing reds. */ + --color-brand: #fc218a; + --color-brand-muted: #d11a73; + --logo-filter: invert(0%); + /* Syntax highlight palette, shared by chapter prose code + blocks and the in-page CodeMirror editor. */ + --syn-keyword: #cf222e; + --syn-type: #953800; + --syn-string: #0a3069; + --syn-number: #0550ae; + --syn-definition: #8250df; + --syn-macro: #116329; + --syn-comment: #6e7781; + --syn-punctuation: #57606a; + --syn-invalid: #cf222e; + --syn-bg: #f6f8fa; + --syn-fg: #1f2328; + --syn-cursor: #1f2328; + --syn-selection-bg: #b4d5fe; + --syn-gutter-bg: #f6f8fa; + --syn-gutter-fg: #59636e; + --syn-active-line: #eaeef2; +} + +html[data-theme="dark"] { + /* GitHub Primer (dark) */ + --color-background: #0d1117; + --color-surface: #151b23; + --color-primary: #4493f8; + --color-primary-muted: #1f6feb; + --color-primary-rgb: 68, 147, 248; + --color-text: #f0f6fc; + --color-text-muted: #9198a1; + --color-border: #3d444d; + --color-input-bg: #0d1117; + --color-input-border: #3d444d; + --color-success: #3fb950; + --color-warning: #d29922; + --color-error: #f85149; + --color-brand: #fc218a; + --color-brand-muted: #ff4ca0; + --logo-filter: invert(100%); + --syn-keyword: #cba6f7; + --syn-type: #f9e2af; + --syn-string: #a6e3a1; + --syn-number: #fab387; + --syn-definition: #89b4fa; + --syn-macro: #f5c2e7; + --syn-comment: #6c7086; + --syn-punctuation: #94a3b8; + --syn-invalid: #f38ba8; + --syn-bg: #1e1e2e; + --syn-fg: #cdd6f4; + --syn-cursor: #cdd6f4; + /* Selection bg has to stay readable against every syntax + token color (yellows, greens, reds), so we pick a + saturated blue instead of a muted surface tone. */ + --syn-selection-bg: #3c5a80; + --syn-gutter-bg: #1e1e2e; + --syn-gutter-fg: #6c7086; + --syn-active-line: #2a2b3c; +} + +/* ---------- Reset ---------- */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: + "Inter", + -apple-system, + BlinkMacSystemFont, + "Segoe UI", + Roboto, + "Helvetica Neue", + sans-serif; + background-color: var(--color-background); + color: var(--color-text); + line-height: 1.6; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + min-height: 100vh; + display: flex; + flex-direction: column; +} + +h1, +.header h1, +.exercise-title { + font-family: + "Happy Headline", + "Inter", + -apple-system, + BlinkMacSystemFont, + sans-serif; + letter-spacing: -0.01em; +} + +h2, +h3, +h4, +h5, +h6 { + /* Sub-headlines stay in the body face so they read as section + labels rather than competing with the page title. */ + font-family: inherit; + letter-spacing: -0.005em; +} + +code, +pre, +kbd, +samp, +.cm-editor, +.cm-content, +.code-block { + font-family: + "JetBrains Mono", "SF Mono", Monaco, Menlo, Consolas, + monospace; +} + +/* Editor preferences from `/settings`. The settings page + writes `data-editor-font-size`, `data-editor-ligatures`, + and `data-editor-active-line` on `` and persists + them to localStorage; `base.html` re-applies them before + paint so every page picks them up at load. The base size + (`0.9rem`) used to live as an inline style on each + editor's root in `static/js/inline-editor.js`; it now + lives here so the user override actually wins. */ +.cm-editor { + font-size: 0.9rem; +} +html[data-editor-font-size="small"] .cm-editor { + font-size: 0.78rem; +} +html[data-editor-font-size="large"] .cm-editor { + font-size: 1.05rem; +} +/* Programming ligatures (JetBrains Mono ships them on by + default). Some readers prefer to see the raw glyphs so + `==`, `!=`, `->`, `=>` etc. read as their literal tokens; + disable across every monospaced surface so the editor, + inline code, and read-only viewers stay consistent. */ +html[data-editor-ligatures="off"] code, +html[data-editor-ligatures="off"] pre, +html[data-editor-ligatures="off"] kbd, +html[data-editor-ligatures="off"] samp, +html[data-editor-ligatures="off"] .cm-editor, +html[data-editor-ligatures="off"] .cm-content, +html[data-editor-ligatures="off"] .code-block, +html[data-editor-ligatures="off"] .rust-editor { + font-variant-ligatures: none; + font-feature-settings: + "liga" 0, + "calt" 0, + "clig" 0, + "dlig" 0; +} +/* The active-line + active-gutter highlight is on by + default (via `highlightActiveLine` / `Gutter` in + `static/js/inline-editor.js`). Some folks find the + extra band distracting; this knob hides the background + without touching the editor's extension list. */ +html[data-editor-active-line="off"] .cm-activeLine, +html[data-editor-active-line="off"] .cm-activeLineGutter { + background-color: transparent !important; +} + +/* CodeMirror selection. We pin this in static CSS (rather than + via EditorView.theme) so it always reflects the current + --syn-selection-bg, even after a theme toggle. */ +.cm-editor .cm-selectionBackground, +.cm-editor.cm-focused .cm-selectionBackground, +.cm-editor .cm-content ::selection { + background-color: var(--syn-selection-bg) !important; +} + +/* ---------- Layout ---------- */ +.container { + width: 100%; + max-width: 1100px; + margin: 0 auto; + padding: 1.5rem 1.5rem 4rem; + flex: 1 0 auto; +} + +.header { + text-align: center; + margin-bottom: 3rem; +} +.header h1 { + font-size: 2.5rem; + font-weight: 700; + letter-spacing: -0.01em; + color: var(--color-text); + margin-bottom: 0.5rem; +} +.header p { + font-size: 1.05rem; + color: var(--color-text-muted); +} + +/* ---------- Cards / forms / buttons (used by other pages) ---------- */ +.card { + background-color: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: 8px; + padding: 1.5rem; + margin-bottom: 1.25rem; +} +.form-group { + margin-bottom: 1.25rem; +} +.form-group label { + display: block; + margin-bottom: 0.4rem; + font-weight: 600; + color: var(--color-text); +} +.form-control { + width: 100%; + padding: 0.7rem 1rem; + background-color: var(--color-input-bg); + border: 1px solid var(--color-input-border); + border-radius: 6px; + color: var(--color-text); + font-size: 1rem; +} +.form-control:focus { + outline: none; + border-color: var(--color-primary); +} +.form-control::placeholder { + color: var(--color-text-muted); +} + +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.4rem; + height: 2.25rem; + padding: 0 0.95rem; + background-color: var(--color-primary); + color: #fff; + text-decoration: none; + border: 1px solid var(--color-primary); + border-radius: 6px; + font-family: inherit; + font-size: 0.9rem; + font-weight: 600; + line-height: 1; + white-space: nowrap; + cursor: pointer; + box-sizing: border-box; +} +.btn:disabled { + opacity: 0.6; + cursor: not-allowed; +} +/* Post-autosubmit success state: keeps the button visible as a + confirmation indicator without looking like a generic + disabled primary action. The Run → Submit handler in + `static/js/inline-editor.js` adds/removes this class. */ +.btn.is-submitted, +.btn.is-submitted:disabled { + opacity: 1; + background-color: var(--color-success, #2e7d32); + border-color: var(--color-success, #2e7d32); + color: #fff; + cursor: default; +} +.btn:hover { + background-color: var(--color-primary-muted); +} +.btn-secondary { + background-color: var(--color-surface); + color: var(--color-text); + border: 1px solid var(--color-border); +} +.btn-secondary:hover { + background-color: var(--color-background); + border-color: var(--color-text-muted); + color: var(--color-text); +} +.btn-success { + background-color: var(--color-success); + border-color: var(--color-success); + color: #fff; +} +.btn-success:hover { + /* Slight darken on hover; falls back to filter so we don't + need a separate token per theme. */ + filter: brightness(0.92); +} + +.exercise-list { + display: grid; +} +.exercise-item { + display: flex; + align-items: center; + padding: 1rem; + background-color: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: 6px; +} +.exercise-item:hover { + border-color: var(--color-primary); +} +.exercise-status { + font-size: 1.25rem; + margin-right: 1rem; +} +.exercise-name { + font-weight: 500; + color: var(--color-text); + font-family: "SF Mono", Monaco, monospace; +} +.status-completed { + color: var(--color-success); +} +.status-perfected { + color: var(--color-warning); +} +.status-pending { + color: var(--color-text-muted); +} + +.stats { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + gap: 1rem; + margin-bottom: 1.5rem; +} +.stat-card { + text-align: center; + padding: 1rem; + background-color: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: 8px; +} +.stat-number { + font-size: 1.75rem; + font-weight: 700; + color: var(--color-primary); + display: block; +} +.stat-label { + color: var(--color-text-muted); + font-size: 0.8rem; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +/* ---------- Admin / team page layout rhythm ---------- + Both `/admin` and the per-team pages add `.admin-page` to + the outer container so they share the same column width, + vertical padding, and section spacing. The rules live here + (rather than in either page's scoped ` + - - - {# Quiz interaction. Inert no-op on every chapter except the one that ships - a `quiz.toml`; the script just looks for `[data-quiz]` and bails when - there's nothing to wire up. #} + {% if exercise.is_quiz() %} + {% endif %} {% if next_dot.is_some() && ulid.is_none() && exercise.wants_signup_on_pass() %} {% match next_dot %} {% when Some with @@ -631,25 +630,15 @@

Results

// Map of named effects fired when a section's tests all pass. // Each handler receives `{ section, data }` (the run-result payload). const PassHooks = { - confetti() { - if (typeof window.confetti !== "function") { - console.warn( - "[corrode] confetti hook fired but window.confetti is not loaded (CDN blocked?)", + async confetti() { + try { + const { celebrateWithConfetti } = await import( + "/static/js/confetti.js" ); - return; + await celebrateWithConfetti(); + } catch (err) { + console.warn("[corrode] confetti failed to load", err); } - const fire = (originX) => - window.confetti({ - particleCount: 120, - spread: 80, - startVelocity: 55, - ticks: 250, - zIndex: 10000, - origin: { x: originX, y: 0.7 }, - }); - fire(0.2); - fire(0.5); - fire(0.8); }, }; @@ -772,35 +761,38 @@

Results