diff --git a/.github/workflows/lint-frontend.yaml b/.github/workflows/lint-frontend.yaml
index b816941e..ed106d6a 100644
--- a/.github/workflows/lint-frontend.yaml
+++ b/.github/workflows/lint-frontend.yaml
@@ -27,7 +27,7 @@ jobs:
# See .stylelintignore for files that are not linted.
- name: Run stylelint
run: >
- npx stylelint bookwyrm/static/css/*.scss \
+ npx stylelint bookwyrm/static/css/*.scss bookwyrm/static/css/bookwyrm/**/*.scss \
--config dev-tools/.stylelintrc.js
# See .eslintignore for files that are not linted.
diff --git a/bookwyrm/settings.py b/bookwyrm/settings.py
index 5b5d7702..3747e30a 100644
--- a/bookwyrm/settings.py
+++ b/bookwyrm/settings.py
@@ -11,7 +11,7 @@ from django.utils.translation import gettext_lazy as _
env = Env()
env.read_env()
DOMAIN = env("DOMAIN")
-VERSION = "0.3.0"
+VERSION = "0.3.1"
RELEASE_API = env(
"RELEASE_API",
@@ -21,7 +21,7 @@ RELEASE_API = env(
PAGE_LENGTH = env("PAGE_LENGTH", 15)
DEFAULT_LANGUAGE = env("DEFAULT_LANGUAGE", "English")
-JS_CACHE = "7eb9174b"
+JS_CACHE = "a60e5a55"
# email
EMAIL_BACKEND = env("EMAIL_BACKEND", "django.core.mail.backends.smtp.EmailBackend")
diff --git a/bookwyrm/static/css/bookwyrm.scss b/bookwyrm/static/css/bookwyrm.scss
index 77263690..6b5e7e6b 100644
--- a/bookwyrm/static/css/bookwyrm.scss
+++ b/bookwyrm/static/css/bookwyrm.scss
@@ -1,1488 +1,7 @@
@charset "utf-8";
@import "instance-settings";
+@import "themes/light.scss";
@import "vendor/bulma/bulma.sass";
@import "vendor/icons.css";
-
-html {
- scroll-behavior: smooth;
-}
-
-body {
- min-height: 100vh;
- display: flex;
- flex-direction: column;
-}
-
-button {
- border: none;
- margin: 0;
- padding: 0;
- width: auto;
- overflow: visible;
- background: transparent;
-
- /* inherit font, color & alignment from ancestor */
- color: inherit;
- font: inherit;
- text-align: inherit;
-
- /* Normalize `line-height`. Cannot be changed from `normal` in Firefox 4+. */
- line-height: normal;
-
- /* Corrects font smoothing for webkit */
- -webkit-font-smoothing: inherit;
- -moz-osx-font-smoothing: inherit;
-
- /* Corrects inability to style clickable `input` types in iOS */
- -webkit-appearance: none;
-
- /* Generalizes pointer cursor */
- cursor: pointer;
-}
-
-button::-moz-focus-inner {
- /* Remove excess padding and border in Firefox 4+ */
- border: 0;
- padding: 0;
-}
-
-/* Better accessibility for keyboard users */
-*:focus-visible {
- outline-style: auto !important;
-}
-
-.image {
- overflow: hidden;
-}
-
-.navbar .logo {
- max-height: 50px;
-}
-
-.card {
- overflow: visible;
-}
-
-.card.has-border {
- border: 1px solid #eee;
-}
-
-.scroll-x {
- overflow: hidden;
- overflow-x: auto;
-}
-
-.modal-card {
- pointer-events: none;
-}
-
-.modal-card > * {
- pointer-events: all;
-}
-
-/* stylelint-disable no-descending-specificity */
-.modal-card:focus {
- outline-style: auto;
-}
-
-.modal-card:focus:not(:focus-visible) {
- outline-style: initial;
-}
-
-.modal-card:focus-visible {
- outline-style: auto;
-}
-/* stylelint-enable no-descending-specificity */
-
-.modal-card.is-fullwidth {
- min-width: 75% !important;
-}
-
-@media only screen and (min-width: 769px) {
- .modal-card.is-thin {
- width: 350px !important;
- }
-}
-
-.modal-card-body {
- max-height: 70vh;
-}
-
-.clip-text {
- max-height: 35em;
- overflow: hidden;
-}
-
-/** Utilities not covered by Bulma
- ******************************************************************************/
-
-@media only screen and (max-width: 768px) {
- .is-sr-only-mobile {
- border: none !important;
- clip: rect(0, 0, 0, 0) !important;
- height: 0.01em !important;
- overflow: hidden !important;
- padding: 0 !important;
- position: absolute !important;
- white-space: nowrap !important;
- width: 0.01em !important;
- }
-
- .m-0-mobile {
- margin: 0 !important;
- }
-
- .card-footer.is-stacked-mobile {
- flex-direction: column;
- }
-
- .card-footer.is-stacked-mobile .card-footer-item:not(:last-child) {
- border-bottom: 1px solid #ededed;
- border-right: 0;
- }
-
- .is-flex-direction-row-mobile {
- flex-direction: row !important;
- }
-
- .is-flex-direction-column-mobile {
- flex-direction: column !important;
- }
-}
-
-.tag.is-small {
- height: auto;
-}
-
-.button.is-transparent {
- background-color: transparent;
-}
-
-.card.is-stretchable {
- display: flex;
- flex-direction: column;
- height: 100%;
-}
-
-.card.is-stretchable .card-content {
- flex-grow: 1;
-}
-
-.preserve-whitespace p {
- white-space: pre-wrap !important;
-}
-
-.display-inline p {
- display: inline !important;
-}
-
-button .button-invisible-overlay {
- position: absolute;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- padding: 1rem;
- box-sizing: border-box;
- display: flex;
- align-items: center;
- flex-direction: column;
- justify-content: center;
- background: rgba(0, 0, 0, 0.66);
- color: white;
- opacity: 0;
- transition: opacity 0.2s ease;
-}
-
-button:hover .button-invisible-overlay,
-button:active .button-invisible-overlay,
-button:focus-visible .button-invisible-overlay {
- opacity: 1;
-}
-
-/** File input styles
- ******************************************************************************/
-
-input[type="file"]::file-selector-button {
- -moz-appearance: none;
- -webkit-appearance: none;
- background-color: #fff;
- border-radius: 4px;
- border: 1px solid #dbdbdb;
- box-shadow: none;
- color: #363636;
- cursor: pointer;
- font-size: 1rem;
- height: 2.5em;
- justify-content: center;
- line-height: 1.5;
- padding-bottom: calc(0.5em - 1px);
- padding-left: 1em;
- padding-right: 1em;
- padding-top: calc(0.5em - 1px);
- text-align: center;
- white-space: nowrap;
-}
-
-input[type="file"]::file-selector-button:hover {
- border-color: #b5b5b5;
- color: #363636;
-}
-
-/** General `details` element styles
- ******************************************************************************/
-
-details summary {
- cursor: pointer;
-}
-
-summary::-webkit-details-marker {
- display: none;
-}
-
-details summary::marker {
- content: none;
-}
-
-details.detail-pinned-button summary {
- position: absolute;
- right: 0;
-}
-
-details.detail-pinned-button form {
- float: left;
- width: 100%;
- margin-top: 1em;
-}
-
-/** Dropdown w/ Details element
- ******************************************************************************/
-
-details.dropdown[open] summary.dropdown-trigger::before {
- content: "";
- position: fixed;
- top: 0;
- bottom: 0;
- left: 0;
- right: 0;
-}
-
-details.dropdown .dropdown-menu {
- display: block !important;
-}
-
-details.dropdown .dropdown-menu button {
- /* Fix weird Safari defaults */
- box-sizing: border-box;
-}
-
-details.dropdown .dropdown-menu button:focus-visible,
-details.dropdown .dropdown-menu a:focus-visible {
- outline-style: auto;
- outline-offset: -2px;
-}
-
-@media only screen and (max-width: 768px) {
- details.dropdown[open] summary.dropdown-trigger::before {
- background-color: rgba(0, 0, 0, 0.5);
- z-index: 30;
- }
-
- details .dropdown-menu {
- position: fixed;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- display: flex !important;
- align-items: center;
- justify-content: center;
- pointer-events: none;
- z-index: 100;
- }
-
- details .dropdown-menu > * {
- pointer-events: all;
- }
-}
-
-/** Bookwyrm Tabs
- ******************************************************************************/
-
-.bw-tabs {
- -webkit-overflow-scrolling: touch;
- -webkit-touch-callout: none;
- position: relative;
- align-items: center;
- display: flex;
- font-size: 1rem;
- justify-content: flex-start;
- overflow-x: auto;
- overflow-y: hidden;
- user-select: none;
- white-space: nowrap;
-}
-
-.bw-tabs::before {
- border-bottom-color: #dbdbdb;
- border-bottom-style: solid;
- border-bottom-width: 1px;
- bottom: 0;
- content: "";
- position: absolute;
- width: 100%;
-}
-
-.bw-tabs:not(:last-child) {
- margin-bottom: 1.5rem;
-}
-
-.bw-tabs a {
- align-items: center;
- border-bottom-color: #dbdbdb;
- border-bottom-style: solid;
- border-bottom-width: 1px;
- color: #4a4a4a;
- display: flex;
- justify-content: center;
- margin-bottom: -1px;
- padding: 0.5em 1em;
- position: relative;
-}
-
-.bw-tabs a:hover {
- border-bottom-color: transparent;
- color: #363636;
-}
-
-.bw-tabs a.is-active {
- border-bottom-color: transparent;
- color: #3273dc;
-}
-
-.bw-tabs.is-left {
- padding-right: 0.75em;
-}
-
-.bw-tabs.is-center {
- flex: none;
- justify-content: center;
- padding-left: 0.75em;
- padding-right: 0.75em;
-}
-
-.bw-tabs.is-right {
- justify-content: flex-end;
- padding-left: 0.75em;
-}
-
-.bw-tabs .icon:first-child {
- margin-right: 0.5em;
-}
-
-.bw-tabs .icon:last-child {
- margin-left: 0.5em;
-}
-
-.bw-tabs.is-centered {
- justify-content: center;
-}
-
-.bw-tabs.is-boxed a {
- border: 1px solid transparent;
- border-radius: 4px 4px 0 0;
-}
-
-.bw-tabs.is-boxed a:hover {
- background-color: #f5f5f5;
- border-bottom-color: #dbdbdb;
-}
-
-.bw-tabs.is-boxed a.is-active {
- background-color: #fff;
- border-color: #dbdbdb;
- border-bottom-color: #fff !important;
-}
-
-.bw-tabs.is-fullwidth a {
- flex-grow: 1;
- flex-shrink: 0;
-}
-
-.bw-tabs.is-toggle a {
- border-color: #dbdbdb;
- border-style: solid;
- border-width: 1px;
- margin-bottom: 0;
- position: relative;
-}
-
-.bw-tabs.is-toggle a:hover {
- background-color: #f5f5f5;
- border-color: #b5b5b5;
- z-index: 2;
-}
-
-.bw-tabs.is-toggle a + a {
- margin-left: -1px;
-}
-
-.bw-tabs.is-toggle a:first-child {
- border-top-left-radius: 4px;
- border-bottom-left-radius: 4px;
-}
-
-.bw-tabs.is-toggle a:last-child {
- border-top-right-radius: 4px;
- border-bottom-right-radius: 4px;
-}
-
-.bw-tabs.is-toggle a.is-active {
- background-color: #3273dc;
- border-color: #3273dc;
- color: #fff;
- z-index: 1;
-}
-
-.bw-tabs.is-toggle {
- border-bottom: none;
-}
-
-.bw-tabs.is-toggle.is-toggle-rounded a:first-child {
- border-bottom-left-radius: 290486px;
- border-top-left-radius: 290486px;
- padding-left: 1.25em;
-}
-
-.bw-tabs.is-toggle.is-toggle-rounded a:last-child {
- border-bottom-right-radius: 290486px;
- border-top-right-radius: 290486px;
- padding-right: 1.25em;
-}
-
-.bw-tabs.is-small {
- font-size: 0.75rem;
-}
-
-.bw-tabs.is-medium {
- font-size: 1.25rem;
-}
-
-.bw-tabs.is-large {
- font-size: 1.5rem;
-}
-
-.bw-tabs.has-aside-text a {
- margin-top: 1.5rem;
-}
-
-.bw-tabs a .aside-text {
- position: absolute;
- top: calc(-0.75rem - 0.75rem);
- left: 0;
- color: #4a4a4a;
-}
-
-/** Details panel
- ******************************************************************************/
-
-details.details-panel {
- box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.1);
- transition: box-shadow 0.2s ease;
- padding: 0.75rem;
-}
-
-details[open].details-panel,
-details.details-panel:hover {
- box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.2);
-}
-
-details.details-panel summary {
- position: relative;
-}
-
-details summary .details-close {
- position: absolute;
- right: 0;
- top: 0;
- transform: rotate(45deg);
- transition: transform 0.2s ease;
-}
-
-details[open] summary .details-close {
- transform: rotate(0deg);
-}
-
-@media only screen and (min-width: 769px) {
- .details-panel .filters-field:not(:last-child) {
- border-right: 1px solid rgba(0, 0, 0, 0.1);
- margin-top: 0.75rem;
- margin-bottom: 0.75rem;
- padding-top: 0.25rem;
- padding-bottom: 0.25rem;
- }
-}
-
-/** Shelving
- ******************************************************************************/
-
-/** @todo Replace icons with SVG symbols.
- @see https://www.youtube.com/watch?v=9xXBYcWgCHA */
-.shelf-option:disabled > *::after {
- font-family: icomoon; /* stylelint-disable font-family-no-missing-generic-family-keyword */
- content: "\e919"; /* icon-check */
- margin-left: 0.5em;
-}
-
-/** Toggles
- ******************************************************************************/
-
-.toggle-button[aria-pressed="true"],
-.toggle-button[aria-pressed="true"]:hover {
- background-color: hsl(171deg, 100%, 41%);
- color: white;
-}
-
-.hide-active[aria-pressed="true"],
-.hide-inactive[aria-pressed="false"] {
- display: none;
-}
-
-.transition-x.is-hidden,
-.transition-y.is-hidden {
- display: block !important;
- visibility: hidden !important;
- height: 0 !important;
- width: 0 !important;
- margin: 0 !important;
- padding: 0 !important;
- overflow: auto;
-}
-
-.transition-x,
-.transition-y {
- transition-duration: 0.5s;
- transition-timing-function: ease;
-}
-
-.transition-x {
- transition-property: width, margin-left, margin-right, padding-left, padding-right;
-}
-
-.transition-y {
- transition-property: height, margin-top, margin-bottom, padding-top, padding-bottom;
-}
-
-@media (prefers-reduced-motion: reduce) {
- .transition-x,
- .transition-y {
- transition-duration: 0.001ms !important;
- }
-}
-
-/** Stars
- ******************************************************************************/
-
-.stars {
- white-space: nowrap;
-}
-
-/** Stars in a review form
- *
- * Specificity makes hovering taking over checked inputs.
- *
- * \e9d9: filled star
- * \e9d7: empty star;
- * -------------------------------------------------------------------------- */
-
-.form-rate-stars {
- width: max-content;
-}
-
-/* All stars are visually filled by default. */
-.form-rate-stars .icon::before {
- content: "\e9d9"; /* icon-star-full */
-}
-
-/* Icons directly following half star inputs are marked as half */
-.form-rate-stars input.half:checked ~ .icon::before {
- content: "\e9d8"; /* icon-star-half */
-}
-
-/* stylelint-disable no-descending-specificity */
-.form-rate-stars input.half:checked + input + .icon:hover::before {
- content: "\e9d8" !important; /* icon-star-half */
-}
-
-/* Icons directly following half check inputs that follow the checked input are emptied. */
-.form-rate-stars input.half:checked + input + .icon ~ .icon::before {
- content: "\e9d7"; /* icon-star-empty */
-}
-
-/* Icons directly following inputs that follow the checked input are emptied. */
-.form-rate-stars input:checked ~ input + .icon::before {
- content: "\e9d7"; /* icon-star-empty */
-}
-
-/* When a label is hovered, repeat the fill-all-then-empty-following pattern. */
-.form-rate-stars:hover .icon.icon::before {
- content: "\e9d9" !important; /* icon-star-full */
-}
-
-.form-rate-stars .icon:hover ~ .icon::before {
- content: "\e9d7" !important; /* icon-star-empty */
-}
-
-/** Book covers
- *
- * - .is-cover gives the behaviour of the cover and its surrounding. (optional)
- * - .cover-container gives the dimensions and position (for borders, image and other elements).
- * - .book-cover is positioned and sized based on its container.
- *
- * To have the cover within specific dimensions, specify a width or height for
- * standard bulma’s named breapoints:
- *
- * `is-(w|h)-(auto|xs|s|m|l|xl|xxl)[-(mobile|tablet|desktop)]`
- *
- * The cover will be centered horizontally and vertically within those dimensions.
- *
- * When using `.column.is-N`, add `.is-w-auto` to the container so that the flex
- * calculations are not biased by the default `max-content`.
- ******************************************************************************/
-
-.column.is-cover {
- flex-grow: 0 !important;
-}
-
-.column.is-cover,
-.column.is-cover + .column {
- flex-basis: auto !important;
-}
-
-.cover-container {
- display: flex;
- justify-content: center;
- align-items: center;
- position: relative;
- width: max-content;
- max-width: 100%;
- overflow: hidden;
-}
-
-/* Book cover
- * -------------------------------------------------------------------------- */
-
-.book-cover {
- display: block;
- max-width: 100%;
- max-height: 100%;
-
- /* Useful when stretching under-sized images. */
- image-rendering: optimizequality;
- image-rendering: smooth;
-}
-
-/* Cover caption
- * -------------------------------------------------------------------------- */
-
-.no-cover .cover-caption {
- position: absolute;
- top: 0;
- right: 0;
- bottom: 0;
- left: 0;
- padding: 0.5em;
- font-size: 0.75em;
- color: white;
- background-color: #002549;
- display: flex;
- align-items: center;
- justify-content: center;
- flex-direction: column;
- gap: 1em;
- white-space: initial;
- text-align: center;
-}
-
-/** Avatars
- ******************************************************************************/
-
-.avatar {
- vertical-align: middle;
- display: inline;
-}
-
-/** Statuses: Quotes
- *
- * \e906: icon-quote-open
- * \e905: icon-quote-close
- *
- * The `content` class on the blockquote allows to apply styles to markdown
- * generated HTML in the quote: https://bulma.io/documentation/elements/content/
- *
- * ```html
- *
- *
- * User generated quote in markdown…
- *
- *
- *
— Book Title by Author
- *
- * ```
- ******************************************************************************/
-
-.quote > blockquote {
- position: relative;
- padding-left: 2em;
-}
-
-.quote > blockquote::before,
-.quote > blockquote::after {
- font-family: icomoon;
- position: absolute;
-}
-
-.quote > blockquote::before {
- content: "\e907"; /* icon-quote-open */
- top: 0;
- left: 0;
-}
-
-.quote > blockquote::after {
- content: "\e906"; /* icon-quote-close */
- right: 0;
-}
-
-/** Animations and transitions
- ******************************************************************************/
-
-@keyframes turning {
- from { transform: rotateZ(0deg); }
- to { transform: rotateZ(360deg); }
-}
-
-.is-processing .icon-spinner::before {
- animation: turning 1.5s infinite linear;
-}
-
-.icon-spinner {
- display: none;
-}
-
-.is-processing .icon-spinner {
- display: flex;
-}
-
-@media (prefers-reduced-motion: reduce) {
- .is-processing .icon::before {
- transition-duration: 0.001ms !important;
- }
-}
-
-/** Transient notification
- ******************************************************************************/
-
-#live-messages {
- position: fixed;
- bottom: 1em;
- right: 1em;
-}
-
-/** Tooltips
- ******************************************************************************/
-
-.tooltip {
- width: 100%;
-}
-
-/** States
- ******************************************************************************/
-
-/* "disabled" for non-buttons */
-
-.is-disabled {
- background-color: #dbdbdb;
- border-color: #dbdbdb;
- box-shadow: none;
- color: #7a7a7a;
- opacity: 0.5;
- cursor: not-allowed;
-}
-
-/* Book preview table
- ******************************************************************************/
-
-.book-preview td {
- vertical-align: middle;
-}
-
-@media only screen and (max-width: 768px) {
- table.is-mobile,
- table.is-mobile tbody {
- display: block;
- }
-
- table.is-mobile tr {
- display: flex;
- flex-wrap: wrap;
- justify-content: space-between;
- border-top: 1px solid #dbdbdb;
- }
-
- table.is-mobile td {
- display: block;
- box-sizing: border-box;
- flex: 1 0 100%;
- order: 2;
- border-bottom: 0;
- }
-
- table.is-mobile td.book-preview-top-row {
- order: 1;
- flex-basis: auto;
- }
-
- table.is-mobile td[data-title]:not(:empty)::before {
- content: attr(data-title);
- display: block;
- font-size: 0.75em;
- font-weight: bold;
- }
-
- table.is-mobile td:empty {
- padding: 0;
- }
-
- table.is-mobile th,
- table.is-mobile thead {
- display: none;
- }
-}
-
-/* Book list
- ******************************************************************************/
-
-ol.ordered-list {
- list-style: none;
- counter-reset: list-counter;
-}
-
-ol.ordered-list li {
- counter-increment: list-counter;
-}
-
-ol.ordered-list li::before {
- content: counter(list-counter);
- position: absolute;
- left: -20px;
- width: 20px;
- height: 24px;
- background-color: #fff;
- border: 1px solid #dbdbdb;
- border-right: 0;
- border-top-left-radius: 2px;
- border-top-right-radius: 2px;
- display: flex;
- justify-content: center;
- align-items: center;
- color: #888;
- font-size: 0.8em;
- font-weight: bold;
-}
-
-@media only screen and (max-width: 768px) {
- ol.ordered-list li::before {
- left: 0;
- z-index: 1;
- border: 0;
- border-right: 1px solid #dbdbdb;
- border-bottom: 1px solid #dbdbdb;
- border-radius: 0;
- border-bottom-right-radius: 2px;
- }
-}
-
-.overflow-wrap-anywhere {
- overflow-wrap: anywhere;
- min-width: 10em;
-}
-
-/* Threads
- ******************************************************************************/
-
-.thread .is-main .card {
- box-shadow: 0 0.5em 1em -0.125em rgba(50, 115, 220, 0.35), 0 0 0 1px rgba(50, 115, 220, 0.02);
-}
-
-.thread::after {
- content: "";
- position: absolute;
- z-index: -1;
- top: 0;
- bottom: 0;
- left: 2.5em;
- border-left: 2px solid #e0e0e0;
-}
-
-/* Breadcrumbs
- ******************************************************************************/
-
-.breadcrumb li:first-child * {
- padding-left: 0;
-}
-
-.breadcrumb li > * {
- align-items: center;
- display: flex;
- justify-content: center;
- padding: 0 0.75em;
-}
-
-/* Notifications page
- ******************************************************************************/
-
-.notification a.icon {
- text-decoration: none !important;
-}
-
-/* Breadcrumbs
- ******************************************************************************/
-
-.books-grid {
- display: grid;
- grid-template-columns: repeat(2, 1fr);
- gap: 1rem;
- align-items: end;
- justify-items: stretch;
-}
-
-.books-grid > .is-big {
- grid-column: span 2;
- grid-row: span 2;
- justify-self: stretch;
-}
-
-.books-grid .book-cover {
- width: 100%;
-}
-
-.books-grid .book-title {
- --height-basis: 1.35rem;
-
- display: block;
- margin-top: 0.5rem;
- line-height: var(--height-basis);
- min-height: calc(2 * var(--height-basis));
-}
-
-@media only screen and (min-width: 769px) {
- .books-grid {
- gap: 1.5rem;
- grid-template-columns: repeat(auto-fill, minmax(8em, 1fr));
- }
-}
-
-/* Copy
- ******************************************************************************/
-
-.horizontal-copy {
- display: flex;
- flex-direction: row;
- align-items: center;
- gap: 0.75rem;
-}
-
-.horizontal-copy textarea {
- min-width: initial;
- white-space: nowrap;
-}
-
-.horizontal-copy button {
- align-self: stretch;
- height: unset;
-}
-
-.vertical-copy {
- display: flex;
- flex-direction: column;
- align-items: stretch;
- gap: 0.75rem;
-}
-
-.vertical-copy button {
- width: 100%;
-}
-
-/* Dimensions
- * @todo These could be in rem.
- ******************************************************************************/
-
-.is-32x32 {
- min-width: 32px !important;
- min-height: 32px !important;
-}
-
-.is-96x96 {
- min-width: 96px !important;
- min-height: 96px !important;
-}
-
-.is-w-auto {
- width: auto !important;
-}
-
-.is-w-xs {
- width: 80px !important;
-}
-
-.is-w-s {
- width: 100px !important;
-}
-
-.is-w-m {
- width: 150px !important;
-}
-
-.is-w-l {
- width: 200px !important;
-}
-
-.is-w-xl {
- width: 250px !important;
-}
-
-.is-w-xxl {
- width: 500px !important;
-}
-
-.is-h-xs {
- height: 80px !important;
-}
-
-.is-h-s {
- height: 100px !important;
-}
-
-.is-h-m {
- height: 150px !important;
-}
-
-.is-h-l {
- height: 200px !important;
-}
-
-.is-h-xl {
- height: 250px !important;
-}
-
-.is-h-xxl {
- height: 500px !important;
-}
-
-@media only screen and (max-width: 768px) {
- .is-w-auto-mobile {
- width: auto !important;
- }
-
- .is-w-xs-mobile {
- width: 80px !important;
- }
-
- .is-w-s-mobile {
- width: 100px !important;
- }
-
- .is-w-m-mobile {
- width: 150px !important;
- }
-
- .is-w-l-mobile {
- width: 200px !important;
- }
-
- .is-w-xl-mobile {
- width: 250px !important;
- }
-
- .is-w-xxl-mobile {
- width: 500px !important;
- }
-
- .is-h-xs-mobile {
- height: 80px !important;
- }
-
- .is-h-s-mobile {
- height: 100px !important;
- }
-
- .is-h-m-mobile {
- height: 150px !important;
- }
-
- .is-h-l-mobile {
- height: 200px !important;
- }
-
- .is-h-xl-mobile {
- height: 250px !important;
- }
-
- .is-h-xxl-mobile {
- height: 500px !important;
- }
-}
-
-@media only screen and (min-width: 769px) {
- .is-w-auto-tablet {
- width: auto !important;
- }
-
- .is-w-xs-tablet {
- width: 80px !important;
- }
-
- .is-w-s-tablet {
- width: 100px !important;
- }
-
- .is-w-m-tablet {
- width: 150px !important;
- }
-
- .is-w-l-tablet {
- width: 200px !important;
- }
-
- .is-w-xl-tablet {
- width: 250px !important;
- }
-
- .is-w-xxl-tablet {
- width: 500px !important;
- }
-
- .is-h-xs-tablet {
- height: 80px !important;
- }
-
- .is-h-s-tablet {
- height: 100px !important;
- }
-
- .is-h-m-tablet {
- height: 150px !important;
- }
-
- .is-h-l-tablet {
- height: 200px !important;
- }
-
- .is-h-xl-tablet {
- height: 250px !important;
- }
-
- .is-h-xxl-tablet {
- height: 500px !important;
- }
-}
-
-@media only screen and (min-width: 1024px) {
- .is-w-auto-desktop {
- width: auto !important;
- }
-
- .is-w-xs-desktop {
- width: 80px !important;
- }
-
- .is-w-s-desktop {
- width: 100px !important;
- }
-
- .is-w-m-desktop {
- width: 150px !important;
- }
-
- .is-w-l-desktop {
- width: 200px !important;
- }
-
- .is-w-xl-desktop {
- width: 250px !important;
- }
-
- .is-w-xxl-desktop {
- width: 500px !important;
- }
-
- .is-h-xs-desktop {
- height: 80px !important;
- }
-
- .is-h-s-desktop {
- height: 100px !important;
- }
-
- .is-h-m-desktop {
- height: 150px !important;
- }
-
- .is-h-l-desktop {
- height: 200px !important;
- }
-
- .is-h-xl-desktop {
- height: 250px !important;
- }
-
- .is-h-xxl-desktop {
- height: 500px !important;
- }
-}
-
-/* Alignments
- *
- * Use them with `.align.to-(c|t|r|b|l)[-(mobile|tablet)]`
- ******************************************************************************/
-
-/* Flex item position
- * -------------------------------------------------------------------------- */
-
-.align {
- display: flex !important;
- flex-direction: row !important;
-}
-
-.align.to-c {
- justify-content: center !important;
-}
-
-.align.to-t {
- align-items: flex-start !important;
-}
-
-.align.to-r {
- justify-content: flex-end !important;
-}
-
-.align.to-b {
- align-items: flex-end !important;
-}
-
-.align.to-l {
- justify-content: flex-start !important;
-}
-
-@media screen and (max-width: 768px) {
- .align.to-c-mobile {
- justify-content: center !important;
- }
-
- .align.to-t-mobile {
- align-items: flex-start !important;
- }
-
- .align.to-r-mobile {
- justify-content: flex-end !important;
- }
-
- .align.to-b-mobile {
- align-items: flex-end !important;
- }
-
- .align.to-l-mobile {
- justify-content: flex-start !important;
- }
-}
-
-@media screen and (min-width: 769px) {
- .align.to-c-tablet {
- justify-content: center !important;
- }
-
- .align.to-t-tablet {
- align-items: flex-start !important;
- }
-
- .align.to-r-tablet {
- justify-content: flex-end !important;
- }
-
- .align.to-b-tablet {
- align-items: flex-end !important;
- }
-
- .align.to-l-tablet {
- justify-content: flex-start !important;
- }
-}
-
-/* Spacings
- *
- * Those are supplementary rules to Bulma’s. They follow the same conventions.
- * Add those you’ll need.
- ******************************************************************************/
-
-.mr-auto {
- margin-right: auto !important;
-}
-
-.ml-auto {
- margin-left: auto !important;
-}
-
-@media screen and (max-width: 768px) {
- .m-0-mobile {
- margin: 0 !important;
- }
-
- .mr-auto-mobile {
- margin-right: auto !important;
- }
-
- .ml-auto-mobile {
- margin-left: auto !important;
- }
-
- .mt-3-mobile {
- margin-top: 0.75rem !important;
- }
-
- .ml-3-mobile {
- margin-left: 0.75rem !important;
- }
-
- .mx-3-mobile {
- margin-right: 0.75rem !important;
- margin-left: 0.75rem !important;
- }
-
- .my-3-mobile {
- margin-top: 0.75rem !important;
- margin-bottom: 0.75rem !important;
- }
-}
-
-@media screen and (min-width: 769px) {
- .m-0-tablet {
- margin: 0 !important;
- }
-
- .mr-auto-tablet {
- margin-right: auto !important;
- }
-
- .ml-auto-tablet {
- margin-left: auto !important;
- }
-
- .mt-3-tablet {
- margin-top: 0.75rem !important;
- }
-
- .ml-3-tablet {
- margin-left: 0.75rem !important;
- }
-
- .mx-3-tablet {
- margin-right: 0.75rem !important;
- margin-left: 0.75rem !important;
- }
-
- .my-3-tablet {
- margin-top: 0.75rem !important;
- margin-bottom: 0.75rem !important;
- }
-}
-
-/* Gaps (for Flexbox and Grid)
- *
- * Those are supplementary rules to Bulma’s. They follow the same conventions.
- * Add those you’ll need.
- ******************************************************************************/
-
-.is-gap-0 {
- gap: 0;
-}
-
-.is-gap-1 {
- gap: 0.25rem;
-}
-
-.is-gap-2 {
- gap: 0.5rem;
-}
-
-.is-gap-3 {
- gap: 0.75rem;
-}
-
-.is-gap-4 {
- gap: 1rem;
-}
-
-.is-gap-5 {
- gap: 1.5rem;
-}
-
-.is-gap-6 {
- gap: 3rem;
-}
-
-.is-row-gap-0 {
- row-gap: 0;
-}
-
-.is-row-gap-1 {
- row-gap: 0.25rem;
-}
-
-.is-row-gap-2 {
- row-gap: 0.5rem;
-}
-
-.is-row-gap-3 {
- row-gap: 0.75rem;
-}
-
-.is-row-gap-4 {
- row-gap: 1rem;
-}
-
-.is-row-gap-5 {
- row-gap: 1.5rem;
-}
-
-.is-row-gap-6 {
- row-gap: 3rem;
-}
-
-.is-column-gap-0 {
- column-gap: 0;
-}
-
-.is-column-gap-1 {
- column-gap: 0.25rem;
-}
-
-.is-column-gap-2 {
- column-gap: 0.5rem;
-}
-
-.is-column-gap-3 {
- column-gap: 0.75rem;
-}
-
-.is-column-gap-4 {
- column-gap: 1rem;
-}
-
-.is-column-gap-5 {
- column-gap: 1.5rem;
-}
-
-.is-column-gap-6 {
- column-gap: 3rem;
-}
+@import "bookwyrm/all.scss";
diff --git a/bookwyrm/static/css/bookwyrm/_all.scss b/bookwyrm/static/css/bookwyrm/_all.scss
new file mode 100644
index 00000000..11d7e403
--- /dev/null
+++ b/bookwyrm/static/css/bookwyrm/_all.scss
@@ -0,0 +1,159 @@
+/** Imports
+ ******************************************************************************/
+@import "components/avatar";
+@import "components/book_cover";
+@import "components/book_grid";
+@import "components/book_list";
+@import "components/book_preview_table";
+@import "components/breadcrumbs";
+@import "components/copy";
+@import "components/details";
+@import "components/file_input";
+@import "components/live_message";
+@import "components/shelving";
+@import "components/stars";
+@import "components/status";
+@import "components/tabs";
+@import "components/toggle";
+
+@import "overrides/bulma_overrides";
+
+@import "utilities/a11y";
+@import "utilities/alignments";
+@import "utilities/colors";
+@import "utilities/size";
+@import "utilities/spacings";
+@import "utilities/transitions";
+
+html {
+ scroll-behavior: smooth;
+}
+
+body {
+ min-height: 100vh;
+ display: flex;
+ flex-direction: column;
+}
+
+button {
+ border: none;
+ margin: 0;
+ padding: 0;
+ width: auto;
+ overflow: visible;
+ background: transparent;
+
+ /* inherit font, color & alignment from ancestor */
+ color: inherit;
+ font: inherit;
+ text-align: inherit;
+
+ /* Normalize `line-height`. Cannot be changed from `normal` in Firefox 4+. */
+ line-height: normal;
+
+ /* Corrects font smoothing for webkit */
+ -webkit-font-smoothing: inherit;
+ -moz-osx-font-smoothing: inherit;
+
+ /* Corrects inability to style clickable `input` types in iOS */
+ -webkit-appearance: none;
+
+ /* Generalizes pointer cursor */
+ cursor: pointer;
+}
+
+button::-moz-focus-inner {
+ /* Remove excess padding and border in Firefox 4+ */
+ border: 0;
+ padding: 0;
+}
+
+/* Better accessibility for keyboard users */
+*:focus-visible {
+ outline-style: auto !important;
+}
+
+/** Utilities not covered by Bulma
+ ******************************************************************************/
+
+
+.tag.is-small {
+ height: auto;
+}
+
+.button.is-transparent {
+ background-color: transparent;
+}
+
+.card.is-stretchable {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+}
+
+.card.is-stretchable .card-content {
+ flex-grow: 1;
+}
+
+.preserve-whitespace p {
+ white-space: pre-wrap !important;
+}
+
+.display-inline p {
+ display: inline !important;
+}
+
+button .button-invisible-overlay {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ padding: 1rem;
+ box-sizing: border-box;
+ display: flex;
+ align-items: center;
+ flex-direction: column;
+ justify-content: center;
+ background: rgba($scheme-invert, 0.66);
+ color: white;
+ opacity: 0;
+ transition: opacity 0.2s ease;
+}
+
+button:hover .button-invisible-overlay,
+button:active .button-invisible-overlay,
+button:focus-visible .button-invisible-overlay {
+ opacity: 1;
+}
+
+
+
+/** Tooltips
+ ******************************************************************************/
+
+.tooltip {
+ width: 100%;
+}
+
+/** States
+ ******************************************************************************/
+
+/* "disabled" for non-buttons */
+
+.is-disabled {
+ background-color: $pagination-disabled-background-color;
+ border-color: $pagination-disabled-border-color;
+ box-shadow: none;
+ color: $pagination-disabled-color;
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
+
+/* Notifications page
+ ******************************************************************************/
+
+.notification a.icon {
+ text-decoration: none !important;
+}
diff --git a/bookwyrm/static/css/bookwyrm/components/_avatar.css b/bookwyrm/static/css/bookwyrm/components/_avatar.css
new file mode 100644
index 00000000..433b8946
--- /dev/null
+++ b/bookwyrm/static/css/bookwyrm/components/_avatar.css
@@ -0,0 +1,7 @@
+/** Avatars
+ ******************************************************************************/
+
+.avatar {
+ vertical-align: middle;
+ display: inline;
+}
diff --git a/bookwyrm/static/css/bookwyrm/components/_book_cover.scss b/bookwyrm/static/css/bookwyrm/components/_book_cover.scss
new file mode 100644
index 00000000..d1125197
--- /dev/null
+++ b/bookwyrm/static/css/bookwyrm/components/_book_cover.scss
@@ -0,0 +1,70 @@
+/** Book covers
+ *
+ * - .is-cover gives the behaviour of the cover and its surrounding. (optional)
+ * - .cover-container gives the dimensions and position (for borders, image and other elements).
+ * - .book-cover is positioned and sized based on its container.
+ *
+ * To have the cover within specific dimensions, specify a width or height for
+ * standard bulma’s named breapoints:
+ *
+ * `is-(w|h)-(auto|xs|s|m|l|xl|xxl)[-(mobile|tablet|desktop)]`
+ *
+ * The cover will be centered horizontally and vertically within those dimensions.
+ *
+ * When using `.column.is-N`, add `.is-w-auto` to the container so that the flex
+ * calculations are not biased by the default `max-content`.
+ ******************************************************************************/
+
+.column.is-cover {
+ flex-grow: 0 !important;
+}
+
+.column.is-cover,
+.column.is-cover + .column {
+ flex-basis: auto !important;
+}
+
+.cover-container {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ position: relative;
+ width: max-content;
+ max-width: 100%;
+ overflow: hidden;
+}
+
+/* Book cover
+ * -------------------------------------------------------------------------- */
+
+.book-cover {
+ display: block;
+ max-width: 100%;
+ max-height: 100%;
+
+ /* Useful when stretching under-sized images. */
+ image-rendering: optimizequality;
+ image-rendering: smooth;
+}
+
+/* Cover caption
+ * -------------------------------------------------------------------------- */
+
+.no-cover .cover-caption {
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ padding: 0.5em;
+ font-size: 0.75em;
+ color: white;
+ background-color: $no-cover-color;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-direction: column;
+ gap: 1em;
+ white-space: initial;
+ text-align: center;
+}
diff --git a/bookwyrm/static/css/bookwyrm/components/_book_grid.scss b/bookwyrm/static/css/bookwyrm/components/_book_grid.scss
new file mode 100644
index 00000000..5993bb43
--- /dev/null
+++ b/bookwyrm/static/css/bookwyrm/components/_book_grid.scss
@@ -0,0 +1,36 @@
+/* Books grid
+ ******************************************************************************/
+
+.books-grid {
+ display: grid;
+ grid-template-columns: repeat(2, 1fr);
+ gap: 1rem;
+ align-items: end;
+ justify-items: stretch;
+}
+
+.books-grid > .is-big {
+ grid-column: span 2;
+ grid-row: span 2;
+ justify-self: stretch;
+}
+
+.books-grid .book-cover {
+ width: 100%;
+}
+
+.books-grid .book-title {
+ --height-basis: 1.35rem;
+
+ display: block;
+ margin-top: 0.5rem;
+ line-height: var(--height-basis);
+ min-height: calc(2 * var(--height-basis));
+}
+
+@media only screen and (min-width: 769px) {
+ .books-grid {
+ gap: 1.5rem;
+ grid-template-columns: repeat(auto-fill, minmax(8em, 1fr));
+ }
+}
diff --git a/bookwyrm/static/css/bookwyrm/components/_book_list.scss b/bookwyrm/static/css/bookwyrm/components/_book_list.scss
new file mode 100644
index 00000000..0b109348
--- /dev/null
+++ b/bookwyrm/static/css/bookwyrm/components/_book_list.scss
@@ -0,0 +1,47 @@
+/* Book list
+ ******************************************************************************/
+
+ol.ordered-list {
+ list-style: none;
+ counter-reset: list-counter;
+}
+
+ol.ordered-list li {
+ counter-increment: list-counter;
+}
+
+ol.ordered-list li::before {
+ content: counter(list-counter);
+ position: absolute;
+ left: -20px;
+ width: 20px;
+ height: 24px;
+ background-color: $scheme-main;
+ border: 1px solid $border;
+ border-right: 0;
+ border-top-left-radius: 2px;
+ border-top-right-radius: 2px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ color: $text-light;
+ font-size: 0.8em;
+ font-weight: bold;
+}
+
+@media only screen and (max-width: 768px) {
+ ol.ordered-list li::before {
+ left: 0;
+ z-index: 1;
+ border: 0;
+ border-right: 1px solid $border;
+ border-bottom: 1px solid $border;
+ border-radius: 0;
+ border-bottom-right-radius: 2px;
+ }
+}
+
+.overflow-wrap-anywhere {
+ overflow-wrap: anywhere;
+ min-width: 10em;
+}
diff --git a/bookwyrm/static/css/bookwyrm/components/_book_preview_table.scss b/bookwyrm/static/css/bookwyrm/components/_book_preview_table.scss
new file mode 100644
index 00000000..fdbb29f0
--- /dev/null
+++ b/bookwyrm/static/css/bookwyrm/components/_book_preview_table.scss
@@ -0,0 +1,49 @@
+/* Book preview table
+ ******************************************************************************/
+
+.book-preview td {
+ vertical-align: middle;
+}
+
+@media only screen and (max-width: 768px) {
+ table.is-mobile,
+ table.is-mobile tbody {
+ display: block;
+ }
+
+ table.is-mobile tr {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: space-between;
+ border-top: 1px solid $border;
+ }
+
+ table.is-mobile td {
+ display: block;
+ box-sizing: border-box;
+ flex: 1 0 100%;
+ order: 2;
+ border-bottom: 0;
+ }
+
+ table.is-mobile td.book-preview-top-row {
+ order: 1;
+ flex-basis: auto;
+ }
+
+ table.is-mobile td[data-title]:not(:empty)::before {
+ content: attr(data-title);
+ display: block;
+ font-size: 0.75em;
+ font-weight: bold;
+ }
+
+ table.is-mobile td:empty {
+ padding: 0;
+ }
+
+ table.is-mobile th,
+ table.is-mobile thead {
+ display: none;
+ }
+}
diff --git a/bookwyrm/static/css/bookwyrm/components/_breadcrumbs.scss b/bookwyrm/static/css/bookwyrm/components/_breadcrumbs.scss
new file mode 100644
index 00000000..9d445629
--- /dev/null
+++ b/bookwyrm/static/css/bookwyrm/components/_breadcrumbs.scss
@@ -0,0 +1,13 @@
+/* Breadcrumbs
+ ******************************************************************************/
+
+.breadcrumb li:first-child * {
+ padding-left: 0;
+}
+
+.breadcrumb li > * {
+ align-items: center;
+ display: flex;
+ justify-content: center;
+ padding: 0 0.75em;
+}
diff --git a/bookwyrm/static/css/bookwyrm/components/_copy.scss b/bookwyrm/static/css/bookwyrm/components/_copy.scss
new file mode 100644
index 00000000..e0c4246e
--- /dev/null
+++ b/bookwyrm/static/css/bookwyrm/components/_copy.scss
@@ -0,0 +1,30 @@
+/* Copy
+ ******************************************************************************/
+
+.horizontal-copy {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ gap: 0.75rem;
+}
+
+.horizontal-copy textarea {
+ min-width: initial;
+ white-space: nowrap;
+}
+
+.horizontal-copy button {
+ align-self: stretch;
+ height: unset;
+}
+
+.vertical-copy {
+ display: flex;
+ flex-direction: column;
+ align-items: stretch;
+ gap: 0.75rem;
+}
+
+.vertical-copy button {
+ width: 100%;
+}
diff --git a/bookwyrm/static/css/bookwyrm/components/_details.scss b/bookwyrm/static/css/bookwyrm/components/_details.scss
new file mode 100644
index 00000000..645de4a1
--- /dev/null
+++ b/bookwyrm/static/css/bookwyrm/components/_details.scss
@@ -0,0 +1,116 @@
+/** General `details` element styles
+ ******************************************************************************/
+
+details summary {
+ cursor: pointer;
+}
+
+summary::-webkit-details-marker {
+ display: none;
+}
+
+details summary::marker {
+ content: none;
+}
+
+details.detail-pinned-button summary {
+ position: absolute;
+ right: 0;
+}
+
+details.detail-pinned-button form {
+ float: left;
+ width: 100%;
+ margin-top: 1em;
+}
+
+/** Dropdown w/ Details element
+ ******************************************************************************/
+
+details.dropdown[open] summary.dropdown-trigger::before {
+ content: "";
+ position: fixed;
+ top: 0;
+ bottom: 0;
+ left: 0;
+ right: 0;
+}
+
+details.dropdown .dropdown-menu {
+ display: block !important;
+}
+
+details.dropdown .dropdown-menu button {
+ /* Fix weird Safari defaults */
+ box-sizing: border-box;
+}
+
+details.dropdown .dropdown-menu button:focus-visible,
+details.dropdown .dropdown-menu a:focus-visible {
+ outline-style: auto;
+ outline-offset: -2px;
+}
+
+@media only screen and (max-width: 768px) {
+ details.dropdown[open] summary.dropdown-trigger::before {
+ background-color: rgba($scheme-invert, 0.5);
+ z-index: 30;
+ }
+
+ details .dropdown-menu {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ display: flex !important;
+ align-items: center;
+ justify-content: center;
+ pointer-events: none;
+ z-index: 100;
+ }
+
+ details .dropdown-menu > * {
+ pointer-events: all;
+ }
+}
+
+/** Details panel
+ ******************************************************************************/
+
+details.details-panel {
+ box-shadow: 0 0 0 1px $border;
+ transition: box-shadow 0.2s ease;
+ padding: 0.75rem;
+}
+
+details[open].details-panel,
+details.details-panel:hover {
+ box-shadow: 0 0 0 1px $border;
+}
+
+details.details-panel summary {
+ position: relative;
+}
+
+details summary .details-close {
+ position: absolute;
+ right: 0;
+ top: 0;
+ transform: rotate(45deg);
+ transition: transform 0.2s ease;
+}
+
+details[open] summary .details-close {
+ transform: rotate(0deg);
+}
+
+@media only screen and (min-width: 769px) {
+ .details-panel .filters-field:not(:last-child) {
+ border-right: 1px solid $border;
+ margin-top: 0.75rem;
+ margin-bottom: 0.75rem;
+ padding-top: 0.25rem;
+ padding-bottom: 0.25rem;
+ }
+}
diff --git a/bookwyrm/static/css/bookwyrm/components/_file_input.scss b/bookwyrm/static/css/bookwyrm/components/_file_input.scss
new file mode 100644
index 00000000..3ccc70f5
--- /dev/null
+++ b/bookwyrm/static/css/bookwyrm/components/_file_input.scss
@@ -0,0 +1,28 @@
+/** File input styles
+ ******************************************************************************/
+
+input[type="file"]::file-selector-button {
+ -moz-appearance: none;
+ -webkit-appearance: none;
+ background-color: $scheme-main;
+ border-radius: 4px;
+ border: 1px solid $border;
+ box-shadow: none;
+ color: $text;
+ cursor: pointer;
+ font-size: 1rem;
+ height: 2.5em;
+ justify-content: center;
+ line-height: 1.5;
+ padding-bottom: calc(0.5em - 1px);
+ padding-left: 1em;
+ padding-right: 1em;
+ padding-top: calc(0.5em - 1px);
+ text-align: center;
+ white-space: nowrap;
+}
+
+input[type="file"]::file-selector-button:hover {
+ border-color: $border-hover;
+ color: text;
+}
diff --git a/bookwyrm/static/css/bookwyrm/components/_live_message.scss b/bookwyrm/static/css/bookwyrm/components/_live_message.scss
new file mode 100644
index 00000000..5d8680cc
--- /dev/null
+++ b/bookwyrm/static/css/bookwyrm/components/_live_message.scss
@@ -0,0 +1,8 @@
+/** Transient notification
+ ******************************************************************************/
+
+#live-messages {
+ position: fixed;
+ bottom: 1em;
+ right: 1em;
+}
diff --git a/bookwyrm/static/css/bookwyrm/components/_shelving.scss b/bookwyrm/static/css/bookwyrm/components/_shelving.scss
new file mode 100644
index 00000000..15b01e55
--- /dev/null
+++ b/bookwyrm/static/css/bookwyrm/components/_shelving.scss
@@ -0,0 +1,10 @@
+/** Shelving
+ ******************************************************************************/
+
+/** @todo Replace icons with SVG symbols.
+ @see https://www.youtube.com/watch?v=9xXBYcWgCHA */
+.shelf-option:disabled > *::after {
+ font-family: icomoon; /* stylelint-disable font-family-no-missing-generic-family-keyword */
+ content: "\e919"; /* icon-check */
+ margin-left: 0.5em;
+}
diff --git a/bookwyrm/static/css/bookwyrm/components/_stars.scss b/bookwyrm/static/css/bookwyrm/components/_stars.scss
new file mode 100644
index 00000000..1a8e3680
--- /dev/null
+++ b/bookwyrm/static/css/bookwyrm/components/_stars.scss
@@ -0,0 +1,52 @@
+/** Stars
+ ******************************************************************************/
+
+.stars {
+ white-space: nowrap;
+}
+
+/** Stars in a review form
+ *
+ * Specificity makes hovering taking over checked inputs.
+ *
+ * \e9d9: filled star
+ * \e9d7: empty star;
+ * -------------------------------------------------------------------------- */
+
+.form-rate-stars {
+ width: max-content;
+}
+
+/* All stars are visually filled by default. */
+.form-rate-stars .icon::before {
+ content: "\e9d9"; /* icon-star-full */
+}
+
+/* Icons directly following half star inputs are marked as half */
+.form-rate-stars input.half:checked ~ .icon::before {
+ content: "\e9d8"; /* icon-star-half */
+}
+
+/* stylelint-disable no-descending-specificity */
+.form-rate-stars input.half:checked + input + .icon:hover::before {
+ content: "\e9d8" !important; /* icon-star-half */
+}
+
+/* Icons directly following half check inputs that follow the checked input are emptied. */
+.form-rate-stars input.half:checked + input + .icon ~ .icon::before {
+ content: "\e9d7"; /* icon-star-empty */
+}
+
+/* Icons directly following inputs that follow the checked input are emptied. */
+.form-rate-stars input:checked ~ input + .icon::before {
+ content: "\e9d7"; /* icon-star-empty */
+}
+
+/* When a label is hovered, repeat the fill-all-then-empty-following pattern. */
+.form-rate-stars:hover .icon.icon::before {
+ content: "\e9d9" !important; /* icon-star-full */
+}
+
+.form-rate-stars .icon:hover ~ .icon::before {
+ content: "\e9d7" !important; /* icon-star-empty */
+}
diff --git a/bookwyrm/static/css/bookwyrm/components/_status.scss b/bookwyrm/static/css/bookwyrm/components/_status.scss
new file mode 100644
index 00000000..3ec679cd
--- /dev/null
+++ b/bookwyrm/static/css/bookwyrm/components/_status.scss
@@ -0,0 +1,57 @@
+/** Statuses: Quotes
+ *
+ * \e906: icon-quote-open
+ * \e905: icon-quote-close
+ *
+ * The `content` class on the blockquote allows to apply styles to markdown
+ * generated HTML in the quote: https://bulma.io/documentation/elements/content/
+ *
+ * ```html
+ *
+ *
+ * User generated quote in markdown…
+ *
+ *
+ *
— Book Title by Author
+ *
+ * ```
+ ******************************************************************************/
+
+.quote > blockquote {
+ position: relative;
+ padding-left: 2em;
+}
+
+.quote > blockquote::before,
+.quote > blockquote::after {
+ font-family: icomoon; /* stylelint-disable font-family-no-missing-generic-family-keyword */
+ position: absolute;
+}
+
+.quote > blockquote::before {
+ content: "\e907"; /* icon-quote-open */
+ top: 0;
+ left: 0;
+}
+
+.quote > blockquote::after {
+ content: "\e906"; /* icon-quote-close */
+ right: 0;
+}
+
+/* Threads
+ ******************************************************************************/
+
+.thread .is-main .card {
+ box-shadow: 0 0.5em 1em -0.125em rgba($link, 0.35), 0 0 0 1px rgba($link, 0.02);
+}
+
+.thread::after {
+ content: "";
+ position: absolute;
+ z-index: -1;
+ top: 0;
+ bottom: 0;
+ left: 2.5em;
+ border-left: 2px solid $border;
+}
diff --git a/bookwyrm/static/css/bookwyrm/components/_tabs.scss b/bookwyrm/static/css/bookwyrm/components/_tabs.scss
new file mode 100644
index 00000000..8e00f6a8
--- /dev/null
+++ b/bookwyrm/static/css/bookwyrm/components/_tabs.scss
@@ -0,0 +1,176 @@
+/** Bookwyrm Tabs
+ ******************************************************************************/
+
+.bw-tabs {
+ -webkit-overflow-scrolling: touch;
+ -webkit-touch-callout: none;
+ position: relative;
+ align-items: center;
+ display: flex;
+ font-size: 1rem;
+ justify-content: flex-start;
+ overflow-x: auto;
+ overflow-y: hidden;
+ user-select: none;
+ white-space: nowrap;
+}
+
+.bw-tabs::before {
+ border-bottom-color: $border;
+ border-bottom-style: solid;
+ border-bottom-width: 1px;
+ bottom: 0;
+ content: "";
+ position: absolute;
+ width: 100%;
+}
+
+.bw-tabs:not(:last-child) {
+ margin-bottom: 1.5rem;
+}
+
+.bw-tabs a {
+ align-items: center;
+ border-bottom-color: $border;
+ border-bottom-style: solid;
+ border-bottom-width: 1px;
+ color: $text;
+ display: flex;
+ justify-content: center;
+ margin-bottom: -1px;
+ padding: 0.5em 1em;
+ position: relative;
+}
+
+.bw-tabs a:hover {
+ border-bottom-color: transparent;
+ color: $text;
+}
+
+.bw-tabs a.is-active {
+ border-bottom-color: transparent;
+ color: $link;
+}
+
+.bw-tabs.is-left {
+ padding-right: 0.75em;
+}
+
+.bw-tabs.is-center {
+ flex: none;
+ justify-content: center;
+ padding-left: 0.75em;
+ padding-right: 0.75em;
+}
+
+.bw-tabs.is-right {
+ justify-content: flex-end;
+ padding-left: 0.75em;
+}
+
+.bw-tabs .icon:first-child {
+ margin-right: 0.5em;
+}
+
+.bw-tabs .icon:last-child {
+ margin-left: 0.5em;
+}
+
+.bw-tabs.is-centered {
+ justify-content: center;
+}
+
+.bw-tabs.is-boxed a {
+ border: 1px solid transparent;
+ border-radius: 4px 4px 0 0;
+}
+
+.bw-tabs.is-boxed a:hover {
+ background-color: $background-secondary;
+ border-bottom-color: $border-hover;
+}
+
+.bw-tabs.is-boxed a.is-active {
+ background-color: $background-body;
+ border-color: $border;
+ border-bottom-color: $border !important;
+}
+
+.bw-tabs.is-fullwidth a {
+ flex-grow: 1;
+ flex-shrink: 0;
+}
+
+.bw-tabs.is-toggle a {
+ border-color: $border;
+ border-style: solid;
+ border-width: 1px;
+ margin-bottom: 0;
+ position: relative;
+}
+
+.bw-tabs.is-toggle a:hover {
+ background-color: $background-secondary;
+ border-color: $border;
+ z-index: 2;
+}
+
+.bw-tabs.is-toggle a + a {
+ margin-left: -1px;
+}
+
+.bw-tabs.is-toggle a:first-child {
+ border-top-left-radius: 4px;
+ border-bottom-left-radius: 4px;
+}
+
+.bw-tabs.is-toggle a:last-child {
+ border-top-right-radius: 4px;
+ border-bottom-right-radius: 4px;
+}
+
+.bw-tabs.is-toggle a.is-active {
+ background-color: $link-background;
+ border-color: $link;
+ color: $text;
+ z-index: 1;
+}
+
+.bw-tabs.is-toggle {
+ border-bottom: none;
+}
+
+.bw-tabs.is-toggle.is-toggle-rounded a:first-child {
+ border-bottom-left-radius: 290486px;
+ border-top-left-radius: 290486px;
+ padding-left: 1.25em;
+}
+
+.bw-tabs.is-toggle.is-toggle-rounded a:last-child {
+ border-bottom-right-radius: 290486px;
+ border-top-right-radius: 290486px;
+ padding-right: 1.25em;
+}
+
+.bw-tabs.is-small {
+ font-size: 0.75rem;
+}
+
+.bw-tabs.is-medium {
+ font-size: 1.25rem;
+}
+
+.bw-tabs.is-large {
+ font-size: 1.5rem;
+}
+
+.bw-tabs.has-aside-text a {
+ margin-top: 1.5rem;
+}
+
+.bw-tabs a .aside-text {
+ position: absolute;
+ top: calc(-0.75rem - 0.75rem);
+ left: 0;
+ color: $text;
+}
diff --git a/bookwyrm/static/css/bookwyrm/components/_toggle.scss b/bookwyrm/static/css/bookwyrm/components/_toggle.scss
new file mode 100644
index 00000000..c2c07dfb
--- /dev/null
+++ b/bookwyrm/static/css/bookwyrm/components/_toggle.scss
@@ -0,0 +1,45 @@
+/** Toggles
+ ******************************************************************************/
+
+.toggle-button[aria-pressed="true"],
+.toggle-button[aria-pressed="true"]:hover {
+ background-color: hsl(171deg, 100%, 41%);
+ color: white;
+}
+
+.hide-active[aria-pressed="true"],
+.hide-inactive[aria-pressed="false"] {
+ display: none;
+}
+
+.transition-x.is-hidden,
+.transition-y.is-hidden {
+ display: block !important;
+ visibility: hidden !important;
+ height: 0 !important;
+ width: 0 !important;
+ margin: 0 !important;
+ padding: 0 !important;
+ overflow: auto;
+}
+
+.transition-x,
+.transition-y {
+ transition-duration: 0.5s;
+ transition-timing-function: ease;
+}
+
+.transition-x {
+ transition-property: width, margin-left, margin-right, padding-left, padding-right;
+}
+
+.transition-y {
+ transition-property: height, margin-top, margin-bottom, padding-top, padding-bottom;
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .transition-x,
+ .transition-y {
+ transition-duration: 0.001ms !important;
+ }
+}
diff --git a/bookwyrm/static/css/bookwyrm/overrides/_bulma_overrides.scss b/bookwyrm/static/css/bookwyrm/overrides/_bulma_overrides.scss
new file mode 100644
index 00000000..f46e7b95
--- /dev/null
+++ b/bookwyrm/static/css/bookwyrm/overrides/_bulma_overrides.scss
@@ -0,0 +1,61 @@
+.image {
+ overflow: hidden;
+}
+
+.navbar .logo {
+ max-height: 50px;
+}
+
+.card {
+ overflow: visible;
+}
+
+.card.has-border {
+ border: 1px solid $border;
+}
+
+.scroll-x {
+ overflow: hidden;
+ overflow-x: auto;
+}
+
+.modal-card {
+ pointer-events: none;
+}
+
+.modal-card > * {
+ pointer-events: all;
+}
+
+/* stylelint-disable no-descending-specificity */
+.modal-card:focus {
+ outline-style: auto;
+}
+
+.modal-card:focus:not(:focus-visible) {
+ outline-style: initial;
+}
+
+.modal-card:focus-visible {
+ outline-style: auto;
+}
+/* stylelint-enable no-descending-specificity */
+
+.modal-card.is-fullwidth {
+ min-width: 75% !important;
+}
+
+@media only screen and (min-width: 769px) {
+ .modal-card.is-thin {
+ width: 350px !important;
+ }
+}
+
+.modal-card-body {
+ max-height: 70vh;
+}
+
+.clip-text {
+ max-height: 35em;
+ overflow: hidden;
+}
diff --git a/bookwyrm/static/css/bookwyrm/utilities/_a11y.scss b/bookwyrm/static/css/bookwyrm/utilities/_a11y.scss
new file mode 100644
index 00000000..cf4ef6ab
--- /dev/null
+++ b/bookwyrm/static/css/bookwyrm/utilities/_a11y.scss
@@ -0,0 +1,33 @@
+@media only screen and (max-width: 768px) {
+ .is-sr-only-mobile {
+ border: none !important;
+ clip: rect(0, 0, 0, 0) !important;
+ height: 0.01em !important;
+ overflow: hidden !important;
+ padding: 0 !important;
+ position: absolute !important;
+ white-space: nowrap !important;
+ width: 0.01em !important;
+ }
+
+ .m-0-mobile {
+ margin: 0 !important;
+ }
+
+ .card-footer.is-stacked-mobile {
+ flex-direction: column;
+ }
+
+ .card-footer.is-stacked-mobile .card-footer-item:not(:last-child) {
+ border-bottom: 1px solid $background-tertiary;
+ border-right: 0;
+ }
+
+ .is-flex-direction-row-mobile {
+ flex-direction: row !important;
+ }
+
+ .is-flex-direction-column-mobile {
+ flex-direction: column !important;
+ }
+}
diff --git a/bookwyrm/static/css/bookwyrm/utilities/_alignments.scss b/bookwyrm/static/css/bookwyrm/utilities/_alignments.scss
new file mode 100644
index 00000000..34e36b3b
--- /dev/null
+++ b/bookwyrm/static/css/bookwyrm/utilities/_alignments.scss
@@ -0,0 +1,76 @@
+/* Alignments
+ *
+ * Use them with `.align.to-(c|t|r|b|l)[-(mobile|tablet)]`
+ ******************************************************************************/
+
+/* Flex item position
+ * -------------------------------------------------------------------------- */
+
+.align {
+ display: flex !important;
+ flex-direction: row !important;
+}
+
+.align.to-c {
+ justify-content: center !important;
+}
+
+.align.to-t {
+ align-items: flex-start !important;
+}
+
+.align.to-r {
+ justify-content: flex-end !important;
+}
+
+.align.to-b {
+ align-items: flex-end !important;
+}
+
+.align.to-l {
+ justify-content: flex-start !important;
+}
+
+@media screen and (max-width: 768px) {
+ .align.to-c-mobile {
+ justify-content: center !important;
+ }
+
+ .align.to-t-mobile {
+ align-items: flex-start !important;
+ }
+
+ .align.to-r-mobile {
+ justify-content: flex-end !important;
+ }
+
+ .align.to-b-mobile {
+ align-items: flex-end !important;
+ }
+
+ .align.to-l-mobile {
+ justify-content: flex-start !important;
+ }
+}
+
+@media screen and (min-width: 769px) {
+ .align.to-c-tablet {
+ justify-content: center !important;
+ }
+
+ .align.to-t-tablet {
+ align-items: flex-start !important;
+ }
+
+ .align.to-r-tablet {
+ justify-content: flex-end !important;
+ }
+
+ .align.to-b-tablet {
+ align-items: flex-end !important;
+ }
+
+ .align.to-l-tablet {
+ justify-content: flex-start !important;
+ }
+}
diff --git a/bookwyrm/static/css/bookwyrm/utilities/_colors.scss b/bookwyrm/static/css/bookwyrm/utilities/_colors.scss
new file mode 100644
index 00000000..e44efee9
--- /dev/null
+++ b/bookwyrm/static/css/bookwyrm/utilities/_colors.scss
@@ -0,0 +1,25 @@
+/* Semantic color classes */
+
+.has-background-primary-highlight {
+ background-color: $primary-highlight;
+}
+
+.has-background-info-highlight {
+ background-color: $info-highlight;
+}
+
+.has-background-success-highlight {
+ background-color: $success-highlight;
+}
+
+.has-background-body {
+ background-color: $background-body;
+}
+
+.has-background-secondary {
+ background-color: $background-secondary !important;
+}
+
+.has-background-tertiary {
+ background-color: $background-tertiary !important;
+}
diff --git a/bookwyrm/static/css/bookwyrm/utilities/_size.scss b/bookwyrm/static/css/bookwyrm/utilities/_size.scss
new file mode 100644
index 00000000..cbc74d7a
--- /dev/null
+++ b/bookwyrm/static/css/bookwyrm/utilities/_size.scss
@@ -0,0 +1,227 @@
+/* Dimensions
+ * @todo These could be in rem.
+ ******************************************************************************/
+
+.is-32x32 {
+ min-width: 32px !important;
+ min-height: 32px !important;
+}
+
+.is-96x96 {
+ min-width: 96px !important;
+ min-height: 96px !important;
+}
+
+.is-w-auto {
+ width: auto !important;
+}
+
+.is-w-xs {
+ width: 80px !important;
+}
+
+.is-w-s {
+ width: 100px !important;
+}
+
+.is-w-m {
+ width: 150px !important;
+}
+
+.is-w-l {
+ width: 200px !important;
+}
+
+.is-w-xl {
+ width: 250px !important;
+}
+
+.is-w-xxl {
+ width: 500px !important;
+}
+
+.is-h-xs {
+ height: 80px !important;
+}
+
+.is-h-s {
+ height: 100px !important;
+}
+
+.is-h-m {
+ height: 150px !important;
+}
+
+.is-h-l {
+ height: 200px !important;
+}
+
+.is-h-xl {
+ height: 250px !important;
+}
+
+.is-h-xxl {
+ height: 500px !important;
+}
+
+@media only screen and (max-width: 768px) {
+ .is-w-auto-mobile {
+ width: auto !important;
+ }
+
+ .is-w-xs-mobile {
+ width: 80px !important;
+ }
+
+ .is-w-s-mobile {
+ width: 100px !important;
+ }
+
+ .is-w-m-mobile {
+ width: 150px !important;
+ }
+
+ .is-w-l-mobile {
+ width: 200px !important;
+ }
+
+ .is-w-xl-mobile {
+ width: 250px !important;
+ }
+
+ .is-w-xxl-mobile {
+ width: 500px !important;
+ }
+
+ .is-h-xs-mobile {
+ height: 80px !important;
+ }
+
+ .is-h-s-mobile {
+ height: 100px !important;
+ }
+
+ .is-h-m-mobile {
+ height: 150px !important;
+ }
+
+ .is-h-l-mobile {
+ height: 200px !important;
+ }
+
+ .is-h-xl-mobile {
+ height: 250px !important;
+ }
+
+ .is-h-xxl-mobile {
+ height: 500px !important;
+ }
+}
+
+@media only screen and (min-width: 769px) {
+ .is-w-auto-tablet {
+ width: auto !important;
+ }
+
+ .is-w-xs-tablet {
+ width: 80px !important;
+ }
+
+ .is-w-s-tablet {
+ width: 100px !important;
+ }
+
+ .is-w-m-tablet {
+ width: 150px !important;
+ }
+
+ .is-w-l-tablet {
+ width: 200px !important;
+ }
+
+ .is-w-xl-tablet {
+ width: 250px !important;
+ }
+
+ .is-w-xxl-tablet {
+ width: 500px !important;
+ }
+
+ .is-h-xs-tablet {
+ height: 80px !important;
+ }
+
+ .is-h-s-tablet {
+ height: 100px !important;
+ }
+
+ .is-h-m-tablet {
+ height: 150px !important;
+ }
+
+ .is-h-l-tablet {
+ height: 200px !important;
+ }
+
+ .is-h-xl-tablet {
+ height: 250px !important;
+ }
+
+ .is-h-xxl-tablet {
+ height: 500px !important;
+ }
+}
+
+@media only screen and (min-width: 1024px) {
+ .is-w-auto-desktop {
+ width: auto !important;
+ }
+
+ .is-w-xs-desktop {
+ width: 80px !important;
+ }
+
+ .is-w-s-desktop {
+ width: 100px !important;
+ }
+
+ .is-w-m-desktop {
+ width: 150px !important;
+ }
+
+ .is-w-l-desktop {
+ width: 200px !important;
+ }
+
+ .is-w-xl-desktop {
+ width: 250px !important;
+ }
+
+ .is-w-xxl-desktop {
+ width: 500px !important;
+ }
+
+ .is-h-xs-desktop {
+ height: 80px !important;
+ }
+
+ .is-h-s-desktop {
+ height: 100px !important;
+ }
+
+ .is-h-m-desktop {
+ height: 150px !important;
+ }
+
+ .is-h-l-desktop {
+ height: 200px !important;
+ }
+
+ .is-h-xl-desktop {
+ height: 250px !important;
+ }
+
+ .is-h-xxl-desktop {
+ height: 500px !important;
+ }
+}
diff --git a/bookwyrm/static/css/bookwyrm/utilities/_spacings.scss b/bookwyrm/static/css/bookwyrm/utilities/_spacings.scss
new file mode 100644
index 00000000..f1a1645b
--- /dev/null
+++ b/bookwyrm/static/css/bookwyrm/utilities/_spacings.scss
@@ -0,0 +1,167 @@
+/* Spacings
+ *
+ * Those are supplementary rules to Bulma’s. They follow the same conventions.
+ * Add those you’ll need.
+ ******************************************************************************/
+
+.mr-auto {
+ margin-right: auto !important;
+}
+
+.ml-auto {
+ margin-left: auto !important;
+}
+
+@media screen and (max-width: 768px) {
+ .m-0-mobile {
+ margin: 0 !important;
+ }
+
+ .mr-auto-mobile {
+ margin-right: auto !important;
+ }
+
+ .ml-auto-mobile {
+ margin-left: auto !important;
+ }
+
+ .mt-3-mobile {
+ margin-top: 0.75rem !important;
+ }
+
+ .ml-3-mobile {
+ margin-left: 0.75rem !important;
+ }
+
+ .mx-3-mobile {
+ margin-right: 0.75rem !important;
+ margin-left: 0.75rem !important;
+ }
+
+ .my-3-mobile {
+ margin-top: 0.75rem !important;
+ margin-bottom: 0.75rem !important;
+ }
+}
+
+@media screen and (min-width: 769px) {
+ .m-0-tablet {
+ margin: 0 !important;
+ }
+
+ .mr-auto-tablet {
+ margin-right: auto !important;
+ }
+
+ .ml-auto-tablet {
+ margin-left: auto !important;
+ }
+
+ .mt-3-tablet {
+ margin-top: 0.75rem !important;
+ }
+
+ .ml-3-tablet {
+ margin-left: 0.75rem !important;
+ }
+
+ .mx-3-tablet {
+ margin-right: 0.75rem !important;
+ margin-left: 0.75rem !important;
+ }
+
+ .my-3-tablet {
+ margin-top: 0.75rem !important;
+ margin-bottom: 0.75rem !important;
+ }
+}
+
+/* Gaps (for Flexbox and Grid)
+ *
+ * Those are supplementary rules to Bulma’s. They follow the same conventions.
+ * Add those you’ll need.
+ ******************************************************************************/
+
+.is-gap-0 {
+ gap: 0;
+}
+
+.is-gap-1 {
+ gap: 0.25rem;
+}
+
+.is-gap-2 {
+ gap: 0.5rem;
+}
+
+.is-gap-3 {
+ gap: 0.75rem;
+}
+
+.is-gap-4 {
+ gap: 1rem;
+}
+
+.is-gap-5 {
+ gap: 1.5rem;
+}
+
+.is-gap-6 {
+ gap: 3rem;
+}
+
+.is-row-gap-0 {
+ row-gap: 0;
+}
+
+.is-row-gap-1 {
+ row-gap: 0.25rem;
+}
+
+.is-row-gap-2 {
+ row-gap: 0.5rem;
+}
+
+.is-row-gap-3 {
+ row-gap: 0.75rem;
+}
+
+.is-row-gap-4 {
+ row-gap: 1rem;
+}
+
+.is-row-gap-5 {
+ row-gap: 1.5rem;
+}
+
+.is-row-gap-6 {
+ row-gap: 3rem;
+}
+
+.is-column-gap-0 {
+ column-gap: 0;
+}
+
+.is-column-gap-1 {
+ column-gap: 0.25rem;
+}
+
+.is-column-gap-2 {
+ column-gap: 0.5rem;
+}
+
+.is-column-gap-3 {
+ column-gap: 0.75rem;
+}
+
+.is-column-gap-4 {
+ column-gap: 1rem;
+}
+
+.is-column-gap-5 {
+ column-gap: 1.5rem;
+}
+
+.is-column-gap-6 {
+ column-gap: 3rem;
+}
diff --git a/bookwyrm/static/css/bookwyrm/utilities/_transitions.scss b/bookwyrm/static/css/bookwyrm/utilities/_transitions.scss
new file mode 100644
index 00000000..18f4bc7f
--- /dev/null
+++ b/bookwyrm/static/css/bookwyrm/utilities/_transitions.scss
@@ -0,0 +1,25 @@
+/** Animations and transitions
+ ******************************************************************************/
+
+@keyframes turning {
+ from { transform: rotateZ(0deg); }
+ to { transform: rotateZ(360deg); }
+}
+
+.is-processing .icon-spinner::before {
+ animation: turning 1.5s infinite linear;
+}
+
+.icon-spinner {
+ display: none;
+}
+
+.is-processing .icon-spinner {
+ display: flex;
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .is-processing .icon::before {
+ transition-duration: 0.001ms !important;
+ }
+}
diff --git a/bookwyrm/static/css/themes/dark.scss b/bookwyrm/static/css/themes/dark.scss
new file mode 100644
index 00000000..8df4ce50
--- /dev/null
+++ b/bookwyrm/static/css/themes/dark.scss
@@ -0,0 +1,55 @@
+@import "../vendor/bulma/sass/utilities/derived-variables.sass";
+
+/* Colors
+ ******************************************************************************/
+
+/* states */
+$primary: #016a5b;
+$info: #1f4666;
+$success: #246447;
+$warning: #8b6c15;
+$danger: #872538;
+
+/* book cover standins */
+$no-cover-color: #002549;
+
+/* background colors */
+$scheme-main: $grey-darker;
+$scheme-main-bis: $black-ter;
+$background-body: $grey-darker;
+$background-secondary: $grey-dark;
+$background-tertiary: #555;
+
+/* highlight colors */
+$primary-highlight: $primary;
+$info-highlight: $info;
+$success-highlight: $success;
+
+/* borders */
+$border: $grey;
+$border-hover: $grey-light;
+$border-light: $grey;
+$border-light-hover: $grey-light;
+
+/* text */
+$text: $grey-lightest;
+$text-light: $grey-lighter;
+$text-strong: $white-ter;
+
+/* links */
+$link: $white;
+$link-background: $background-tertiary;
+$link-hover: $white-bis;
+$link-focus: $white-bis;
+$link-active: $white-bis;
+
+/* misc */
+
+/* bulma overrides */
+$background: $background-secondary;
+$menu-item-active-background-color: $link-background;
+
+/* Fonts
+ ******************************************************************************/
+$family-primary: $family-sans-serif;
+$family-secondary: $family-sans-serif;
diff --git a/bookwyrm/static/css/themes/light.scss b/bookwyrm/static/css/themes/light.scss
new file mode 100644
index 00000000..339fc2c3
--- /dev/null
+++ b/bookwyrm/static/css/themes/light.scss
@@ -0,0 +1,53 @@
+@import "../vendor/bulma/sass/utilities/derived-variables.sass";
+
+/* Colors
+ ******************************************************************************/
+
+/* states */
+$primary: $turquoise;
+$info: $cyan;
+$success: $green;
+$warning: $yellow;
+$danger: $red;
+
+/* book cover standins */
+$no-cover-color: #002549;
+
+/* background colors */
+$scheme-main: $white;
+$scheme-main: $white-bis;
+$background-body: $white;
+$background-secondary: $white-ter;
+$background-tertiary: $white-bis;
+
+/* highlight colors */
+$primary-highlight: $primary-light;
+$info-highlight: $info-light;
+$success-highlight: $success-light;
+
+/* borders */
+$border: $grey-lighter;
+$border-hover: $grey-light;
+$border-light: $grey-lightest;
+$border-light-hover: $grey-light;
+
+/* text */
+$text: $grey-dark;
+$text-light: $grey;
+$text-strong: $grey-darker;
+
+/* links */
+$link: #3273dc;
+$link-background: $link;
+$link-hover: $grey-darker;
+$link-focus: $grey-darker;
+$link-active: $grey-darker;
+
+/* bulma overrides */
+$background: $background-secondary;
+$menu-item-active-background-color: $link-background;
+
+/* Fonts
+ ******************************************************************************/
+$family-primary: $family-sans-serif;
+$family-secondary: $family-sans-serif;
diff --git a/bookwyrm/templates/about/about.html b/bookwyrm/templates/about/about.html
index 6f16aa67..553bfee1 100644
--- a/bookwyrm/templates/about/about.html
+++ b/bookwyrm/templates/about/about.html
@@ -19,7 +19,7 @@
{% blocktrans with site_name=site.name %}Welcome to {{ site_name }}!{% endblocktrans %}
-
+
{% blocktrans trimmed with site_name=site.name %}
{{ site_name }} is part of BookWyrm, a network of independent, self-directed communities for readers.
While you can interact seamlessly with users anywhere in the BookWyrm network, this community is unique.
@@ -107,7 +107,7 @@
{% with role=user.groups.first.name %}
-