Graduately/style.css
pepaskopec-blip 91a848b408 Add a self-updater that installs new releases.
Bake the release tag into the binary so the app knows what it is running;
plain builds keep the "dev" default and never offer an update. The CI
workflow already exported the tag on Linux and macOS, so only the Windows
job needed it.

Read the newest version from the redirect GitHub serves for
/releases/latest, which avoids the REST API and its unauthenticated rate
limit and needs no JSON parser. Networking goes through the curl command
rather than a linked library: curl handles TLS, redirects and proxies, it
ships on all three platforms, and it keeps the bundling scripts free of
another shared library.

An application cannot reliably replace its own files while running, so
every platform follows one shape: unpack beside the current install,
write a script that waits for this process to exit before swapping, then
launch it detached and quit. Staging beside the install keeps the move on
one filesystem and fails early when the location is read-only. The
Windows script copies without mirroring so the progress files kept inside
the install directory survive.

Check quietly a few seconds after startup and offer the result in a
banner above the pages, with a manual check in the settings popover.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-17 15:04:55 +02:00

956 lines
20 KiB
CSS

/* Interface stylesheet.
*
* theme.c prepends the @define-color bindings of the active palette before
* this file is parsed, so nothing here names a literal colour: the rules
* describe shape, spacing and type only, and every palette stays swappable.
*
* Keep to the scales below when adding rules.
*
* type 12 caption | 13 small | 15 body | 17 lead | 20 26 30 headings
* weight 400 body | 500 emphasis | 600 controls and labels | 700 headings
* radius 8 controls | 12 rows and cards | 16 popovers | 999 circles
* motion 120ms colour | 150ms elevation
*
* Fills are flat and elevation is neutral; accent colour marks state, never
* decoration. Rules below the no-scale marker at the end of the file keep a
* fixed font size while the rest grows with the window.
*/
/* ------------------------------------------------------------------ */
/* Window chrome */
/* ------------------------------------------------------------------ */
window {
background-color: @bg_base;
}
headerbar,
headerbar.titlebar,
.titlebar {
background-color: @bg_base;
background-image: none;
color: @fg_text;
border: none;
box-shadow: inset 0 -1px 0 alpha(@fg_text, 0.07);
}
.app-title {
color: @fg_text;
font-size: 13px;
font-weight: 600;
}
windowcontrols button:not(:hover),
windowcontrols button:not(:hover) image {
color: @fg_subtext;
}
button {
background-image: none;
}
tooltip {
background-color: @bg_crust;
border: 1px solid alpha(@fg_text, 0.10);
border-radius: 8px;
color: @fg_text;
}
/* Keyboard focus stays visible on every real control; the map nodes below
* are canvas items and opt out explicitly. */
.btn-primary:focus-visible,
.back-btn:focus-visible,
.settings-btn:focus-visible,
.pill:focus-visible,
.chip:focus-visible,
.group-btn:focus-visible,
.mode-chip:focus-visible,
.theme-card:focus-visible,
.kahoot-opt:focus-visible {
outline-style: solid;
outline-width: 2px;
outline-color: alpha(@accent, 0.65);
outline-offset: 2px;
}
/* ------------------------------------------------------------------ */
/* Type */
/* ------------------------------------------------------------------ */
.heading {
color: @fg_text;
font-size: 30px;
font-weight: 700;
letter-spacing: -0.5px;
}
.roadmap-title {
color: @fg_text;
font-size: 26px;
font-weight: 700;
letter-spacing: -0.3px;
}
.roadmap-sub {
color: @fg_subtext;
font-size: 15px;
}
.welcome-body {
color: @fg_subtext;
font-size: 15px;
}
.welcome-meta {
color: @fg_overlay;
font-size: 12px;
}
/* Small all-caps-ish section markers. */
.settings-label,
.stats-section,
.stats-metric-label,
.notes-kicker,
.group-panel-label {
color: @fg_subtext;
font-size: 12px;
font-weight: 600;
}
.notes-kicker {
color: @accent;
letter-spacing: 0.6px;
}
.hint,
.meaning {
color: @fg_subtext;
font-size: 13px;
}
.ex-prompt {
color: @fg_text;
font-size: 15px;
font-weight: 400;
}
.ex-sub {
color: @fg_subtext;
font-size: 13px;
font-weight: 500;
}
.trans-word {
color: @fg_text;
font-size: 24px;
font-weight: 600;
}
.dlg-speaker {
color: @accent;
font-size: 15px;
font-weight: 600;
}
.number-big {
color: @fg_text;
font-size: 24px;
font-weight: 600;
}
.hm-word {
color: @fg_text;
font-family: monospace;
font-size: 26px;
font-weight: 600;
letter-spacing: 2px;
}
.chain {
color: @fg_text;
font-size: 17px;
font-weight: 500;
letter-spacing: 0.5px;
}
.u3-answer {
color: @success;
font-size: 13px;
font-weight: 600;
}
.feedback-ok {
color: @success;
font-size: 14px;
font-weight: 600;
}
.feedback-err {
color: @error;
font-size: 14px;
font-weight: 600;
}
.feedback-warn {
color: @warning;
font-size: 14px;
font-weight: 600;
}
/* ------------------------------------------------------------------ */
/* Surfaces */
/* ------------------------------------------------------------------ */
.quiz-card,
.notes-card,
.stats-metric,
.stats-unit-row,
.book-card,
.lit-link {
background-color: mix(@bg_base, @bg_surface0, 0.55);
background-image: none;
border: 1px solid alpha(@fg_text, 0.09);
border-radius: 12px;
color: @fg_text;
box-shadow: none;
}
.quiz-card {
padding: 16px 18px;
}
.notes-card {
padding: 22px 26px;
}
.notes-title {
color: @fg_text;
font-size: 20px;
font-weight: 700;
}
.notes-body {
color: @fg_text;
font-size: 15px;
}
.notes-tip {
color: @success;
font-size: 14px;
font-weight: 500;
}
.group-panel,
.sent-group {
background-color: alpha(@fg_text, 0.04);
border: 1px solid alpha(@fg_text, 0.09);
border-radius: 12px;
padding: 12px;
}
.sent-group.ok {
border-color: @success;
}
.sent-group.wrong {
border-color: @error;
}
popover,
popover.background,
.settings-popover {
background-color: @bg_mantle;
color: @fg_text;
border: 1px solid alpha(@fg_text, 0.12);
border-radius: 16px;
box-shadow: 0 12px 32px alpha(@bg_crust, 0.45);
}
popover > contents,
.settings-popover > contents {
background-color: transparent;
border: none;
border-radius: 16px;
box-shadow: none;
padding: 0;
}
/* ------------------------------------------------------------------ */
/* Buttons */
/* ------------------------------------------------------------------ */
.btn-primary {
background-image: none;
background-color: @accent;
color: @on_accent;
font-size: 15px;
font-weight: 600;
padding: 10px 22px;
border: none;
border-radius: 8px;
box-shadow: none;
transition: background-color 120ms ease;
}
.btn-primary:hover {
background-color: shade(@accent, 1.1);
}
.btn-primary:active {
background-color: shade(@accent, 0.92);
}
.btn-primary:disabled {
background-color: alpha(@fg_text, 0.10);
color: @fg_overlay;
}
/* Flat icon buttons in the header bar and above each page. */
.back-btn,
.settings-btn,
.settings-btn:hover,
.settings-btn:active,
.settings-btn:checked,
.settings-btn:focus {
min-width: 32px;
min-height: 32px;
padding: 0;
border-radius: 8px;
background-color: transparent;
background-image: none;
border: none;
box-shadow: none;
color: @fg_subtext;
transition: background-color 120ms ease, color 120ms ease;
}
.back-btn:hover,
.settings-btn:hover {
background-color: alpha(@fg_text, 0.09);
color: @fg_text;
}
.back-btn:active,
.settings-btn:active {
background-color: alpha(@fg_text, 0.15);
color: @fg_text;
}
/* Word / answer chips. */
.pill,
.chip,
.group-btn {
background-image: none;
background-color: mix(@bg_base, @bg_surface0, 0.7);
color: @fg_text;
border: 1px solid alpha(@fg_text, 0.12);
border-radius: 8px;
padding: 7px 14px;
font-size: 14px;
font-weight: 500;
box-shadow: none;
transition: background-color 120ms ease, border-color 120ms ease;
}
.pill:hover,
.chip:hover,
.group-btn:hover {
background-image: none;
background-color: mix(@bg_base, @bg_surface0, 1.0);
border-color: alpha(@accent, 0.7);
}
.pill:active,
.chip:active,
.group-btn:active {
background-image: none;
background-color: @bg_surface1;
}
.pill:checked,
.group-btn:checked {
background-image: none;
background-color: @accent;
border-color: @accent;
color: @on_accent;
font-weight: 600;
}
.pill.ok,
.pill.ok:checked,
.pill.ok:hover {
background-image: none;
background-color: @success;
border-color: @success;
color: @on_accent;
}
.pill.wrong,
.pill.wrong:checked,
.pill.wrong:hover {
background-image: none;
background-color: @error;
border-color: @error;
color: @on_accent;
}
.pill.hm-key {
min-width: 32px;
padding: 5px 6px;
font-size: 13px;
}
.chip {
border-radius: 999px;
}
.chip.ghost-host {
opacity: 0;
}
.group-panel-label {
margin-bottom: 4px;
}
/* Multiple-choice rows read as list items rather than as chips. They are
* outlined instead of filled, so they do not have to be lighter than the card
* in dark mode and darker in light mode. */
.quiz-pill {
background-color: transparent;
border-color: alpha(@fg_text, 0.13);
border-radius: 10px;
padding: 11px 14px;
min-height: 40px;
}
.quiz-pill label {
font-weight: 400;
}
.quiz-pill:checked label {
font-weight: 600;
}
.quiz-heading {
color: @fg_text;
font-size: 17px;
font-weight: 600;
}
.quiz-intro {
color: @fg_subtext;
font-size: 14px;
}
.quiz-q {
color: @fg_text;
font-size: 15px;
font-weight: 600;
}
.quiz-options {
margin-top: 4px;
}
/* Four-colour quiz board: the colours are the mechanic, so they stay, but
* they are flat and the answer state is shown by the border alone. */
.kahoot-opt {
background-image: none;
color: @on_accent;
font-size: 15px;
font-weight: 600;
padding: 14px 16px;
border: 2px solid transparent;
border-radius: 12px;
box-shadow: none;
transition: border-color 120ms ease, opacity 120ms ease;
}
.kahoot-0 {
background-color: @accent;
}
.kahoot-1 {
background-color: @error;
}
.kahoot-2 {
background-color: @warning;
}
.kahoot-3 {
background-color: @success;
}
.kahoot-opt:hover {
border-color: alpha(@fg_text, 0.35);
}
.kahoot-opt.ok {
border-color: @fg_text;
}
.kahoot-opt.wrong {
opacity: 0.4;
}
/* ------------------------------------------------------------------ */
/* Text entry */
/* ------------------------------------------------------------------ */
entry {
background-color: @bg_mantle;
color: @fg_text;
border: 1px solid alpha(@fg_text, 0.14);
border-radius: 8px;
padding: 7px 11px;
font-size: 15px;
caret-color: @accent;
box-shadow: none;
}
entry:focus {
border-color: @accent;
box-shadow: 0 0 0 2px alpha(@accent, 0.22);
}
entry.answer-ok {
border-color: @success;
background-color: alpha(@success, 0.10);
}
entry.answer-wrong {
border-color: @error;
background-color: alpha(@error, 0.10);
}
combobox {
background-color: transparent;
color: @fg_text;
}
combobox button {
background-image: none;
background-color: @bg_mantle;
color: @fg_text;
border: 1px solid alpha(@fg_text, 0.14);
border-radius: 8px;
padding: 4px 10px;
box-shadow: none;
}
combobox button:hover {
background-image: none;
border-color: alpha(@accent, 0.7);
}
combobox.answer-ok button {
border-color: @success;
background-color: alpha(@success, 0.10);
}
combobox.answer-wrong button {
border-color: @error;
background-color: alpha(@error, 0.10);
}
/* ------------------------------------------------------------------ */
/* Progress maps */
/* ------------------------------------------------------------------ */
.unit-node,
.ex-bubble {
background-image: none;
background-color: @node_bg;
color: @fg_text;
border: 2px solid @locked_border;
border-radius: 999px;
padding: 0;
box-shadow: none;
transition: background-color 120ms ease, border-color 120ms ease,
box-shadow 150ms ease;
}
.ex-bubble {
min-width: 64px;
min-height: 64px;
}
.unit-node.done,
.ex-bubble.done {
background-color: @success;
border-color: @success;
color: @on_accent;
}
.unit-node.current {
background-color: @accent;
border-color: @accent;
color: @on_accent;
box-shadow: 0 0 0 4px alpha(@accent, 0.2);
}
.unit-node.locked {
background-color: @locked_bg;
border-color: @locked_border;
color: @fg_overlay;
}
.unit-node.finish {
background-color: transparent;
border: none;
color: @fg_overlay;
}
.unit-node:hover,
.ex-bubble:hover {
border-color: @accent;
}
.unit-node.locked:hover {
border-color: @locked_border;
}
.unit-node.done:hover,
.ex-bubble.done:hover {
border-color: shade(@success, 1.15);
}
.unit-node.current:hover {
box-shadow: 0 0 0 5px alpha(@accent, 0.3);
}
.unit-node:focus,
.unit-node:focus-visible,
.ex-bubble:focus,
.ex-bubble:focus-visible {
outline-style: none;
}
.ex-branch {
border-style: dashed;
border-color: @accent;
}
.ex-branch:hover {
border-style: solid;
}
.unit-number {
font-size: 22px;
font-weight: 700;
}
.bubble-number {
font-size: 20px;
font-weight: 700;
}
.unit-node.done .unit-number,
.unit-node.current .unit-number,
.ex-bubble.done .bubble-number,
.state-icon,
.bubble-icon {
color: @on_accent;
}
.unit-node.locked .unit-number {
color: @fg_overlay;
}
.unit-node.locked .lock-icon {
color: @fg_overlay;
}
/* Node captions sit on top of the rail, so they carry the page background. */
.unit-name,
.ex-label {
color: @fg_text;
font-size: 13px;
font-weight: 500;
}
.unit-name-locked {
color: @fg_overlay;
}
.cz-label {
background-color: @bg_base;
border-radius: 6px;
padding: 2px 6px;
}
/* ------------------------------------------------------------------ */
/* Reading list and literature */
/* ------------------------------------------------------------------ */
.book-card,
.lit-link {
padding: 16px 20px;
transition: background-color 120ms ease, border-color 120ms ease;
}
.book-card:hover,
.lit-link:hover {
background-color: mix(@bg_base, @bg_surface0, 0.9);
border-color: alpha(@accent, 0.6);
}
.book-card:active,
.lit-link:active {
background-color: alpha(@accent, 0.14);
}
.reading-hero {
opacity: 0.18;
}
.book-title {
color: @fg_text;
font-size: 20px;
font-weight: 700;
}
.book-author {
color: @fg_subtext;
font-size: 15px;
font-weight: 500;
}
.book-sub,
.lit-link-sub {
color: @fg_subtext;
font-size: 13px;
}
.lit-link-title {
color: @fg_text;
font-size: 17px;
font-weight: 600;
}
.lit-progress {
color: @accent;
font-size: 13px;
font-weight: 600;
}
.lit-score {
color: @fg_subtext;
font-size: 13px;
font-weight: 500;
}
.lit-prompt {
color: @fg_text;
font-size: 17px;
font-weight: 600;
}
.lit-expl {
color: @fg_text;
font-size: 14px;
background-color: alpha(@success, 0.10);
border-left: 3px solid @success;
border-radius: 6px;
padding: 10px 14px;
}
/* ------------------------------------------------------------------ */
/* Settings popover */
/* ------------------------------------------------------------------ */
.settings-box {
background-color: transparent;
color: @fg_text;
min-width: 292px;
padding: 16px;
}
.settings-title {
color: @fg_text;
font-size: 17px;
font-weight: 700;
}
.settings-seg {
background-color: alpha(@fg_text, 0.06);
border: none;
border-radius: 8px;
padding: 3px;
}
.mode-chip {
background-image: none;
background-color: transparent;
color: @fg_subtext;
border: none;
border-radius: 6px;
padding: 7px 12px;
font-size: 13px;
font-weight: 500;
min-width: 0;
box-shadow: none;
transition: background-color 120ms ease, color 120ms ease;
}
.mode-chip:hover {
background-color: alpha(@fg_text, 0.07);
color: @fg_text;
}
.mode-chip:checked,
.mode-chip:checked:hover {
background-image: none;
background-color: @accent;
color: @on_accent;
font-weight: 600;
}
.theme-grid,
flowboxchild {
background-color: transparent;
border: none;
box-shadow: none;
padding: 0;
margin: 0;
}
.theme-card {
background-image: none;
background-color: transparent;
color: @fg_text;
border: none;
border-radius: 8px;
padding: 6px 4px;
min-width: 0;
box-shadow: none;
transition: background-color 120ms ease;
}
.theme-card:hover,
.theme-card:checked:hover {
background-color: alpha(@fg_text, 0.07);
}
.theme-card:checked {
background-color: alpha(@fg_text, 0.04);
}
.theme-card-name {
color: @fg_subtext;
font-size: 12px;
font-weight: 500;
}
.theme-card:checked .theme-card-name {
color: @fg_text;
font-weight: 600;
}
/* ------------------------------------------------------------------ */
/* Updates */
/* ------------------------------------------------------------------ */
.update-banner {
background-color: alpha(@accent, 0.12);
border-bottom: 1px solid alpha(@accent, 0.3);
padding: 8px 16px;
}
.update-banner-text {
color: @fg_text;
font-size: 13px;
font-weight: 500;
}
.update-banner-btn {
background-image: none;
background-color: @accent;
color: @on_accent;
border: none;
border-radius: 8px;
padding: 5px 14px;
font-size: 13px;
font-weight: 600;
box-shadow: none;
transition: background-color 120ms ease;
}
.update-banner-btn:hover {
background-color: shade(@accent, 1.1);
}
.update-banner-later {
background-image: none;
background-color: transparent;
color: @fg_subtext;
border: none;
border-radius: 8px;
padding: 5px 10px;
font-size: 13px;
font-weight: 500;
box-shadow: none;
}
.update-banner-later:hover {
background-color: alpha(@fg_text, 0.09);
color: @fg_text;
}
.update-status {
color: @fg_subtext;
font-size: 12px;
}
.update-btn {
background-image: none;
background-color: mix(@bg_base, @bg_surface0, 0.7);
color: @fg_text;
border: 1px solid alpha(@fg_text, 0.12);
border-radius: 8px;
padding: 5px 12px;
font-size: 12px;
font-weight: 600;
box-shadow: none;
transition: background-color 120ms ease, border-color 120ms ease;
}
.update-btn:hover {
background-image: none;
background-color: mix(@bg_base, @bg_surface0, 1.0);
border-color: alpha(@accent, 0.7);
}
.update-btn:disabled {
color: @fg_overlay;
}
.update-progress,
.update-progress trough {
min-height: 6px;
min-width: 6px;
border: none;
border-radius: 3px;
padding: 0;
background-color: alpha(@fg_text, 0.11);
}
.update-progress progress {
min-height: 6px;
min-width: 6px;
border: none;
border-radius: 3px;
background-image: none;
background-color: @accent;
}
/* ------------------------------------------------------------------ */
/* Statistics */
/* ------------------------------------------------------------------ */
.stats-page {
padding: 8px 28px 24px 28px;
}
.stats-summary {
margin-top: 10px;
margin-bottom: 16px;
}
.stats-metric {
padding: 14px 16px;
min-width: 120px;
}
.stats-metric-value {
color: @fg_text;
font-size: 24px;
font-weight: 700;
letter-spacing: -0.5px;
}
.stats-metric-label {
margin-top: 2px;
}
.stats-section {
margin-bottom: 8px;
}
.stats-subject {
margin-bottom: 20px;
}
.stats-subject-name {
color: @fg_text;
font-size: 15px;
font-weight: 600;
}
.stats-subject-count {
color: @fg_subtext;
font-size: 13px;
font-weight: 500;
}
.stats-subject-count.locked {
color: @fg_overlay;
}
.stats-units {
margin-top: 8px;
padding-left: 14px;
}
.stats-unit-row {
padding: 13px 14px;
margin-bottom: 6px;
}
.stats-unit-row.locked {
opacity: 0.5;
}
.stats-unit-row.done {
border-color: alpha(@success, 0.5);
}
.stats-unit-index {
color: @fg_overlay;
font-size: 13px;
font-weight: 500;
min-width: 20px;
}
.stats-unit-name {
color: @fg_text;
font-size: 15px;
font-weight: 500;
}
.stats-unit-count {
color: @fg_subtext;
font-size: 13px;
font-weight: 500;
}
.stats-unit-badge {
color: @success;
font-size: 12px;
font-weight: 600;
}
/* One shared bar geometry; only the fill colour differs. */
.stats-overall-bar,
.stats-overall-bar trough,
.stats-subject-bar,
.stats-subject-bar trough,
.stats-unit-bar,
.stats-unit-bar trough {
min-height: 6px;
min-width: 6px;
border: none;
border-radius: 3px;
padding: 0;
background-color: alpha(@fg_text, 0.11);
}
.stats-overall-bar progress,
.stats-subject-bar progress,
.stats-unit-bar progress {
min-height: 6px;
min-width: 6px;
border: none;
border-radius: 3px;
background-image: none;
background-color: @accent;
}
.stats-unit-bar.done progress {
background-color: @success;
}
/* stats_set_fraction() marks bars at zero, whose fill GTK would otherwise
* still paint at its minimum width. */
.stats-overall-bar.empty progress,
.stats-subject-bar.empty progress,
.stats-unit-bar.empty progress {
background-color: transparent;
}
.stats-overall-bar {
margin-bottom: 20px;
}
.stats-subject-bar {
margin-top: 6px;
}
/* no-scale */
/* Rules below keep a fixed size (symbols/icons) even when the window grows. */
.bubble-number.branch-star {
font-size: 20px;
}