Files
ryan c18510b3c3 Import the exercise builder
The drag-and-drop workspace for building BI122G exercise sheets, served
by the Workshop at /projects/exercise-builder. Plain HTML, CSS and ES
modules; no build step. It runs entirely in the browser — work in
progress lives in localStorage and finished sheets leave as downloads,
so nothing is written on the server.

This directory is canonical. It began as a deployment target for
deploy_builder.sh syncing from the BI122G course folder, and that
arrangement has ended; see the README.

Includes the sheet's light/dark switch: the stylesheet already carried
both themes and followed the reader's system setting, so this adds the
control that overrides it — a corner toggle built by sheet.js, applied
before first paint by a head script the exporter writes, and hidden in
print. The builder's toolbar previews the same attribute on the canvas.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ed5kU3HfZq9dfj7EYj5J2v
2026-08-11 19:59:59 +02:00

684 lines
17 KiB
CSS

/* ============================================================
BI122G — Exercise 3
Palette from the university's own materials:
#043554 the lecture deck's title-slide navy — masthead only
#0969A8 HiS blue — the lecture-slide heading blue and the
logo crest; the core, examinable track, plus
structural rules and links
#157539 HiS emerald — the side quests
#B52E75 HiS ruby — reserved entirely for the protips
graphite scale for text and rules.
Track system:
core blue edge on the page ground
side quest green tinted panel, dashed green edge
protip ruby tinted band, ruby edge
Each is distinguished by form as well as hue, so the
distinction survives greyscale printing.
Type: Open Sans, as on the university site, with Cascadia
Mono for terminal text — the face students see in VS Code.
============================================================ */
:root{
--paper: #FFFFFF;
--surface: #F4F7FA;
--sunk: #EAEFF4;
--ink: #14181B;
--ink-body: #313538;
--ink-soft: #5C6165;
--ink-faint: #7E858A;
--rule: #DDE4EA;
--rule-firm: #B4BFC7;
--green: #157539;
--green-tint: #EBF4EF;
--green-edge: #9AC7AC;
--blue: #0969A8;
--blue-deep: #06507F;
--blue-tint: #EAF3F9;
--blue-edge: #9CC4DD;
--ruby: #B52E75;
--ruby-tint: #FBEEF4;
--ruby-edge: #E2A8C6;
/* which hue carries which track. Swap these two groups to flip it. */
--core: var(--blue);
--core-tint: var(--blue-tint);
--core-edge: var(--blue-edge);
--side: var(--green);
--side-tint: var(--green-tint);
--side-edge: var(--green-edge);
--term-bg: #17202A;
--term-ink: #DDE4EA;
--term-dim: #939CA3;
--term-cue: #6FBCE8;
--term-warn: #E8A26A;
--logo-filter: none;
--sans: "Open Sans", "Segoe UI", system-ui, -apple-system, "Helvetica Neue", Arial, sans-serif;
--mono: "Cascadia Mono", "Cascadia Code", ui-monospace, "SF Mono", Menlo, "DejaVu Sans Mono", Consolas, monospace;
}
@media (prefers-color-scheme: dark){
:root:not([data-theme="light"]){
--paper: #0B0F12;
--surface: #151B20;
--sunk: #05080A;
--ink: #F4F7FA;
--ink-body: #D6DEE4;
--ink-soft: #A6B0B8;
--ink-faint: #808A92;
--rule: #242C33;
--rule-firm: #3A444C;
--green: #5DBE84;
--green-tint:#0F2318;
--green-edge:#35624A;
--blue: #6FBCE8;
--blue-deep: #A5D6F2;
--blue-tint: #0F2331;
--blue-edge: #35617E;
--ruby: #E87AB0;
--ruby-tint: #26141D;
--ruby-edge: #7C4160;
--term-bg: #161D22;
--logo-filter: invert(1);
}
}
:root[data-theme="dark"]{
--paper: #0B0F12;
--surface: #151B20;
--sunk: #05080A;
--ink: #F4F7FA;
--ink-body: #D6DEE4;
--ink-soft: #A6B0B8;
--ink-faint: #808A92;
--rule: #242C33;
--rule-firm: #3A444C;
--green: #5DBE84;
--green-tint:#0F2318;
--green-edge:#35624A;
--blue: #6FBCE8;
--blue-deep: #A5D6F2;
--blue-tint: #0F2331;
--blue-edge: #35617E;
--ruby: #E87AB0;
--ruby-tint: #26141D;
--ruby-edge: #7C4160;
--term-bg: #161D22;
--logo-filter: invert(1);
}
*{ box-sizing: border-box; }
body{
margin: 0;
background: var(--paper);
color: var(--ink-body);
font-family: var(--sans);
font-size: 16.5px;
line-height: 1.65;
-webkit-font-smoothing: antialiased;
}
.sheet{
counter-reset: part; /* only the core parts take a number */
max-width: 58rem;
margin: 0 auto;
padding: 2.5rem 1.5rem 6rem;
}
/* ---------- masthead ---------- */
.masthead{ margin-bottom: 2.5rem; }
.logo{
display: block;
width: 13.5rem;
max-width: 60%;
height: auto;
margin-bottom: 2.25rem;
filter: var(--logo-filter);
}
.eyebrow{
font-size: .72rem;
font-weight: 600;
letter-spacing: .14em;
text-transform: uppercase;
color: var(--blue);
margin-bottom: .5rem;
}
h1{
font-size: clamp(1.9rem, 4.6vw, 2.7rem);
line-height: 1.14;
font-weight: 700;
/* white in the dark theme; a literal #fff would vanish on the light
theme and in print, so this tracks the theme's strongest ink */
color: var(--ink);
margin: 0 0 .9rem;
letter-spacing: -.015em;
}
/* the masthead spans the full column; the reading measure applies to
body copy further down, not to the title block */
.standfirst{
font-size: 1.08rem;
color: var(--ink);
max-width: none;
margin: 0;
border-top: 3px solid var(--blue);
padding-top: 1rem;
}
/* ---------- generic type ---------- */
p{ margin: 0 0 1rem; max-width: 68ch; }
h2, h3{ text-wrap: balance; }
a{ color: var(--blue); text-underline-offset: .18em; }
strong{ font-weight: 600; color: var(--ink); }
code{
font-family: var(--mono);
font-size: .87em;
background: var(--sunk);
border: 1px solid var(--rule);
border-radius: 3px;
padding: .06em .32em;
white-space: nowrap;
color: var(--ink);
}
h2 code, h3 code, .cmd code, .term code{ background: none; border: 0; padding: 0; }
kbd{
font-family: var(--sans);
font-size: .76em;
font-weight: 600;
background: var(--paper);
border: 1px solid var(--rule-firm);
border-bottom-width: 2px;
border-radius: 4px;
padding: .12em .45em;
color: var(--ink);
white-space: nowrap;
}
/* ---------- legend ---------- */
.legend{
display: grid;
grid-template-columns: repeat(auto-fit, minmax(14.5rem, 1fr));
gap: .9rem;
margin: 2.25rem 0 1rem;
align-items: start;
}
.legend > div{
padding: .85rem 1rem;
font-size: .88rem;
color: var(--ink-soft);
border-radius: 0 4px 4px 0;
}
.legend .l-core{
background: var(--core-tint);
border: 1px solid var(--core-edge);
border-left: 4px solid var(--core);
}
.legend .l-side{
background: var(--side-tint);
border: 1px dashed var(--side-edge);
border-left: 4px solid var(--side);
}
.legend .l-protip{
background: var(--ruby-tint);
border: 1px solid var(--ruby-edge);
border-left: 4px solid var(--ruby);
}
.legend b{
display: block;
font-size: .68rem;
letter-spacing: .14em;
text-transform: uppercase;
margin-bottom: .3rem;
color: var(--ink);
}
.legend .l-core b{ color: var(--core); }
.legend .l-side b{ color: var(--side); }
.legend .l-protip b{ color: var(--ruby); }
/* ---------- parts ---------- */
.part{
display: grid;
grid-template-columns: 5rem 1fr;
gap: 0 2rem;
padding-top: 2.5rem;
margin-top: 2.5rem;
border-top: 1px solid var(--rule);
}
.part:first-of-type{ border-top: 2px solid var(--blue); }
.part:not(.is-side){ counter-increment: part; }
/* a side quest is not a step in the sequence, so it carries no number and
its label sits out to the right, clear of the numbered spine */
.part.is-side{ grid-template-columns: 1fr 6rem; }
.part.is-side > .body{ grid-column: 1; }
.part.is-side > .rail{ grid-column: 2; text-align: right; align-items: flex-end; }
.rail{
font-size: .67rem;
letter-spacing: .12em;
text-transform: uppercase;
color: var(--ink-faint);
display: flex;
flex-direction: column;
gap: .4rem;
padding-top: .5rem;
}
.rail .num::before{ content: counter(part); }
.rail .num{
font-size: 2.1rem;
font-weight: 700;
line-height: .9;
letter-spacing: -.03em;
color: var(--rule-firm);
font-variant-numeric: tabular-nums;
}
.rail .tag{ font-weight: 700; }
.rail .tag.core{ color: var(--core); }
.rail .tag.side{ color: var(--side); }
.part > .body{ min-width: 0; }
.part h2{
font-size: 1.5rem;
font-weight: 700;
color: var(--core);
margin: 0 0 .7rem;
letter-spacing: -.012em;
line-height: 1.2;
}
.part.is-side h2{ color: var(--side); }
/* a whole side-quest part reads as one blue panel */
.part.is-side > .body{
background: var(--side-tint);
border: 1px dashed var(--side-edge);
border-left: 4px solid var(--side);
border-radius: 0 5px 5px 0;
padding: 1.2rem 1.4rem;
}
.part.is-side > .rail{ padding-top: 1.7rem; }
/* ---------- tasks ---------- */
ol.tasks{
list-style: none;
counter-reset: task;
margin: 1.4rem 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 1.3rem;
}
ol.tasks > li{
counter-increment: task;
display: grid;
grid-template-columns: 1.55rem 1fr;
gap: 0 .8rem;
min-width: 0;
}
ol.tasks > li::before{
content: counter(task);
font-size: .72rem;
font-weight: 700;
color: var(--paper);
background: var(--core);
border-radius: 50%;
width: 1.55rem;
height: 1.55rem;
display: grid;
place-items: center;
margin-top: .22rem;
font-variant-numeric: tabular-nums;
}
.is-side ol.tasks > li::before{ background: var(--side); }
ol.tasks > li > *{ grid-column: 2; min-width: 0; }
ol.tasks > li > *:last-child{ margin-bottom: 0; }
ol.tasks p{ margin-bottom: .6rem; }
/* ---------- command + terminal blocks ---------- */
.cmd, .term{
margin: 0 0 .9rem;
border-radius: 4px;
font-family: var(--mono);
font-size: .85rem;
line-height: 1.7;
position: relative;
}
.term{ overflow-x: auto; }
/* the whole block is the copy target, so nothing prints that
would not also work on paper */
.cmd{
background: var(--surface);
border: 1px solid var(--rule);
border-left: 3px solid var(--core);
padding: .6rem 4.5rem .6rem .85rem;
color: var(--ink);
white-space: pre-wrap;
overflow-wrap: anywhere;
cursor: pointer;
}
.cmd::before{
content: "$";
color: var(--core);
padding-right: .55rem;
user-select: none;
}
.cmd::after{
content: "copy";
position: absolute;
top: .45rem;
right: .7rem;
font-family: var(--sans);
font-size: .6rem;
font-weight: 600;
letter-spacing: .1em;
text-transform: uppercase;
color: var(--ink-faint);
opacity: 0;
transition: opacity .15s ease;
}
.cmd:hover::after,
.cmd:focus-visible::after{ opacity: 1; }
.cmd.copied::after{ content: "copied"; opacity: 1; color: var(--core); }
.cmd:focus-visible{ outline: 2px solid var(--core); outline-offset: 2px; }
/* inside blue territory the boxes sit on white so they stay legible */
.is-side .cmd, .side-quest .cmd{
background: var(--paper);
border-left-color: var(--side);
}
.is-side .cmd::before, .side-quest .cmd::before{ color: var(--side); }
.is-side .cmd.copied::after,
.side-quest .cmd.copied::after{ color: var(--side); }
.term{
background: var(--term-bg);
color: var(--term-ink);
padding: .8rem 1rem;
white-space: pre;
border: 1px solid var(--rule-firm);
}
.term .p{ color: var(--term-cue); }
.term .c{ color: var(--term-ink); }
.term .o{ color: var(--term-dim); }
.term .e{ color: var(--term-warn); }
.term .cap{
display: block;
font-family: var(--sans);
font-size: .62rem;
font-weight: 600;
letter-spacing: .13em;
text-transform: uppercase;
color: var(--term-dim);
margin-bottom: .5rem;
white-space: normal;
}
/* ---------- side quest ---------- */
.side-quest{
border: 1px dashed var(--side-edge);
border-left: 4px solid var(--side);
background: var(--side-tint);
padding: 1rem 1.15rem;
margin: 1.5rem 0;
border-radius: 0 4px 4px 0;
}
/* nested inside an already-blue part, drop back to the page ground */
.part.is-side .side-quest{ background: var(--paper); }
.side-quest > summary{
font-size: .71rem;
font-weight: 700;
letter-spacing: .13em;
text-transform: uppercase;
color: var(--side);
cursor: pointer;
list-style: none;
display: flex;
align-items: baseline;
gap: .6rem;
}
.side-quest > summary::-webkit-details-marker{ display: none; }
.side-quest > summary::after{
content: "optional";
font-size: .62rem;
font-weight: 400;
letter-spacing: .08em;
color: var(--ink-faint);
}
.side-quest > summary:focus-visible{ outline: 2px solid var(--side); outline-offset: 3px; }
.side-quest > *:not(summary){ font-size: .95rem; }
.side-quest > summary + *{ margin-top: .85rem; }
.side-quest > *:last-child{ margin-bottom: 0; }
/* ---------- protip ---------- */
.protip{
background: var(--ruby-tint);
border: 1px solid var(--ruby-edge);
border-left: 4px solid var(--ruby);
border-radius: 0 4px 4px 0;
padding: .8rem 1rem;
margin: 1.4rem 0;
display: grid;
grid-template-columns: auto 1fr;
gap: 0 .9rem;
align-items: start;
}
.protip.headline{ border-width: 2px; border-left-width: 5px; }
.protip .keys{ display: flex; gap: .25rem; padding-top: .12rem; }
.protip .said{ margin: 0; font-size: .94rem; color: var(--ink-soft); max-width: 62ch; }
.protip .said b{
font-size: .67rem;
font-weight: 700;
letter-spacing: .16em;
text-transform: uppercase;
color: var(--ruby);
display: block;
margin-bottom: .15rem;
}
.protip .said em{ font-style: italic; }
/* ---------- misc blocks ---------- */
.note{
border-left: 3px solid var(--rule-firm);
padding: .1rem 0 .1rem 1rem;
margin: 1.3rem 0;
color: var(--ink-soft);
font-size: .95rem;
}
.note p:last-child{ margin-bottom: 0; }
.callout{
background: var(--paper);
border: 1px solid var(--core-edge);
border-left: 4px solid var(--core);
padding: 1rem 1.15rem;
margin: 1.5rem 0;
border-radius: 0 4px 4px 0;
}
.callout h3{
font-size: .71rem;
font-weight: 700;
letter-spacing: .13em;
text-transform: uppercase;
color: var(--core);
margin: 0 0 .6rem;
}
.callout > *:last-child{ margin-bottom: 0; }
table{
border-collapse: collapse;
width: 100%;
font-size: .91rem;
}
.tablewrap{ overflow-x: auto; margin: 1.3rem 0; }
.tablewrap table{ min-width: 28rem; }
th, td{
text-align: left;
padding: .5rem .8rem .5rem 0;
border-bottom: 1px solid var(--rule);
vertical-align: top;
}
th{
font-size: .67rem;
font-weight: 700;
letter-spacing: .12em;
text-transform: uppercase;
color: var(--ink-faint);
border-bottom-color: var(--rule-firm);
}
td code{ white-space: nowrap; }
ul.plain{ margin: 1rem 0; padding-left: 1.1rem; }
ul.plain li{ margin-bottom: .45rem; max-width: 66ch; }
/* ---------- closing ---------- */
.closing{
margin-top: 3.5rem;
padding-top: 1.8rem;
border-top: 2px solid var(--blue);
}
.closing h2{
font-size: 1.4rem;
font-weight: 700;
color: var(--blue);
margin: 0 0 .8rem;
}
.colophon{
margin-top: 3rem;
padding-top: 1rem;
border-top: 1px solid var(--rule);
font-size: .74rem;
color: var(--ink-faint);
display: flex;
flex-wrap: wrap;
gap: .3rem 1.5rem;
}
/* ---------- theme switch ---------- */
/* Built by sheet.js rather than written into the markup, so it exists
only where it can actually work. It sits in the corner opposite the
logo, above the reading column at every width. */
.themetoggle{
position: fixed;
top: 1rem;
right: 1rem;
z-index: 10;
width: 2.1rem;
height: 2.1rem;
display: grid;
place-items: center;
padding: 0;
font-family: var(--sans);
font-size: 1rem;
line-height: 1;
color: var(--ink-soft);
background: var(--paper);
border: 1px solid var(--rule-firm);
border-radius: 50%;
cursor: pointer;
transition: color .15s ease, border-color .15s ease;
}
.themetoggle:hover{ color: var(--blue); border-color: var(--blue); }
.themetoggle:focus-visible{ outline: 2px solid var(--blue); outline-offset: 2px; }
/* ---------- narrow screens ---------- */
@media (max-width: 40rem){
body{ font-size: 16px; }
.sheet{ padding: 2rem 1.1rem 4rem; }
.part{ grid-template-columns: 1fr; gap: .7rem; }
.rail{ flex-direction: row; align-items: baseline; gap: .85rem; padding-top: 0; }
.part.is-side{ grid-template-columns: 1fr; }
.part.is-side > .body,
.part.is-side > .rail{ grid-column: auto; }
.part.is-side > .rail{ padding-top: 0; text-align: left; align-items: flex-start; }
.part.is-side > .body{ padding: 1rem 1.1rem; }
.rail .num{ font-size: 1.4rem; }
ol.tasks > li{ grid-template-columns: 1.4rem 1fr; gap: 0 .6rem; }
.cmd{ padding-right: .85rem; }
.cmd::after{ display: none; }
}
@media (prefers-reduced-motion: reduce){
*{ animation: none !important; transition: none !important; }
}
/* ---------- print / PDF ---------- */
@media print{
/* `:root[data-theme]` and not a bare `:root`: the dark blocks above are
attribute-qualified, so they outrank a plain :root here and a reader
printing while in dark mode would get a page flooded with ink. Equal
specificity and later in the file, so print wins either way. */
:root, :root[data-theme]{
--paper: #fff; --surface: #F4F7FA; --sunk: #EEF2F6;
--ink: #000; --ink-body: #1a1c1e; --ink-soft: #3a3d40; --ink-faint: #5a5e62;
--rule: #c4ccd3; --rule-firm: #8d959b;
--green: #0F5A2B; --green-tint: #EDF5F0; --green-edge: #86B69B;
--blue: #06537F; --blue-tint: #EDF4F9; --blue-edge: #7FAECB;
--ruby: #96225F; --ruby-tint: #FBEFF5; --ruby-edge: #C98BAE;
--logo-filter: none;
}
body{ font-size: 10.5pt; background: #fff; }
.sheet{ max-width: none; padding: 0; }
.themetoggle{ display: none; }
.cmd{ cursor: auto; }
.cmd::after{ content: none; }
.part{ break-inside: avoid-page; page-break-inside: avoid; }
ol.tasks > li,
.side-quest, .protip, .callout, .cmd, .term{
break-inside: avoid;
page-break-inside: avoid;
}
.side-quest > summary::after{ content: " — optional"; }
/* terminal blocks invert so they do not flood the page with ink */
.term{ background: #F2F5F8; color: #000; border: 1px solid #8d959b; }
.term .p{ color: #06537F; }
.term .o{ color: #44484b; }
.term .e{ color: #8a3410; }
.term .cap{ color: #5a5e62; }
a{ color: #000; text-decoration: underline; }
a[href^="http"]::after{
content: " (" attr(href) ")";
font-size: .8em;
color: #5a5e62;
word-break: break-all;
}
}