/* Catppuccin Macchiato, matching the palette in the repo README. */ :root { --base: #24273a; --mantle: #1e2030; --crust: #181926; --surface: #363a4f; --overlay: #6e738d; --text: #cad3f5; --subtext: #a5adcb; --lavender: #b7bdf8; --green: #a6da95; --yellow: #eed49f; --peach: #f5a97f; --red: #ed8796; --sky: #91d7e3; } * { box-sizing: border-box; } body { margin: 0 auto; max-width: 1400px; padding: 28px 32px 64px; background: var(--base); color: var(--text); font: 13px / 1.6 "JetBrains Mono", "SF Mono", ui-monospace, monospace; } header { display: flex; align-items: baseline; gap: 14px; margin-bottom: 20px; } h1 { margin: 0; font-size: 15px; font-weight: 500; letter-spacing: 0.01em; } #who, #status { margin: 0; color: var(--overlay); font-size: 12px; } #status.err { color: var(--red); } #controls { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 14px; padding-bottom: 16px; border-bottom: 1px solid var(--surface); } .field { display: flex; flex-direction: column; gap: 4px; min-width: 0; } .actions { display: flex; gap: 8px; margin-left: auto; } label { color: var(--overlay); font-size: 10px; letter-spacing: 0.06em; text-transform: uppercase; } #status { margin: 10px 0 0; min-height: 1.4em; color: var(--subtext); font-size: 12px; } #status.err { color: var(--red); } #status.ok { color: var(--green); } select, button { font: inherit; color: var(--text); background: var(--mantle); border: 1px solid var(--surface); border-radius: 3px; padding: 6px 10px; } select { max-width: 300px; } select:focus-visible, button:focus-visible { outline: 2px solid var(--lavender); outline-offset: 2px; } button { cursor: pointer; transition: border-color 0.12s ease, background 0.12s ease, opacity 0.12s ease; } button:hover:not(:disabled) { border-color: var(--lavender); } button:disabled { opacity: 0.4; cursor: default; } #capture { color: var(--crust); background: var(--lavender); border-color: var(--lavender); } nav { display: flex; gap: 6px; margin: 18px 0; } nav button.on { color: var(--crust); background: var(--lavender); border-color: var(--lavender); } /* ---- tree ---- */ .program { margin-bottom: 26px; } .program > h2 { margin: 0 0 4px; font-size: 13px; font-weight: 500; } .credits { margin-bottom: 12px; color: var(--overlay); font-size: 12px; } details { border-left: 1px solid var(--surface); margin: 2px 0 2px 4px; padding-left: 12px; } summary { cursor: pointer; padding: 3px 0; list-style: none; } summary::-webkit-details-marker { display: none; } summary::before { content: "▸ "; color: var(--overlay); } details[open] > summary::before { content: "▾ "; } .group { padding: 4px 0 4px 14px; border-left: 1px solid var(--surface); margin-left: 4px; } /* A status dot carries completion; the label carries planning. */ .dot { display: inline-block; width: 7px; height: 7px; margin-right: 7px; border-radius: 50%; vertical-align: middle; background: var(--overlay); } .dot.Completed { background: var(--green); } .dot.PartiallyCompleted { background: var(--yellow); } .dot.NotStarted { background: var(--surface); border: 1px solid var(--overlay); } .tag { margin-left: 8px; padding: 1px 6px; border-radius: 3px; background: var(--mantle); color: var(--subtext); font-size: 11px; } .tag.rule { color: var(--peach); } .tag.planned { color: var(--sky); } .courses { margin: 5px 0 2px; color: var(--subtext); font-size: 12px; word-spacing: 2px; } .muted { color: var(--overlay); } /* ---- overlap ---- */ .pool { padding: 10px 0; border-bottom: 1px solid var(--surface); } .pool .sides { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; margin-bottom: 6px; } .pool .side b { display: block; color: var(--lavender); font-weight: 500; } .count { color: var(--green); } .unresolved li { margin-bottom: 5px; color: var(--subtext); } table { border-collapse: collapse; margin-bottom: 18px; } td { padding: 2px 16px 2px 0; } /* ---- schedule ---- */ .week { margin-bottom: 24px; } .clash { margin: 4px 0; padding: 5px 9px; border-left: 2px solid var(--red); background: var(--mantle); color: var(--red); } .toggle { display: inline-flex; align-items: center; gap: 7px; margin-top: 12px; color: var(--subtext); font-size: 11px; text-transform: none; letter-spacing: 0; cursor: pointer; } /* ---- the install page, and the link to it ---- */ #get-bridge { margin-left: auto; padding: 4px 10px; border: 1px solid var(--lavender); border-radius: 3px; color: var(--lavender); font-size: 11px; text-decoration: none; } #get-bridge:hover { background: color-mix(in oklch, var(--lavender) 14%, transparent); } #get-bridge[hidden] { display: none; } .install { max-width: 68ch; } .install h2 { margin: 26px 0 8px; color: var(--subtext); font-size: 12px; font-weight: 500; letter-spacing: 0.04em; text-transform: uppercase; } .install ol, .install ul { margin: 0; padding-left: 20px; } .install li { margin-bottom: 8px; } .install code { padding: 1px 5px; border-radius: 3px; background: var(--mantle); color: var(--sky); } .install a.export { display: inline-block; margin-right: 8px; text-decoration: none; } #version.ok { color: var(--green); } #version.err { color: var(--peach); } /* ---- the semester bar ---- */ .term-bar { display: flex; flex-wrap: wrap; align-items: baseline; gap: 12px; margin-bottom: 4px; } .term-bar h2 { margin: 0; color: var(--text); font-size: 15px; font-weight: 500; letter-spacing: 0.02em; } /* ---- the term's courses, one accordion each ---- */ .here { margin-top: 18px; } details.course { border-left: 2px solid var(--surface); margin-bottom: 2px; } details.course.mine { border-left-color: var(--lavender); } summary.course-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 10px; padding: 5px 10px; border-radius: 3px; cursor: pointer; } summary.course-head:hover { background: var(--mantle); } /* The section the week is holding, which is the only one worth spelling out. */ .slot { color: var(--sky); font-size: 11px; font-variant-numeric: tabular-nums; } .slot.mine { color: var(--lavender); } .slot.none { color: var(--red); } .slot-seats { font-size: 11px; } summary.course-head .release { margin-left: auto; padding: 0 5px; border: 0; background: transparent; color: var(--overlay); font: inherit; line-height: 1; cursor: pointer; } summary.course-head .release:hover { color: var(--red); } summary.course-head .release + .release { margin-left: 0; } /* ---- choosing between the sections ---- */ .options { padding: 2px 0 8px 12px; } .option { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; padding: 3px 8px; border-radius: 3px; cursor: pointer; } .option:hover { background: var(--mantle); } .option input { accent-color: var(--lavender); } .option .num { min-width: 3ch; color: var(--subtext); font-weight: 500; font-variant-numeric: tabular-nums; } .option.in { background: color-mix(in oklch, var(--sky) 8%, transparent); } .option.in .num, .option.in .when { color: var(--sky); } .option.mine { background: color-mix(in oklch, var(--lavender) 14%, transparent); } .option.mine .num, .option.mine .when { color: var(--lavender); } /* Taking this one would cost something else its place. */ .option.clashes .when { color: var(--peach); } .option.clashes:not(.mine) .num { color: var(--peach); } /* Whose choice it is, in two words at the end of the row. */ .mark { margin-left: auto; color: var(--overlay); font-size: 10px; letter-spacing: 0.04em; text-transform: uppercase; } .option.in .mark { color: var(--sky); } .option.mine .mark { color: var(--lavender); } .option.auto .when { color: var(--overlay); font-style: italic; } .term h3 .open-term { margin-left: auto; padding: 1px 7px; border: 1px solid var(--surface); border-radius: 3px; background: transparent; color: var(--subtext); font: inherit; font-size: 10px; cursor: pointer; } .term h3 .open-term:hover { border-color: var(--lavender); color: var(--lavender); } /* ---- the week grid ---- */ .grid { display: flex; gap: 6px; margin-top: 12px; padding-bottom: 4px; overflow-x: auto; } .gutter { position: relative; flex: 0 0 52px; } .hour { position: absolute; right: 6px; color: var(--overlay); font-size: 10px; transform: translateY(20px); } .day { position: relative; flex: 1 0 132px; border-left: 1px solid var(--surface); /* An hour rule every two rows, so a block can be read against the clock without tracking back to the gutter. Offset by the weekday heading. */ background-image: repeating-linear-gradient( to bottom, color-mix(in oklch, var(--surface) 55%, transparent) 0 1px, transparent 1px 40px ); background-position: 0 26px; } .day h3 { margin: 0; padding: 4px 0 0 8px; color: var(--subtext); font-size: 11px; font-weight: 500; } .block { position: absolute; left: 0; cursor: pointer; box-sizing: border-box; width: 100%; margin-right: 4px; overflow: hidden; padding: 3px 6px; border-radius: 3px; border-left: 2px solid var(--surface); background: var(--mantle); font-size: 11px; line-height: 1.3; } /* The three things a block can be, in order of how settled it is. */ .block.pinned { border-left-color: var(--lavender); background: color-mix(in oklch, var(--lavender) 16%, var(--mantle)); } .block.pinned b { color: var(--lavender); } /* The arranger's, until the student says otherwise. */ .block.suggested { border-left-color: var(--sky); background: color-mix(in oklch, var(--sky) 10%, var(--mantle)); } .block.suggested b { color: var(--sky); } /* The section being hovered, where it would fall if it were taken. */ .block.ghost { border: 1px dashed var(--overlay); border-left: 2px dashed var(--green); background: color-mix(in oklch, var(--green) 10%, var(--mantle)); } .block.ghost b { color: var(--green); } .block.ghost.clash { border-left-color: var(--peach); background: color-mix(in oklch, var(--peach) 12%, var(--mantle)); } .block.ghost.clash b { color: var(--peach); } .block.clash { border-left-color: var(--red); background: color-mix(in oklch, var(--red) 13%, var(--mantle)); } .block.clash b { color: var(--red); } .block b { display: block; color: var(--lavender); font-weight: 500; } .block span { color: var(--subtext); } /* A topics course's subject, which is what tells its sections apart. */ .block .topic { display: block; overflow: hidden; color: var(--text); font-style: normal; text-overflow: ellipsis; white-space: nowrap; } .option .topic { color: var(--text); font-weight: 500; } /* ---- course cards ---- */ details.req { margin-bottom: 18px; border-left: 1px solid var(--surface); } details.req > summary { padding: 5px 0; color: var(--subtext); } .code { min-width: 88px; color: var(--text); font-weight: 500; } .title { flex: 1; min-width: 140px; color: var(--subtext); } .cr, .count { color: var(--overlay); font-size: 11px; } .gate { padding: 1px 7px; border-radius: 3px; font-size: 10px; letter-spacing: 0.04em; text-transform: uppercase; } .gate.open { color: var(--green); background: color-mix(in oklch, var(--green) 14%, transparent); } .gate.blocked { color: var(--overlay); background: var(--mantle); } .gate.unknown { color: var(--yellow); background: color-mix(in oklch, var(--yellow) 12%, transparent); } .tag.unlocks { color: var(--sky); cursor: help; } .gate-why { margin: 0 0 4px 10px; color: var(--peach); font-size: 11px; } .when { min-width: 210px; font-variant-numeric: tabular-nums; } .room, .who { color: var(--overlay); font-size: 11px; } .who { flex: 1; min-width: 120px; } .tag.seats { font-variant-numeric: tabular-nums; } .tag.live { border-bottom: 1px dotted currentColor; } .tag.open { color: var(--green); } .tag.full { color: var(--red); } .seats-note { margin: 2px 0 8px; font-size: 11px; } @media (max-width: 720px) { .when { min-width: 0; } .title { min-width: 0; } .grid { margin-left: -16px; margin-right: -16px; padding-left: 16px; } } /* ---- the plan view ---- */ .plan-controls { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; padding-bottom: 14px; border-bottom: 1px solid var(--surface); } .plan-controls input[type="range"] { width: 150px; accent-color: var(--lavender); } .plan-controls .cr { min-width: 2ch; color: var(--lavender); font-variant-numeric: tabular-nums; } .term { margin-bottom: 14px; padding-left: 12px; border-left: 2px solid var(--surface); } .term.summer { border-left-color: var(--yellow); } .term.unplaced { border-left-color: var(--red); } .term h3 { display: flex; align-items: baseline; gap: 10px; margin: 0 0 5px; font-size: 12px; font-weight: 500; } .term h3 .cr { color: var(--overlay); font-size: 11px; } .plan-course { display: flex; gap: 10px; padding: 2px 0; font-size: 12px; } .plan-course b { min-width: 92px; color: var(--text); font-weight: 500; } .plan-course span { color: var(--subtext); } /* ---- editing the plan ---- * * A plan you can argue with has to look like one: a row you can pick up, a * term that says yes or no while you hold it, and a way out of every edit. */ .term.empty { border-left-style: dashed; opacity: 0.65; } .term.dropped { border-left-color: var(--overlay); } .term.drop-ok { border-left-color: var(--green); } .term.drop-bad { border-left-color: var(--red); } .term.over { background: var(--mantle); } .plan-course { align-items: baseline; border-radius: 3px; cursor: grab; } .plan-course:hover { background: var(--mantle); } .plan-course.moved b::before { content: "\00b7 "; color: var(--lavender); } /* Out of the way until the row is under the pointer, so a column of buttons * never competes with the courses it belongs to. */ .plan-course .release { margin-left: auto; padding: 0 5px; border: none; background: none; color: var(--overlay); font-size: 12px; line-height: 1; opacity: 0; } .plan-course:hover .release, .plan-course .release:focus-visible, /* A pin is state, not chrome: the way out of it stays on screen. */ .plan-course.moved .release { opacity: 1; } .plan-course .release:hover { color: var(--red); } /* What the term would become, said while the course is still in the air: * a native tooltip never appears during a drag. */ .term h3 .note { color: var(--lavender); font-size: 11px; font-weight: 400; } .term.drop-bad h3 .note { color: var(--red); } .plan-course.why { padding: 0 0 3px 12px; color: var(--red); font-size: 11px; cursor: default; } .term h3 .add { margin-left: auto; padding: 0 6px; border: none; background: none; color: var(--overlay); font-size: 13px; line-height: 1; } .term h3 .add:hover { color: var(--lavender); } .graph-add { display: flex; align-items: center; gap: 6px; margin-bottom: 8px; font-size: 12px; } .graph-add .add-course, .graph-add .add-term { padding: 2px 6px; font: inherit; color: var(--text); background: var(--mantle); border: 1px solid var(--surface); border-radius: 3px; } .graph-add .add-course { width: 26ch; } .term h3 .add-course { width: 12ch; padding: 1px 4px; font: inherit; font-size: 11px; color: var(--text); background: var(--mantle); border: 1px solid var(--surface); border-radius: 3px; } .plan-controls .regenerate[hidden] { display: none; } /* What a sync would do, shown in full before anything is written. */ .sync:not(:empty) { margin: 0 0 16px; padding: 12px 14px; border: 1px solid var(--surface); border-left: 2px solid var(--lavender); border-radius: 3px; background: var(--mantle); } .sync h3 { margin: 0 0 8px; font-size: 12px; font-weight: 500; } .sync-change { padding: 2px 0; font-size: 12px; color: var(--text); } .sync-change.muted { color: var(--overlay); } /* The reason something failed, which is the one line worth reading. */ .sync .why { margin: 0; color: var(--red); font-size: 12px; } /* Thirty round trips is long enough to need watching. */ .sync .bar { height: 6px; margin: 6px 0 10px; border-radius: 3px; background: var(--surface); overflow: hidden; } .sync .bar span { display: block; height: 100%; min-width: 2px; border-radius: 3px; background: var(--lavender); transition: width 0.2s ease; } .sync-actions { display: flex; gap: 8px; margin-top: 10px; } /* ---- build ---- */ .picker { margin-bottom: 18px; padding-bottom: 16px; border-bottom: 1px solid var(--surface); } .picker h2 { margin: 0 0 8px; font-size: 13px; font-weight: 500; } .picker select { display: block; width: 100%; max-width: 520px; margin-bottom: 10px; } /* Spacing belongs to the row, not to each chip: a margin on the child leaves the gap collapsed wherever two chips sit next to each other in the markup. */ .chips { display: flex; flex-wrap: wrap; align-items: baseline; gap: 8px; margin-bottom: 10px; } .chips .tag { margin: 0; } .tag.on { color: var(--green); } .tag.prog { color: var(--lavender); } .shared { color: var(--green); } .choice { margin-bottom: 18px; padding: 12px 14px; background: var(--mantle); border: 1px solid var(--surface); border-radius: 4px; } .choice h3 { display: flex; align-items: baseline; gap: 8px; margin: 0 0 8px; font-size: 13px; font-weight: 500; } .choice h3 .cr { margin-left: auto; color: var(--overlay); font-size: 11px; } .candidate { display: flex; align-items: baseline; gap: 8px; padding: 3px 0; } .candidate .title { flex: 1; color: var(--subtext); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .candidate.picked { color: var(--green); } /* The pick control is the only affordance in a dense list, so it stays a fixed width: a row that reflows when its label changes is unclickable. */ .pick { width: 26px; padding: 0; color: var(--subtext); line-height: 20px; text-align: center; } .candidate.picked .pick { color: var(--crust); background: var(--green); border-color: var(--green); } /* Cost, ranked by how much it should worry you. */ .tag.free { color: var(--green); } .tag.cheap { color: var(--yellow); } .tag.bad { color: var(--red); } .tag.trying { color: var(--yellow); } /* Sits inside the chip, so it reads as part of it rather than a stray control. */ .tag .drop { margin-left: 6px; padding: 0; color: inherit; background: none; border: none; font-size: 12px; line-height: 1; cursor: pointer; } .tag .drop:hover:not(:disabled) { color: var(--red); } /* A track is a bigger decision than a course, and reads as one. */ .choice.branch { border-left: 2px solid var(--lavender); } .reset { margin-top: 8px; padding: 3px 8px; color: var(--overlay); font-size: 11px; } .shortfall { margin: 0 0 8px; color: var(--yellow); font-size: 11px; } /* Branch option text is a sentence, not a course title, so it wraps. */ .candidate .label { flex: 1; min-width: 0; } /* A met requirement is signalled on its heading, and nowhere else: the pool below it stays fully pickable, because wanting a second literature course is a real thing to want. */ .choice.met { border-left: 2px solid var(--green); } .choice.met h3 { color: var(--green); } .guessed { color: var(--yellow); } .candidate .lands { color: var(--sky); font-size: 11px; } /* ---- map ---- */ /* The picture turns, so the control that turns it sits above the picture. */ .board + .export, .credits + .export { margin-bottom: 12px; } .board { overflow-x: auto; padding-bottom: 8px; } .graph { display: block; } .graph .term-label { fill: var(--overlay); font-size: 10px; letter-spacing: 0.06em; text-transform: uppercase; } .graph .edge { stroke: var(--surface); stroke-width: 1.5; } /* Dimming rather than hiding: the shape of the whole plan stays readable while one chain is traced through it. */ .graph .edge.dim { stroke: var(--surface); opacity: 0.25; } .graph .node rect { fill: var(--mantle); stroke: var(--surface); } .graph .node .code { fill: var(--text); font-size: 11px; font-weight: 500; } .graph .node .sub { fill: var(--overlay); font-size: 9px; } /* The course's own name. Dimmer than its code, because you scan for the code and read the name once you have found it. */ .graph .node .name { fill: var(--subtext); font-size: 9px; } .graph .node.done .name, .graph .node.transfer .name { fill: var(--overlay); } .graph .node .name.why { fill: var(--red); } .graph .node { cursor: default; } .graph .node.dim { opacity: 0.3; } /* History is context, not plan: present and readable, clearly behind you. */ .graph .term-label.past { fill: var(--surface); } .graph .node.done rect { fill: var(--crust); stroke: var(--surface); } .graph .node.done .code { fill: var(--overlay); } .graph .node.running rect { stroke: var(--sky); } /* Credit that arrived rather than being earned: present, and visibly apart. */ .graph .node.transfer rect { fill: var(--crust); stroke: var(--surface); stroke-dasharray: 3 3; } .graph .node.transfer .code { fill: var(--overlay); } /* ---- what an advisor changed by hand ---- */ /* * A substitution is the one line of a degree audit a human wrote on purpose, * so it reads as a sentence rather than as another badge in a row of badges. */ .notes { margin: -6px 0 14px; } .swap { margin: 0 0 5px; color: var(--subtext); font-size: 11px; } .swap b { color: var(--green); font-weight: 500; } .swap .prog-name { color: var(--text); } .swap .carries { color: var(--yellow); } /* ---- load dials ---- */ .dials { display: flex; flex-wrap: wrap; gap: 24px; margin-bottom: 14px; } .dial { display: flex; align-items: center; gap: 8px; color: var(--subtext); font-size: 11px; text-transform: none; letter-spacing: 0; } .dial-label { color: var(--overlay); font-size: 10px; letter-spacing: 0.06em; text-transform: uppercase; } .dial-value { min-width: 2.5em; color: var(--text); text-align: right; } .export { padding: 4px 10px; font-size: 11px; } /* A light term is a thing to fix with one more course, not a thing to hide. */ .graph .term-label.short { fill: var(--yellow); } /* Credits, not courses: a gate with no line drawn to it. */ .graph .sub.standing { fill: var(--peach); } /* A link the catalog implies and the requisite table never states: the two * halves of a sequence. Dashed, because it is a different kind of claim. */ .graph .edge.sequence { stroke-dasharray: 3 3; } /* Tracing a chain: what a course waits on, and what waits on it. * * Two colours rather than one highlight, because they are two different * answers. Peach is behind you — work to do before this. Sky is ahead — * work stuck behind this until it is done. */ .graph .node.before rect { stroke: var(--peach); } .graph .node.after rect { stroke: var(--sky); } .graph .node.focus rect { stroke: var(--lavender); stroke-width: 2; } .graph .edge.before { stroke: var(--peach); } .graph .edge.after { stroke: var(--sky); } /* A course being carried to another term, and the term it would land in. */ .graph .node.moved rect { stroke: var(--lavender); } .graph .node.clashes rect { stroke: var(--red); } .graph .node.dragging { cursor: grabbing; } .graph .node.dragging rect { filter: drop-shadow(0 3px 6px rgb(0 0 0 / 45%)); } .graph .band { fill: var(--surface); opacity: 0.35; pointer-events: none; } .graph .over-band { fill: none; stroke: var(--red); stroke-width: 1.5; stroke-dasharray: 6 4; pointer-events: none; } .graph .term-label.over { fill: var(--red); }