/* ==========================================================================
bes.is — a small terminal-flavoured stylesheet.
1. Tokens design decisions, in one place
2. Reset & base the same starting point in every browser
3. Typography headings, text, lists, links
4. Layout page frame and layout utilities
5. Controls buttons, fields, everything the browser styles for us
6. Components header, footer, log, panels, json, dialogs
7. Media logo, images, video
========================================================================== */
/* 1. Tokens ================================================================ */
:root {
/* Tells the browser to theme native UI (scrollbars, select popups, focus
rings, spinners) to match the page. Without it those stay light in a
dark page, which is the main reason controls look different per
browser. It also enables light-dark() below. */
color-scheme: light dark;
--black: #050505;
--white: #fafafa;
--background: light-dark(var(--white), var(--black));
--color: light-dark(var(--black), var(--white));
--border: var(--color);
--error: light-dark(#b00000, #ff6b6b);
--info: light-dark(#3d6f8e, #86b6d3);
--green: light-dark(#2f7a2d, #6fce6d);
/* Modular scale. Every gap, margin and padding comes from here so
spacing stays proportional to the fluid type size. */
--ratio: 1.5;
--s-2: calc(var(--s-1) / var(--ratio));
--s-1: calc(var(--s0) / var(--ratio));
--s0: 1rem;
--s1: calc(var(--s0) * var(--ratio));
--s2: calc(var(--s1) * var(--ratio));
/* A concrete stack instead of bare `monospace`: browsers disagree both on
which font that is and on its default size. */
--font: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
--measure: 80ch;
--radius: 0.5em;
--border-width: 1px;
--rule: 2px dotted var(--border);
}
/* 2. Reset & base ========================================================== */
*,
*::before,
*::after {
box-sizing: border-box;
}
html {
font-family: var(--font);
font-size: calc(1rem + 0.4vw);
line-height: 1.5;
-webkit-text-size-adjust: 100%;
tab-size: 4;
}
body {
margin: 0;
min-block-size: 100dvh;
display: flex;
flex-direction: column;
background-color: var(--background);
color: var(--color);
}
/* One focus style for every focusable thing, instead of six browser defaults.
`:focus:not(:focus-visible)` keeps programmatic focus (FocusOnNavigate
lands on the
after every navigation) from flashing a ring. */
:focus-visible {
outline: 2px solid var(--color);
outline-offset: 2px;
}
:focus:not(:focus-visible) {
outline: none;
}
::selection {
background-color: var(--color);
color: var(--background);
}
/* 3. Typography ============================================================ */
h1,
h2,
h3,
h4,
h5,
h6 {
margin-block: 0 var(--s-1);
font-weight: 700;
line-height: 1.2;
text-wrap: balance;
}
h1 {
font-size: calc(1em * var(--ratio));
}
h2 {
font-size: calc(1em * 1.25);
}
h3,
h4,
h5,
h6 {
font-size: 1em;
}
p {
margin-block: var(--s-1);
text-wrap: pretty;
}
ul,
ol {
margin-block: var(--s-1);
padding-inline-start: var(--s1);
}
pre {
margin-block: var(--s-1);
/* pre-line, so examples can stay indented in the .razor source */
white-space: pre-line;
overflow-wrap: anywhere;
}
small {
font-size: 0.75em;
}
a {
color: inherit;
text-decoration-line: underline;
text-decoration-thickness: var(--border-width);
text-underline-offset: 0.2em;
}
a:hover,
a:focus-visible {
background-color: var(--color);
color: var(--background);
text-decoration: none;
}
/* 4. Layout ================================================================ */
/* The frame a layout wraps its whole page in: header, content and footer in one column, with
`main` stretching so the footer sits at the bottom of a short page. It carries the rhythm that
used to be on , so that /git can lay itself out its own way. */
.page {
flex: 1;
display: flex;
flex-direction: column;
gap: var(--s0);
padding-block: var(--s0);
}
.center {
box-sizing: content-box;
max-inline-size: var(--measure);
margin-inline: auto;
padding-inline: var(--s1);
}
main {
flex: 1;
border-block-start: var(--rule);
border-block-end: var(--rule);
padding-block: var(--s0);
}
.flex-row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--s0);
}
/* A field shares the row instead of claiming a whole one to itself. */
.flex-row > :is(input[type="text"], textarea) {
flex: 1;
}
.flex-spread {
display: flex;
flex-wrap: wrap;
gap: var(--s0);
justify-content: space-between;
}
/* A stack on the left, the program left to run on the right (the log rows and
the worked examples in the docs). A grid rather than a spread flex row, so a
long program wraps inside its own column instead of taking a full-width line
of its own and leaving its stack stranded on the line above. */
.stack-step {
display: grid;
grid-template-columns: minmax(0, max-content) 1fr;
column-gap: var(--s0);
}
/* break-word, not anywhere: it keeps the column's minimum at one whole word, so
a long stack can never squeeze the program down to a letter per line. */
.stack-step > :last-child {
text-align: right;
overflow-wrap: break-word;
}
.row + .row {
margin-block-start: var(--s0);
}
/* Two columns, but only while they stay wide enough to read. */
.two-column {
columns: 2 24ch;
gap: var(--s1);
}
.noselect {
user-select: none;
-webkit-user-select: none;
}
/* 5. Controls ============================================================== */
/* Controls do not inherit the page font on their own — this is what keeps
fields and buttons from turning into system UI in each browser. */
button,
input,
select,
textarea {
font: inherit;
color: inherit;
background-color: var(--background);
border: var(--border-width) solid var(--border);
border-radius: var(--radius);
padding: 0.1em 0.5em;
max-inline-size: 100%;
}
label {
display: block;
}
input[type="text"] {
/* min-inline-size lets it shrink inside a .flex-row instead of overflowing */
inline-size: 100%;
min-inline-size: 0;
}
textarea {
display: block;
inline-size: 100%;
min-block-size: 4lh;
resize: vertical;
white-space: pre-wrap;
}
/* A block textarea leaves its button hard against it otherwise. */
textarea + button,
textarea + input[type="submit"] {
margin-block-start: var(--s-1);
}
/* Editors sized against the viewport rather than a row count. */
.editor {
block-size: 20vh;
}
.editor-tall {
block-size: 60vh;
}
select {
cursor: pointer;
}
button,
input[type="button"],
input[type="submit"],
input[type="reset"] {
min-inline-size: 8ch;
padding-inline: var(--s0);
text-align: center;
cursor: pointer;
}
button:hover,
input[type="button"]:hover,
input[type="submit"]:hover,
input[type="reset"]:hover {
background-color: var(--color);
color: var(--background);
}
button:active,
input[type="button"]:active,
input[type="submit"]:active,
input[type="reset"]:active {
translate: 0 1px;
}
/* The file input itself is a bare label for its button. */
input[type="file"] {
border: none;
background: none;
padding: 0;
cursor: pointer;
}
/* Kept in its own rule: an unsupported pseudo-element would invalidate the
whole selector list if it shared one with the buttons above. */
::file-selector-button {
font: inherit;
color: inherit;
background-color: var(--background);
border: var(--border-width) solid var(--border);
border-radius: var(--radius);
min-inline-size: 8ch;
padding: 0.1em var(--s0);
margin-inline-end: var(--s-1);
cursor: pointer;
}
::file-selector-button:hover {
background-color: var(--color);
color: var(--background);
}
input[type="checkbox"] {
appearance: none;
inline-size: 3ch;
padding: 0;
border: none;
background: none;
text-align: center;
cursor: pointer;
}
input[type="checkbox"]::after {
content: "[ ]";
}
input[type="checkbox"]:checked::after {
content: "[x]";
}
fieldset {
/* min-inline-size overrides the min-content floor browsers give fieldsets,
which otherwise makes them overflow flex and grid containers. */
min-inline-size: 0;
margin: 0;
border: var(--border-width) solid var(--border);
border-radius: var(--radius);
padding: var(--s-1) var(--s0);
}
legend {
padding-inline: var(--s-2);
}
legend::before {
content: "( ";
}
legend::after {
content: " )";
}
summary {
cursor: help;
}
summary::marker {
content: "> ";
}
details[open] > summary::marker {
content: "x ";
}
/* A heading is allowed to be a summary without breaking the line. */
summary > :is(h1, h2, h3, h4, h5, h6) {
display: inline;
}
/* 6. Components ============================================================ */
/* -- Site header --------------------------------------------------------- */
header {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--s0);
}
header > h1 {
margin-block: 0;
}
/* -- Site footer --------------------------------------------------------- */
/* Markers stay outside the text column, in a gutter wide enough for the
longest of them ("==> "). That way the arrow slides right on hover and
focus while the link itself never moves. The column gap is measured in the
same unit, so the second column's markers cannot reach the first. */
footer ul {
columns: 2 20ch;
padding-inline-start: 4ch;
column-gap: 5ch;
}
footer li::marker {
content: "> ";
}
footer li:has(a:hover)::marker {
content: " >";
}
footer li:has(a:focus)::marker {
content: "==> ";
}
footer li:has(a:hover:focus)::marker {
content: "==>";
}
/* NavLink marks the current page for us. */
footer li:has(a.active)::marker {
content: "* ";
}
/* -- Panels (bordered boxes with a title) -------------------------------- */
.panel {
margin-block: var(--s1);
border: 2px dashed var(--border);
padding: var(--s-1) var(--s0);
}
/* -- Log ------------------------------------------------------------------ */
#log {
margin-block-start: var(--s0);
}
.error::before,
.info::before,
.debug::before {
/* not selectable, so copying a log line copies the message only */
user-select: none;
-webkit-user-select: none;
}
.error::before {
content: "ERR: ";
color: var(--error);
}
.info::before {
content: "INF: ";
color: var(--info);
}
.debug::before {
content: "DBG: ";
color: var(--info);
}
/* Debug lines stay hidden until the checkbox next to the log is ticked. */
label:has(> #debug:not(:checked)) + #log .debug {
display: none;
}
/* Earlier runs, folded away and dimmed. */
.history {
opacity: 30%;
}
* + .history {
margin-block-start: var(--s1);
}
/* -- Rendered JSON -------------------------------------------------------- */
li.dash {
list-style-type: "- ";
}
span.name {
opacity: 50%;
}
.json-true {
color: var(--green);
}
.json-false {
color: var(--error);
}
.json-number {
color: var(--info);
}
.json-null {
opacity: 50%;
}
/* -- Dialogs -------------------------------------------------------------- */
dialog {
background-color: transparent;
color: var(--color);
border: none;
padding: 0;
max-inline-size: min(100% - var(--s2), 60ch);
}
dialog::backdrop {
backdrop-filter: blur(calc(var(--s1) / 20));
}
/* The panel inside supplies the surface, so the dialog itself can stay bare. */
dialog > .panel {
margin-block: 0;
background-color: var(--background);
}
dialog > .panel > legend {
background-color: var(--background);
border-radius: var(--radius);
}
/* -- Tables --------------------------------------------------------------- */
table {
inline-size: 100%;
border-collapse: collapse;
}
:is(th, td) {
text-align: start;
padding: var(--s-2) var(--s-1);
padding-inline-start: 0;
border-block-end: var(--border-width) solid var(--border);
vertical-align: baseline;
}
tbody tr:last-child :is(th, td) {
border-block-end: none;
}
/* A table with more columns than the page has room for scrolls inside its own
box rather than pushing the whole page sideways. */
.table-scroll {
overflow-x: auto;
}
/* -- rvrb stats ----------------------------------------------------------- */
.status-up {
color: var(--green);
}
.status-idle {
color: var(--info);
}
.status-down {
color: var(--error);
}
.muted {
opacity: 50%;
}
.now-playing {
display: flex;
flex-wrap: wrap;
gap: var(--s0);
align-items: start;
}
/* Overrides the block margin and auto-centring the base `img` rule gives every
image, which would otherwise push the art away from the track it belongs to. */
.now-playing .album-art {
margin: 0;
flex: none;
inline-size: 8em;
block-size: 8em;
border-radius: var(--radius);
}
.now-playing > div {
flex: 1 1 20ch;
}
progress {
inline-size: 100%;
block-size: var(--s-1);
}
/* Counts big enough to read at a glance, laid out as many per row as fit. */
.stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(10ch, 1fr));
gap: var(--s-1);
margin-block: 0;
}
.stats dt {
opacity: 60%;
}
.stats dd {
margin-inline-start: 0;
font-size: calc(1em * var(--ratio));
}
/* Plays per day. One
per day, so a quiet day is an empty column rather
than a gap - see Rvrb.Stats.plays_per_day/1.
A grid rather than a flex row, because a bar's height is a percentage and
can only resolve against a track with a height of its own. Sharing one 6em
box with the labels made the two compete for it: flex shrank whichever bar
was tall enough to overflow *and* the labels under it, so the tallest bars
ended up hanging below the baseline the rest stood on. */
.bars {
/* A container, so the labels below can react to how much room each column
actually got rather than to the width of the viewport. */
container-type: inline-size;
display: grid;
grid-auto-flow: column;
grid-auto-columns: 1fr;
/* the bar track, then a row each for the count and the weekday */
grid-template-rows: 6em auto auto;
/* Column gap only: the labels belong against the bar they describe. */
gap: 0 var(--border-width);
margin-block: 0;
padding-inline: 0;
list-style: none;
}
/* subgrid, so every column's count and weekday line up on the same two rows
however tall the bar above them is. */
.bars li {
display: grid;
grid-row: span 3;
grid-template-rows: subgrid;
align-items: end;
justify-items: center;
}
.bars .bar {
inline-size: 100%;
background-color: var(--color);
}
.bars :is(.bar-label, .bar-day) {
font-size: 0.7em;
opacity: 60%;
}
/* Two weeks of columns in one text column: the day has to give way before the
count does, since the count is the number and the day is the axis. */
.bars .bar-day {
overflow: hidden;
max-inline-size: 100%;
/* Clipped rather than wrapped: three letters over two lines would push the
bars up and misalign every column. */
white-space: nowrap;
}
.bars .day-initial {
display: none;
}
/* "Thu" wants about 3.2ch, so a fortnight of them needs roughly this much
chart before the columns stop being wide enough to hold three letters. On a
phone they don't, and the day falls back to its initial rather than being
clipped mid-letter. */
@container (width < 21rem) {
.bars .day-abbrev {
display: none;
}
.bars .day-initial {
display: inline;
}
}
/* -- Storage ------------------------------------------------------------- */
.grid-stores {
display: grid;
grid-template-columns: 1fr auto auto;
align-items: center;
gap: var(--s-1) var(--s0);
}
/* -- Warframe drops ------------------------------------------------------- */
/* Chances are compared down the column, so they align right and shrink to fit. */
.chance {
text-align: end;
font-variant-numeric: tabular-nums;
inline-size: 1%;
white-space: nowrap;
}
/* Up to forty names, so columns rather than one long list. Grid rather than
`columns`, which fragments: a name too long for its column carried its last
word over into the next one. A grid item cannot break across columns, so a
long name wraps inside its own cell instead, and filling row by row puts the
closest match first in reading order. 26ch fits nine names in ten on one
line while still leaving room for a second column; the min() caps that floor
at the container, which a bare minmax() would overflow on a phone. */
.matches {
list-style: none;
margin: 0;
padding: 0;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(min(26ch, 100%), 1fr));
/* The row gap is what keeps two names apart once they are narrow enough to
wrap onto a second line each. */
gap: var(--s-2) var(--s0);
}
.matches a.active {
font-weight: 700;
}
.matches a.active::before {
content: "> ";
}
/* Follows the search box or the match list, neither of which leaves room under
itself. */
.item-name {
margin-block-start: var(--s1);
}
/* Labels the table under it. Outside .table-scroll on purpose: inside, it
scrolled out of sight on a narrow screen. */
.relic-lead {
margin-block: var(--s-2) 0;
}
/* An offsite link sitting beside a name that already links within the site.
Dimmed and smaller so the two read as different kinds of thing, and it does
not compete with the name it follows. */
.wiki {
font-size: 0.8em;
font-weight: 400;
opacity: 50%;
white-space: nowrap;
}
.wiki:hover,
.wiki:focus {
opacity: 100%;
}
/* Each relic is a heading and its own small table. */
.relic + .relic {
margin-block-start: var(--s0);
padding-block-start: var(--s0);
border-block-start: var(--rule);
}
/* Name, rarity and both chances on one line, stacking when there is no room. */
.relic > h3 {
display: flex;
flex-wrap: wrap;
gap: var(--s-1);
align-items: baseline;
font-size: 1em;
}
/* -- Send (peer-to-peer file transfer) ------------------------------------ */
/* A target big enough to aim a dragged file at, dashed so it reads as a place to drop rather
than a box with something in it. */
.dropzone {
border: var(--border-width) dashed var(--border);
border-radius: var(--radius);
padding: var(--s0);
text-align: center;
}
.dropzone > p {
margin-block-end: 0;
}
/* The only feedback that the drop will land here. Translucent so it works over either scheme
without a token of its own. */
.dropzone.over {
background-color: color-mix(in srgb, var(--color) 12%, transparent);
}
.transfers {
list-style: none;
margin: 0;
padding: 0;
}
.transfers:not(:empty) {
margin-block-start: var(--s0);
}
/* Name and size on one line, bar and status under them: two rows of two, with the name free to
wrap and the two right-hand cells sized to their text. */
.transfer {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
column-gap: var(--s0);
align-items: baseline;
}
.transfer + .transfer {
margin-block-start: var(--s-1);
}
/* A file name is not prose and has no spaces to break at. */
.transfer-name {
overflow-wrap: anywhere;
}
.transfer-size,
.transfer-status {
white-space: nowrap;
font-variant-numeric: tabular-nums;
}
/* The one control that arrives with a colour of its own: the browser's accent is nowhere else
on this site. */
.transfer progress {
inline-size: 100%;
accent-color: var(--color);
}
.transfer.failed .transfer-status {
color: var(--error);
}
/* Both sides in one column, oldest first, told apart by the name on each rather than by which
side of the panel they sit on: a bubble layout needs a colour or a width to spend, and this
page has an 80ch measure and no colour. */
.messages {
list-style: none;
margin: 0 0 var(--s0);
padding: 0;
max-block-size: 60vh;
overflow-y: auto;
}
.messages:empty {
display: none;
}
/* Who, when and a copy button on one line, the text under them across the whole row. */
.message {
display: grid;
grid-template-columns: auto 1fr auto;
column-gap: var(--s0);
align-items: baseline;
}
.message + .message {
border-block-start: var(--rule);
margin-block-start: var(--s-1);
padding-block-start: var(--s-1);
}
.message-from {
font-weight: bold;
}
.message.from-you .message-from {
font-weight: normal;
}
.message time {
font-variant-numeric: tabular-nums;
}
.message-copy {
font-size: 0.8em;
}
/* What was typed, as it was typed: its own line breaks, and a pasted URL or token with no spaces
still wrapping instead of pushing the panel wide. */
.message-text {
grid-column: 1 / -1;
margin: 0;
white-space: pre-wrap;
overflow-wrap: anywhere;
}
/* -- Note (rich text editor) ---------------------------------------------- */
.note-toolbar {
display: flex;
flex-wrap: wrap;
gap: var(--s-1);
margin-block-end: var(--s-1);
}
/* One letter or two words each: the 8ch every other button gets would make this a row of gaps. */
.note-toolbar button {
min-inline-size: 4ch;
padding-inline: var(--s-1);
}
/* On for the text under the caret: drawn the way a hovered button is, since that is the only
emphasis a button here has. */
.note-toolbar button[aria-pressed="true"] {
background-color: var(--color);
color: var(--background);
}
/* Dressed as the textarea it replaced. */
.note-editor {
border: var(--border-width) solid var(--border);
border-radius: var(--radius);
padding: 0.1em 0.5em;
overflow-y: auto;
overflow-wrap: break-word;
cursor: text;
}
/* A nested list is one more line of its parent, not a new paragraph. */
.note-editor :is(ul, ol) :is(ul, ol) {
margin-block: 0;
}
/* -- Scroll shortcuts ------------------------------------------------------ */
/* Docked to the viewport edge rather than the page flow, so it stays
reachable no matter how far down a long page (e.g. Query's history and
favorites) you've scrolled. */
.scroll-nav {
position: fixed;
inset-inline-end: var(--s0);
inset-block-start: 50%;
transform: translateY(-50%);
display: flex;
flex-direction: column;
gap: var(--s-1);
z-index: 100;
}
.scroll-nav button {
line-height: 1;
padding: var(--s-1);
}
/* 7. Media ================================================================= */
#logo {
block-size: 5em;
flex: none;
}
img {
display: block;
max-inline-size: 100%;
block-size: auto;
margin-inline: auto;
margin-block: var(--s0);
}
video {
display: block;
inline-size: 100%;
block-size: auto;
border-radius: var(--radius);
}
#qrcode {
margin-block: var(--s0);
text-align: center;
}
#qrcode :is(canvas, table) {
margin-inline: auto;
}