diff --git a/app/assets/images/add.svg b/app/assets/images/add.svg
new file mode 100644
index 000000000..1a598b952
--- /dev/null
+++ b/app/assets/images/add.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/assets/images/arrow-left.svg b/app/assets/images/arrow-left.svg
new file mode 100644
index 000000000..261f04a68
--- /dev/null
+++ b/app/assets/images/arrow-left.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/assets/images/bolt.svg b/app/assets/images/bolt.svg
new file mode 100644
index 000000000..78cb2c57d
--- /dev/null
+++ b/app/assets/images/bolt.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/assets/images/chart.svg b/app/assets/images/chart.svg
new file mode 100644
index 000000000..8a42e538e
--- /dev/null
+++ b/app/assets/images/chart.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/assets/images/check.svg b/app/assets/images/check.svg
new file mode 100644
index 000000000..3d70a50ee
--- /dev/null
+++ b/app/assets/images/check.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/assets/images/hourglass.svg b/app/assets/images/hourglass.svg
new file mode 100644
index 000000000..50d90d9c9
--- /dev/null
+++ b/app/assets/images/hourglass.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/assets/images/inbox.svg b/app/assets/images/inbox.svg
new file mode 100644
index 000000000..50252472c
--- /dev/null
+++ b/app/assets/images/inbox.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/assets/images/pencil.svg b/app/assets/images/pencil.svg
new file mode 100644
index 000000000..b8224adbb
--- /dev/null
+++ b/app/assets/images/pencil.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/assets/images/settings.svg b/app/assets/images/settings.svg
new file mode 100644
index 000000000..3237d6970
--- /dev/null
+++ b/app/assets/images/settings.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/assets/images/splat-1.svg b/app/assets/images/splat-1.svg
new file mode 100644
index 000000000..e2097e4f1
--- /dev/null
+++ b/app/assets/images/splat-1.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/assets/images/temperature.svg b/app/assets/images/temperature.svg
new file mode 100644
index 000000000..7f02650ac
--- /dev/null
+++ b/app/assets/images/temperature.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css
new file mode 100644
index 000000000..288b9ab71
--- /dev/null
+++ b/app/assets/stylesheets/application.css
@@ -0,0 +1,15 @@
+/*
+ * This is a manifest file that'll be compiled into application.css, which will include all the files
+ * listed below.
+ *
+ * Any CSS (and SCSS, if configured) file within this directory, lib/assets/stylesheets, or any plugin's
+ * vendor/assets/stylesheets directory can be referenced here using a relative path.
+ *
+ * You're free to add application-wide styles to this file and they'll appear at the bottom of the
+ * compiled file so the styles you add here take precedence over styles defined in any other CSS
+ * files in this directory. Styles in this file should be added after the last require_* statement.
+ * It is generally better to create a new file per style scope.
+ *
+ *= require_tree .
+ *= require_self
+ */
diff --git a/app/assets/stylesheets/avatars.css b/app/assets/stylesheets/avatars.css
new file mode 100644
index 000000000..6aeeba783
--- /dev/null
+++ b/app/assets/stylesheets/avatars.css
@@ -0,0 +1,16 @@
+.avatar {
+ --avatar-border-radius: 50%;
+
+ aspect-ratio: 1;
+ block-size: var(--avatar-size, 2.65em);
+ border-radius: var(--avatar-border-radius);
+ display: grid;
+ inline-size: var(--avatar-size, 2.65em);
+ margin: 0;
+ place-items: center;
+
+ > * {
+ aspect-ratio: 1;
+ grid-area: 1/1;
+ }
+}
diff --git a/app/assets/stylesheets/buttons.css b/app/assets/stylesheets/buttons.css
new file mode 100644
index 000000000..dc89a877c
--- /dev/null
+++ b/app/assets/stylesheets/buttons.css
@@ -0,0 +1,183 @@
+:root {
+ --btn-size: 2.65em;
+}
+
+.btn {
+ --transition: 300ms ease;
+
+ align-items: center;
+ background-color: var(--btn-background, var(--color-bg));
+ border-radius: var(--btn-border-radius, 2em);
+ border: var(--btn-border-size, 1px) solid var(--btn-border-color, var(--color-subtle-dark));
+ color: var(--btn-color, var(--color-ink));
+ cursor: pointer;
+ display: inline-flex;
+ font-size: 1em;
+ font-weight: 600;
+ gap: var(--btn-gap, 0.5em);
+ justify-content: center;
+ padding: var(--btn-padding, 0.5em 1.1em);
+ pointer-events: auto;
+ transition:
+ background-color var( --transition),
+ border var( --transition),
+ color var( --transition),
+ filter var( --transition),
+ opacity var( --transition);
+
+ img {
+ -webkit-touch-callout: none;
+
+ pointer-events: none;
+ user-select: none;
+ }
+
+ /* Default icon styles */
+ &:where(:has(img, svg)) {
+ text-align: start;
+
+ img, svg {
+ block-size: var(--btn-icon-size, 1.3em);
+ inline-size: var(--btn-icon-size, 1.3em);
+ max-inline-size: unset;
+ }
+
+ img:not([class]) {
+ filter: invert(0);
+
+ @media (prefers-color-scheme: dark) {
+ filter: invert(1);
+ }
+ }
+ }
+
+ /* Circle buttons */
+ &.btn--circle,
+ &:where(:has(.for-screen-reader):has(img, svg)) {
+ --btn-border-radius: 50%;
+ --btn-padding: 0;
+
+ aspect-ratio: 1;
+ block-size: var(--btn-size);
+ display: grid;
+ inline-size: var(--btn-size);
+ place-items: center;
+
+ > * {
+ grid-area: 1/1;
+ }
+ }
+
+ /* With radios and checkboxes */
+ &:has(input[type=radio], input[type=checkbox]) {
+ :is(input[type=radio], input[type=checkbox]) {
+ appearance: none;
+ block-size: calc(var(--btn-size) - var(--outline-size));
+ border-radius: var(--btn-border-radius);
+ cursor: pointer;
+ display: flex;
+ inline-size: calc(var(--btn-size) - var(--outline-size));
+ margin: 0;
+ padding: 0;
+ }
+
+ img.checked {
+ display: none;
+ }
+ }
+
+ &:has(input:checked) {
+ --btn-background: var(--color-ink);
+ --btn-color: var(--color-ink-reversed);
+ --outline-color: var(--color-ink);
+
+ img {
+ filter: invert(1);
+ }
+
+ @media (prefers-color-scheme: dark) {
+ img {
+ filter: invert(0);
+ }
+ }
+
+ img.checked {
+ display: block;
+ }
+ }
+
+ &[disabled],
+ &:has([disabled]),
+ [disabled] &[type=submit],
+ &[type=submit]:disabled {
+ cursor: not-allowed;
+ opacity: 0.3;
+ pointer-events: none;
+ }
+
+ @media print {
+ display: none;
+ }
+}
+
+/* Variants */
+.btn--link {
+ --btn-background: var(--color-link);
+ --btn-color: var(--color-ink-reversed);
+ --outline-color: var(--color-link);
+}
+
+.btn--negative {
+ --btn-background: var(--color-negative);
+ --btn-color: var(--color-ink-reversed);
+ --outline-color: var(--color-negative);
+}
+
+.btn--placeholder {
+ pointer-events: none;
+ visibility: hidden;
+}
+
+.btn--plain {
+ --btn-border-radius: 0.5em;
+ --btn-border-size: 0;
+ --btn-icon-size: 100%;
+ --btn-padding: 0;
+ --hover-size: 0;
+}
+
+.btn--positive {
+ --btn-background: var(--color-positive);
+ --btn-color: var(--color-ink-reversed);
+ --outline-color: var(--color-positive);
+}
+
+.btn--reversed {
+ --btn-background: var(--color-ink);
+ --btn-color: var(--color-bg);
+ --outline-color: var(--color-ink);
+}
+
+:is(.btn--link, .btn--negative, .btn--positive, .btn--reversed) {
+ --btn-border-color: var(--color-bg);
+
+ img:not([class]) {
+ filter: invert(1);
+
+ @media (prefers-color-scheme: dark) {
+ filter: invert(0);
+ }
+ }
+}
+
+.btn--small {
+ font-size: 0.8em;
+}
+
+.btn--success {
+ animation: success 1s ease-out;
+
+ img {
+ animation: zoom-fade 300ms ease-out;
+ }
+}
diff --git a/app/assets/stylesheets/colors.css b/app/assets/stylesheets/colors.css
new file mode 100644
index 000000000..7c286dcd2
--- /dev/null
+++ b/app/assets/stylesheets/colors.css
@@ -0,0 +1,65 @@
+:root {
+ /* Named color values */
+ --lch-black: 0% 0 0;
+ --lch-white: 100% 0 0;
+ --lch-gray-light: 96% 0.005 96;
+ --lch-gray: 92% 0.005 96;
+ --lch-gray-dark: 75% 0.005 96;
+ --lch-blue: 54% 0.15 255;
+ --lch-blue-light: 95% 0.03 255;
+ --lch-blue-dark: 80% 0.08 255;
+ --lch-orange: 70% 0.2 44;
+ --lch-red: 51% 0.2 31;
+ --lch-green: 65.59% 0.234 142.49;
+ --lch-green-light: 95% 0.03 142.49;
+ --lch-always-black: 0% 0 0;
+ --lch-always-white: 100% 0 0;
+
+ /* Abstractions */
+ --color-negative: oklch(var(--lch-red));
+ --color-positive: oklch(var(--lch-green));
+ --color-positive-light: oklch(var(--lch-green-light));
+ --color-bg: oklch(var(--lch-white));
+ --color-ink: oklch(var(--lch-black));
+ --color-ink-reversed: oklch(var(--lch-white));
+ --color-link: oklch(var(--lch-blue));
+ --color-subtle-light: oklch(var(--lch-gray-light));
+ --color-subtle: oklch(var(--lch-gray));
+ --color-subtle-dark: oklch(var(--lch-gray-dark));
+ --color-selected: oklch(var(--lch-blue-light));
+ --color-selected-dark: oklch(var(--lch-blue-dark));
+ --color-marker: oklch(var(--lch-orange));
+ --color-always-black: oklch(var(--lch-always-black));
+ --color-always-white: oklch(var(--lch-always-white));
+
+ /* Redefine named color values for dark mode */
+ @media (prefers-color-scheme: dark) {
+ --lch-black: 100% 0 0;
+ --lch-white: 0% 0 0;
+ --lch-gray-light: 25.2% 0 0;
+ --lch-gray: 30.12% 0 0;
+ --lch-gray-dark: 44.95% 0 0;
+ --lch-blue: 72.25% 0.16 248;
+ --lch-blue-light: 28.11% 0.053 248;
+ --lch-blue-dark: 42.25% 0.07 248;
+ --lch-red: 73.8% 0.184 29.18;
+ --lch-green: 75% 0.21 141.89;
+ --lch-green-light: 28.11% 0.02 142.49;
+ }
+}
+
+.colorize--white {
+ filter: invert(1);
+
+ @media (prefers-color-scheme: dark) {
+ filter: invert(0);
+ }
+}
+
+.colorize--black {
+ filter: invert(0);
+
+ @media (prefers-color-scheme: dark) {
+ filter: invert(1);
+ }
+}
diff --git a/app/assets/stylesheets/inputs.css b/app/assets/stylesheets/inputs.css
new file mode 100644
index 000000000..f0a56e4ff
--- /dev/null
+++ b/app/assets/stylesheets/inputs.css
@@ -0,0 +1,181 @@
+/* Text inputs */
+.input {
+ accent-color: var(--input-accent-color, var(--color-ink));
+ background-color: var(--input-background, transparent);
+ border-radius: var(--input-border-radius, 0.5em);
+ border: var(--input-border-size, 1px) solid var(--input-border-color, var(--color-subtle-dark));
+ color: var(--input-color, var(--color-ink));
+ font-size: max(16px, 1em);
+ inline-size: 100%;
+ line-height: 1.2;
+ max-inline-size: 100%;
+ padding: var(--input-padding, 0.5em 0.8em);
+ resize: none;
+
+ &:autofill,
+ &:-webkit-autofill,
+ &:-webkit-autofill:hover,
+ &:-webkit-autofill:focus {
+ -webkit-text-fill-color: var(--color-ink);
+ -webkit-box-shadow: 0 0 0px 1000px var(--color-selected) inset;
+ }
+
+ &:where(:not(:active)):focus {
+ --input-border-color: var(--color-selected-dark);
+ --hover-color: var(--color-selected-dark);
+ --outline-size: 0;
+ --outline-color: transparent;
+
+ filter: var(--hover-filter);
+ box-shadow: 0 0 0 var(--hover-size) var(--hover-color);
+ }
+}
+
+.input--file {
+ border: 0;
+ cursor: pointer;
+ display: grid;
+ inline-size: auto;
+ place-items: center;
+
+ > * {
+ grid-area: 1 / 1;
+ }
+
+ img {
+ border-radius: 0.4em;
+ }
+
+ input[type="file"] {
+ --input-border-color: transparent;
+ --input-border-radius: 8px;
+
+ block-size: 100%;
+ cursor: pointer;
+ font-size: 0;
+ inline-size: 100%;
+ overflow: clip;
+
+ &::file-selector-button {
+ appearance: none;
+ cursor: pointer;
+ opacity: 0;
+ }
+
+ &:focus,
+ &:focus-visible {
+ --input-border-color: var(--color-selected-dark);
+ --input-border-radius: 8px;
+ --input-border-size: 0.15rem;
+ }
+ }
+}
+
+.input--textara {
+ --input-padding: 0.5em;
+
+ line-height: 1.1;
+ min-block-size: calc(4lh + (2 * var(--input-padding)));
+ min-inline-size: 100%;
+ padding-block: var(--input-padding);
+ padding-inline: calc(var(--input-padding) + calc((1lh - 1ex) / 2));
+
+ @supports (field-sizing: content) {
+ field-sizing: content;
+ max-block-size: calc(10lh + (2 * var(--input-padding)));
+ min-block-size: calc(1lh + (2 * var(--input-padding)));
+ }
+}
+
+/* Switches */
+.switch {
+ block-size: 1.75em;
+ border-radius: 2em;
+ display: inline-flex;
+ inline-size: 3em;
+ position: relative;
+
+ &:has(:focus-visible) {
+ .switch__btn {
+ box-shadow:
+ 0 0 0 var(--outline-size) var(--color-bg),
+ 0 0 0 calc(var(--outline-size) * 2) var(--color-ink);
+ }
+ }
+}
+
+.switch__input {
+ block-size: 0;
+ inline-size: 0;
+ opacity: 0.1;
+}
+
+.switch__btn {
+ background-color: var(--color-subtle-dark);
+ border-radius: 2em;
+ cursor: pointer;
+ inset: 0;
+ position: absolute;
+ transition: 150ms ease;
+
+ &::before {
+ background-color: var(--color-ink-reversed);
+ block-size: 1.35em;
+ border-radius: 50%;
+ content: "";
+ inline-size: 1.35em;
+ inset-block-end: 0.2em;
+ inset-inline-start: 0.2em;
+ position: absolute;
+ transition: 150ms ease;
+ }
+
+ .switch__input:disabled + & {
+ background-color: var(--color-subtle-dark) !important;
+ cursor: not-allowed;
+ }
+
+ .switch__input:checked + & {
+ background-color: var(--color-link);
+
+ &::before {
+ transform: translateX(1.2em);
+ }
+ }
+}
+
+/* Containers that act like (and contain) inputs */
+.input--actor {
+ transition: box-shadow 150ms ease, outline-offset 150ms ease;
+
+ &:focus-within {
+ --input-border-color: var(--color-selected-dark);
+ --hover-color: var(--color-selected-dark);
+ --outline-size: 0;
+
+ filter: var(--hover-filter);
+ box-shadow: 0 0 0 var(--hover-size) var(--hover-color);
+ }
+
+ .input {
+ --input-padding: 0;
+ --input-border-radius: 0;
+ --input-background: transparent;
+ --input-border-size: 0;
+ --hover-size: 0;
+ --outline-size: 0;
+ --outline-color: transparent;
+
+ inline-size: 100%;
+ outline: 0;
+ }
+
+ &:has(.input:is(
+ :autofill,
+ :-webkit-autofill,
+ :-webkit-autofill:hover,
+ :-webkit-autofill:focus)) {
+ -webkit-text-fill-color: var(--color-text);
+ -webkit-box-shadow: 0 0 0px 1000px var(--color-selected) inset;
+ }
+}
diff --git a/app/assets/stylesheets/layout.css b/app/assets/stylesheets/layout.css
new file mode 100644
index 000000000..adffe4101
--- /dev/null
+++ b/app/assets/stylesheets/layout.css
@@ -0,0 +1,87 @@
+body {
+ display: grid;
+ grid-template-rows: auto auto 1fr auto var(--block-space);
+}
+
+:where(#main) {
+ container-type: inline-size;
+ font-size: var(--font-medium-responsive);
+ /* inline-size: 67ch; */
+ inline-size: 90vw;
+ margin-inline: auto;
+ max-inline-size: 100vw;
+ padding-block-end: clamp(var(--block-space), 5%, calc(var(--block-space) * 3));
+ padding-inline: clamp(var(--inline-space), 5%, calc(var(--inline-space) * 3));
+ text-align: center;
+
+ @media (max-width: 70ch) {
+ padding-inline: var(--inline-space);
+ }
+
+ @media print {
+ font-size: 11pt;
+ inline-size: unset;
+ line-height: 1.3;
+ margin: 0;
+ orphans: 3;
+ padding: 0;
+ text-align: justify;
+ text-justify: distribute;
+ widows: 2;
+ }
+}
+
+:where(#footer) {
+ max-inline-size: 100vw;
+ view-transition-name: footer;
+
+ > nav {
+ padding-inline: var(--inline-space);
+ view-transition-name: footer-nav;
+ }
+
+ &:has(.new-book-btn) {
+ inset-block-end: 0;
+ pointer-events: none;
+ position: sticky;
+ }
+}
+
+:where(#header) {
+ max-inline-size: 100vw;
+ view-transition-name: header;
+
+ > nav {
+ view-transition-name: nav;
+ }
+
+ > * {
+ align-items: center;
+ display: flex;
+ gap: 1ch;
+ justify-content: center;
+ padding: var(--block-space-half) var(--inline-space);
+ }
+
+ .btn {
+ flex-shrink: 0;
+ }
+}
+
+:where(#toolbar) {
+ display: flex;
+ inset: 0 0 auto;
+ justify-content: center;
+ min-width: 0;
+ overflow: hidden;
+ padding-inline: var(--inline-space);
+ position: sticky;
+ view-transition-name: toolbar;
+ z-index: 1;
+}
+
+:is(#header, #footer, #toolbar) {
+ @media print {
+ display: none;
+ }
+}
diff --git a/app/assets/stylesheets/panels.css b/app/assets/stylesheets/panels.css
new file mode 100644
index 000000000..2ca61bd17
--- /dev/null
+++ b/app/assets/stylesheets/panels.css
@@ -0,0 +1,13 @@
+.panel {
+ background-color: var(--color-bg);
+ border: 1px solid var(--panel-border-color, var(--color-subtle));
+ color: var(--color-ink);
+ border-radius: var(--panel-border-radius, 1em);
+ inline-size: var(--panel-size, 60ch);
+ max-inline-size: 100%;
+ padding: calc(var(--block-space) * 2);
+
+ @media (prefers-color-scheme: dark) {
+ --panel-border-color: var(--color-subtle-dark);
+ }
+}
diff --git a/app/assets/stylesheets/splats.css b/app/assets/stylesheets/splats.css
new file mode 100644
index 000000000..8d69e1c42
--- /dev/null
+++ b/app/assets/stylesheets/splats.css
@@ -0,0 +1,111 @@
+.windshield {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(15vw, 1fr));
+ justify-content: center;
+
+ > * {
+ flex-shrink: 1;
+ }
+}
+
+.splat {
+ --hover-size: 0;
+ --splat-color: red;
+ --splat-rotate: 20deg;
+ --splat-size: 30vw;
+
+ aspect-ratio: 1;
+ block-size: var(--splat-size);
+ color: var(--color-ink-reversed);
+ container-type: inline-size;
+ display: grid;
+ place-items: center;
+ position: relative;
+
+ > * {
+ grid-area: 1/1;
+ }
+}
+
+.splat__date {
+ aspect-ratio: 1;
+ background-color: var(--splat-color);
+ border-radius: 50%;
+ display: flex;
+ font-size: 5cqi;
+ font-weight: 800;
+ inset: 10cqi 5cqi auto auto;
+ padding: 0.5em 1em;
+ place-items: center;
+ position: absolute;
+}
+
+.splat__link {
+ color: var(--color-ink-reversed);
+}
+
+.splat__svg {
+ fill: var(--splat-color);
+ position: absolute;
+ scale: 120%;
+ transform: rotate(var(--splat-rotate));
+ z-index: -1;
+}
+
+.splat__tag {
+ aspect-ratio: 1;
+ background-color: var(--splat-color);
+ border-radius: 50%;
+ display: flex;
+ font-size: 5cqi;
+ font-weight: 800;
+ inset: auto auto 3cqi 3cqi;
+ padding: 0.5em 1em;
+ place-items: center;
+ position: absolute;
+
+ + & {
+ inset: auto auto 0 24cqi;
+ }
+}
+
+.splat__title {
+ font-size: 10cqi;
+ line-height: 1.2;
+ margin: 12cqi 14cqi;
+
+ -webkit-line-clamp: var(--lines, 3);
+ -webkit-box-orient: vertical;
+ display: -webkit-box;
+ overflow: hidden;
+ text-overflow: clip;
+ white-space: normal;
+}
+
+.splats-list {
+ --hover-size: 0;
+
+ a {
+ color: var(--color-txt);
+ text-decoration: none;
+ }
+
+ li {
+ border-radius: 0.6em;
+ list-style: none;
+ padding: 0.5em 1em;
+ transition: background-color 0.2s ease;
+
+ &:hover{
+ background-color: var(--color-selected);
+ }
+ }
+}
+
+.splats-filter {
+ li {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ }
+}
diff --git a/app/assets/stylesheets/utilities.css b/app/assets/stylesheets/utilities.css
new file mode 100644
index 000000000..61dde2d9a
--- /dev/null
+++ b/app/assets/stylesheets/utilities.css
@@ -0,0 +1,240 @@
+:root {
+ --inline-space: 1ch;
+ --inline-space-half: calc(var(--inline-space) / 2);
+ --inline-space-double: calc(var(--inline-space) * 2);
+
+ --block-space: 1rem;
+ --block-space-half: calc(var(--block-space) / 2);
+ --block-space-double: calc(var(--block-space) * 2);
+
+ --font-small-responsive: clamp(0.8rem, 2cqi, 1rem);
+ --font-medium-responsive: clamp(1rem, 2.5cqi, 1.4rem);
+ --font-large-responsive: clamp(1.3rem, 4cqi, 1.8rem);
+ --font-x-large-responsive: clamp(1.8rem, 5cqi, 3.2rem);
+}
+
+/* Text */
+.txt-small { font-size: 0.8rem; }
+.txt-medium { font-size: 1rem; }
+.txt-large { font-size: 1.4rem; }
+.txt-x-large { font-size: 1.8rem; }
+.txt-xx-large { font-size: 2.4rem; }
+
+.txt-small--responsive { font-size: var(--font-small-responsive); }
+.txt-medium--responsive { font-size: var(--font-medium-responsive); }
+.txt-large--responsive { font-size: var(--font-large-responsive); }
+.txt-x-large--responsive { font-size: var(--font-x-large-responsive); }
+
+.txt-align-center { text-align: center; }
+.txt-align-start { text-align: start; }
+.txt-align-end { text-align: end; }
+
+.txt-ink { color: var(--color-ink); }
+.txt-reversed { color: var(--color-ink-reversed); }
+.txt-negative { color: var(--color-negative); }
+.txt-subtle { color: var(--color-subtle-dark); }
+.txt-undecorated { text-decoration: none; }
+.txt-tight-lines { line-height: 1.2; }
+.txt-normal { font-weight: 400; font-style: normal; }
+.txt-nowrap { white-space: nowrap; }
+.txt-uppercase { text-transform: uppercase; }
+
+/* Flexbox and Grid */
+.justify-end { justify-content: end; }
+.justify-start { justify-content: start; }
+.justify-center { justify-content: center; }
+.justify-space-between { justify-content: space-between; }
+
+.align-center { align-items: center; }
+.align-start { align-items: start; }
+.align-end { align-items: end; }
+
+.align-self-start { align-self: start; }
+
+.contain { contain: inline-size; }
+
+.flex { display: flex; }
+.flex-inline { display: inline-flex; }
+.flex-column { flex-direction: column; }
+.flex-wrap { flex-wrap: wrap; }
+
+.flex-item-grow { flex-grow: 1; }
+.flex-item-shrink { flex-shrink: 1; }
+.flex-item-no-shrink { flex-shrink: 0; }
+.flex-item-justify-start { margin-inline-end: auto; }
+.flex-item-justify-end { margin-inline-start: auto; }
+
+.gap {
+ column-gap: var(--column-gap, var(--inline-space));
+ row-gap: var(--row-gap, var(--block-space));
+}
+
+.gap-half {
+ column-gap: var(--inline-space-half);
+ row-gap: var(--block-space-half);
+}
+
+/* Sizing */
+.full-width { inline-size: 100%; }
+.min-width { min-inline-size: 0; }
+.half-width { inline-size: 50%; }
+.max-width { max-inline-size: 100%; }
+.min-content { inline-size: min-content; }
+.max-inline-size { max-inline-size: 100%; }
+
+/* Overflow */
+.overflow-x { overflow-x: auto; scroll-snap-type: x mandatory; scroll-behavior: smooth; }
+.overflow-y { overflow-y: auto; scroll-snap-type: y mandatory; scroll-behavior: smooth; }
+.overflow-clip { text-overflow: clip; white-space: nowrap; overflow: hidden; }
+.overflow-ellipsis { text-overflow: ellipsis; white-space: nowrap; overflow: hidden; }
+
+.overflow-hide-scrollbar::-webkit-scrollbar {
+ @media (pointer: course) {
+ display: none;
+ }
+}
+
+.overflow-line-clamp {
+ -webkit-line-clamp: var(--lines, 2);
+ -webkit-box-orient: vertical;
+ display: -webkit-box;
+ overflow: hidden;
+ text-overflow: clip;
+ white-space: normal;
+}
+
+/* Padding */
+.pad { padding: var(--block-space) var(--inline-space); }
+.pad-double { padding: var(--block-space-double) var(--inline-space-double); }
+
+.pad-block { padding-block: var(--block-space); }
+.pad-block-start { padding-block-start: var(--block-space); }
+.pad-block-end { padding-block-end: var(--block-space); }
+.pad-block-half { padding-block: var(--block-space-half); }
+.pad-block-start-half { padding-block-start: var(--block-space-half); }
+
+.pad-inline { padding-inline: var(--inline-space); }
+.pad-inline-start { padding-inline-start: var(--inline-space); }
+.pad-inline-end { padding-inline-end: var(--inline-space); }
+.pad-inline-half { padding-inline: var(--inline-space-half); }
+.pad-inline-double { padding-inline: var(--inline-space-double); }
+
+.unpad { padding: 0; }
+
+/* Margins */
+.margin { margin: var(--block-space) var(--inline-space); }
+.margin-block { margin-block: var(--block-space); }
+.margin-block-half { margin-block: var(--block-space-half); }
+.margin-block-start { margin-block-start: var(--block-space); }
+.margin-block-start-half { margin-block-start: var(--block-space-half); }
+.margin-block-end { margin-block-end: var(--block-space); }
+.margin-block-end-half { margin-block-end: var(--block-space-half); }
+.margin-block-double { margin-block: var(--block-space-double); }
+.margin-block-end-double { margin-block-end: var(--block-space-double); }
+.margin-block-start-double { margin-block-start: var(--block-space-double); }
+
+.margin-inline { margin-inline: var(--inline-space); }
+.margin-inline-start { margin-inline-start: var(--inline-space); }
+.margin-inline-start-half { margin-inline-start: var(--inline-space-half); }
+.margin-inline-end { margin-inline-end: var(--inline-space); }
+.margin-inline-end-half { margin-inline-end: var(--inline-space-half); }
+.margin-inline-half { margin-inline: var(--inline-space-half); }
+.margin-inline-double { margin-inline: var(--inline-space-double); }
+
+.margin-none { margin: 0; }
+.margin-none-block { margin-block: 0; }
+.margin-none-block-start { margin-block-start: 0; }
+.margin-none-block-end { margin-block-end: 0; }
+
+.margin-none-inline { margin-inline: 0; }
+.margin-none-inline-start { margin-inline-start: 0; }
+.margin-none-inline-end { margin-inline-end: 0; }
+
+.center { margin-inline: auto; }
+.center-block { margin-block: auto; }
+
+/* Position */
+.position-relative { position: relative; }
+.position-sticky { position: sticky; }
+
+/* Fills */
+.fill { background-color: var(--color-bg); }
+.fill-black { background-color: var(--color-ink); }
+.fill-white { background-color: var(--color-ink-reversed); }
+.fill-shade { background-color: var(--color-subtle-light); }
+.fill-selected { background-color: var(--color-selected); }
+.fill-transparent { background-color: transparent; }
+
+.translucent { opacity: var(--opacity, 0.5); }
+
+/* Borders */
+.border { border: var(--border-size, 1px) solid var(--border-color, var(--color-subtle)); }
+.border-top { border-top: var(--border-size, 1px) solid var(--border-color, var(--color-subtle)); }
+.borderless { border: 0; }
+
+/* Border radius */
+.border-radius { border-radius: var(--border-radius, 1em); }
+
+/* Shadows */
+.shadow {
+ box-shadow:
+ 0 0 0 1px oklch(var(--lch-always-black) / 0.02),
+ 0 .2em 1.6em -0.8em oklch(var(--lch-always-black) / 0.2),
+ 0 .4em 2.4em -1em oklch(var(--lch-always-black) / 0.3),
+ 0 .4em .8em -1.2em oklch(var(--lch-always-black) / 0.4),
+ 0 .8em 1.2em -1.6em oklch(var(--lch-always-black) / 0.5),
+ 0 1.2em 1.6em -2em oklch(var(--lch-always-black) / 0.6);
+
+ @media (prefers-color-scheme: dark) {
+ box-shadow:
+ 0 0 0 1px oklch(var(--lch-always-black) / 0.42),
+ 0 .2em 1.6em -0.8em oklch(var(--lch-always-black) / 0.6),
+ 0 .4em 2.4em -1em oklch(var(--lch-always-black) / 0.7),
+ 0 .4em .8em -1.2em oklch(var(--lch-always-black) / 0.8),
+ 0 .8em 1.2em -1.6em oklch(var(--lch-always-black) / 0.9),
+ 0 1.2em 1.6em -2em oklch(var(--lch-always-black) / 1);
+ }
+}
+
+/* Seperators */
+.separator {
+ border-inline-start: 1px solid var(--color-subtle-dark);
+ display: inline-flex;
+ inline-size: 0;
+}
+
+/* Spinners */
+.spinner {
+ position: relative;
+
+ &::before {
+ --mask: no-repeat radial-gradient(#000 68%, #0000 71%);
+ --dot-size: 1.25em;
+
+ -webkit-mask: var(--mask), var(--mask), var(--mask);
+ -webkit-mask-size: 28% 45%;
+ animation: submitting 1.3s infinite linear;
+ aspect-ratio: 8/5;
+ background: currentColor;
+ content: "";
+ inline-size: var(--dot-size);
+ inset: 50% 0.25em;
+ margin-block: calc((var(--dot-size) / 3) * -1);
+ margin-inline: calc((var(--dot-size) / 2) * -1);
+ position: absolute;
+ }
+}
+
+/* Accessibility */
+.for-screen-reader {
+ block-size: 1px;
+ clip-path: inset(50%);
+ inline-size: 1px;
+ overflow: hidden;
+ position: absolute;
+ white-space: nowrap;
+}
+
+/* Visibility */
+[hidden] { display: none; }
+[contents] { display: contents; }