On this page
The stylesheet
One stylesheet for every page. A readable column is on the left and the file tree on the right, in light or dark from the reader's setting. Prose is the page and code is the aside, in a quieter panel, so a file reads as its prose first.
It runs in the order of the page, from the tokens and the frame to the bar, the text, the code and the contents. Each part says why it's built the way it is.
#Layers
Three cascade layers state each rule's precedence, whatever its specificity. Tokens only define values. The base styles bare elements. Components style everything with a class, and always beat the base, so an element's default can't win over a part of the page.
Every rule is in a layer, since a rule outside any layer would beat all three. Only
@property and @view-transition stay outside, because they're global. A test checks this
(style.test.ts).
Inside a layer, a component nests its parts and its media queries with &. A rule nests only
under a single selector. Under a list, & becomes :is(), which takes the highest
specificity in the list for every branch. Rules that tie together parts of the page, like the
dock checkbox's, stay flat.
@layer tokens, base, components;#Tokens
Colours, widths, fonts, the type scale, the radii, the chevrons and the motion are set once, here. The dark palette redefines the same names. Every size is in rem, so the whole page scales as one, as browser zoom would. A reader who asks for less motion gets none, since every duration becomes zero.
Colour
Every colour is in OKLCH, where equal lightness looks equally light whatever the hue. The neutrals are one warm hue at a few lightness steps. Dark mode flips the steps and keeps the hues. Ink and paper stop short of black and white, at about 13:1 in light and 11:1 in dark. Higher contrast glares on a long read, and lower would fail readers who need it.
The accent marks links, focus and the reader's place. Its lightness and chroma are fixed, and
only its hue, --accent-h, varies. So every hue reads the same and passes AA in both modes.
Each project gets its own hue from its name (accent.ts), and the page sets it
on <html>.
Code takes its colours from the same tokens. Each kind of token has one, like --code-keyword,
in the page's warm ink (highlight.ts). They don't follow the accent, so a
project's colour never changes how its code reads.
Type
Five sizes. --text-s is for the bar, the rail and code, --text-xs for labels, and the
other three for the text and its headings. Three line heights go with them. font-size-adjust
gives code the text's x-height, whatever the two fonts are, so code needs no size of its own.
@layer tokens {
:root {
color-scheme: light dark;
--accent-h: 75;
--ink: oklch(0.29 0.012 70);
--muted: oklch(0.52 0.012 70);
--paper: oklch(0.975 0.006 85);
--panel: oklch(0.95 0.007 85);
--rule: oklch(0.9 0.009 85);
--accent: oklch(0.5 0.13 var(--accent-h));
--link: var(--accent);
--selection: oklch(0.75 0.13 var(--accent-h) / 0.3);
--live: oklch(0.63 0.16 148);
--shade: oklch(0.2 0.01 70 / 0.2);
--measure: 44rem;
--rail: 16rem;
--bar: 3.5rem;
/* The most room left of the text; the page's own width, not the window's, decides how much. */
--lead: 7rem;
/* 100 columns of code (oxfmt's print width), plus a gutter and padding, in the code font. */
--code-width: 60rem;
--gutter-ink: var(--muted);
--code-ink: var(--ink);
--code-comment: oklch(0.52 0.02 85);
--code-keyword: oklch(0.49 0.13 40);
--code-string: oklch(0.5 0.1 133);
--code-function: oklch(0.46 0.12 257);
--code-constant: oklch(0.5 0.13 312);
--code-punctuation: oklch(0.52 0.018 83);
--code-link: var(--code-function);
--text-xs: 0.75rem;
--text-s: 0.875rem;
--text-m: 1rem;
--text-l: 1.25rem;
--text-xl: 1.75rem;
--leading-tight: 1.25;
--leading-snug: 1.4;
--leading: 1.6;
--radius: 0.5rem;
--radius-s: 0.25rem;
--sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
--mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
/* The chevron that turns as a folder or a code run opens, and the Docs menu's, which points down. */
--chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M6 4l4 4-4 4' fill='none' stroke='black' stroke-width='1.5'/%3E%3C/svg%3E");
--chevron-down: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M4 6l4 4 4-4' fill='none' stroke='black' stroke-width='1.5'/%3E%3C/svg%3E");
/* How long the tree takes to dock or slide in, and a chevron to turn. */
--slide: 0.2s;
--turn: 0.1s;
font-family: var(--sans);
font-size-adjust: ex-height from-font;
line-height: var(--leading);
/* 16px up to a 1280px window, then growing to 20px at 2560px. Every size here is in rem, so the
whole page scales as one, as browser zoom would, and a wide screen holds more page and less
margin. */
font-size: clamp(1rem, 0.75rem + 0.3125vw, 1.25rem);
}
@media (prefers-color-scheme: dark) {
:root {
--ink: oklch(0.86 0.012 80);
--muted: oklch(0.68 0.012 75);
--paper: oklch(0.215 0.006 70);
--panel: oklch(0.25 0.006 70);
--rule: oklch(0.32 0.008 75);
--accent: oklch(0.78 0.1 var(--accent-h));
--selection: oklch(0.6 0.1 var(--accent-h) / 0.35);
--live: oklch(0.7 0.15 148);
--shade: oklch(0 0 0 / 0.45);
--code-comment: oklch(0.63 0.019 81);
--code-keyword: oklch(0.74 0.106 52);
--code-string: oklch(0.78 0.09 126);
--code-function: oklch(0.77 0.08 253);
--code-constant: oklch(0.77 0.09 314);
--code-punctuation: oklch(0.7 0.019 81);
}
}
@media (prefers-reduced-motion: reduce) {
:root {
--slide: 0s;
--turn: 0s;
}
}
}#Moving between pages
A move to another page is a cross-document view transition. The rail is held still, so only the page changes, in a quick fade.
@view-transition {
navigation: auto;
}
@layer base {
::view-transition-old(rail),
::view-transition-new(rail) {
animation: none;
}
::view-transition-old(root),
::view-transition-new(root) {
animation-duration: 0.12s;
}
}#The base
Box sizing, the page's paper and ink, links, selection, focus and the code font.
Anything focused from the keyboard gets the same ring in the accent. Scrollbars take the page's
colours, so the rail's doesn't show the platform's grey on the warm panel. Text marked unseen
is for a screen reader alone, like the word that names a code run's header.
Buttons and popovers start bare. A button takes the page's type with no box, and a popover keeps the browser's placement without its border, padding or colours. So a component sets only its own look, and doesn't first undo the browser's.
@layer base {
* {
box-sizing: border-box;
}
:root {
scrollbar-color: color-mix(in oklch, var(--muted) 45%, transparent) transparent;
}
:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
.unseen {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
}
body {
margin: 0;
background: var(--paper);
color: var(--ink);
}
a {
color: var(--link);
text-decoration-thickness: 1px;
text-underline-offset: 2px;
}
::selection {
background: var(--selection);
}
code,
pre {
font-family: var(--mono);
}
/* Buttons take the page's type and nothing else, so a component sets only its own look. */
button {
font: inherit;
background: none;
border: 0;
cursor: pointer;
}
/* A popover keeps the browser's place and size, but none of its box or colours. */
[popover] {
margin: 0;
padding: 0;
border: 0;
background: none;
color: inherit;
}
}#The frame
The bar and the page share one container, so both take their margins from its width.
--docked is how much of that width the docked tree holds. It animates, so the column and the
margins move together.
@property --docked {
syntax: "<length>";
inherits: true;
initial-value: 16rem;
}
@layer components {
.shell {
container-type: inline-size;
--docked: var(--rail);
--side: clamp(2rem, calc((100cqw - var(--docked) - var(--measure)) / 2), var(--lead));
transition: --docked var(--slide) ease;
}
.layout {
display: grid;
grid-template-columns: minmax(0, 1fr) var(--docked);
min-height: calc(100vh - var(--bar));
overflow-x: clip;
}
.rail {
position: sticky;
top: var(--bar);
height: calc(100vh - var(--bar));
overflow-y: auto;
padding: 0.75rem 0 0;
display: flex;
flex-direction: column;
border-left: 1px solid var(--rule);
background: var(--panel);
font-size: var(--text-s);
line-height: var(--leading-snug);
view-transition-name: rail;
--edge: 0.9rem;
--step: 0.75rem;
--twisty: 0.9rem;
--gap: 0.35rem;
}
}#The file tree
Rows span the rail's full width, so a highlight does too. Each row indents itself by its depth. A folder's chevron sits under the start of its parent's name. A file leaves the chevron's slot empty, so names at one level line up. The top-level chevrons line up with the project's name.
The current file has a mark in the accent at its left edge, so it differs from a hovered row. A row's focus ring sits inside it, since the rail clips anything outside.
@layer components {
.rail {
& ul {
list-style: none;
margin: 0;
padding: 0;
}
& a {
color: var(--ink);
text-decoration: none;
overflow-wrap: anywhere;
}
& .rail-project {
display: block;
padding: 0.2rem var(--edge);
margin-bottom: 0.4rem;
font-weight: 600;
}
& a.file,
& summary {
display: flex;
align-items: center;
gap: var(--gap);
padding: 0.2rem var(--edge) 0.2rem calc(var(--edge) + var(--depth) * var(--step));
}
& a.file {
padding-left: calc(var(--edge) + var(--depth) * var(--step) + var(--twisty) + var(--gap));
}
& a.file:hover,
& summary:hover,
& .rail-project:hover {
background: var(--rule);
}
& [aria-current] {
background: var(--rule);
box-shadow: inset 2px 0 var(--accent);
font-weight: 600;
}
& :focus-visible {
outline-offset: -2px;
}
& summary {
cursor: pointer;
list-style: none;
font-weight: 500;
}
}
}#Guides
A guide runs down from each open folder's chevron, beside its contents. The guides on the way to the current page are darker, so the eye can follow the path. They're only vertical. Elbows to each row would make a diagram of the tree, not a list of files.
@layer components {
.rail {
& details > ul {
position: relative;
&::before {
content: "";
position: absolute;
inset-block: 0;
left: calc(var(--edge) + var(--depth) * var(--step) + var(--twisty) / 2);
border-left: 1px solid var(--rule);
pointer-events: none;
}
}
& details:has([aria-current]) > ul::before {
border-left-color: color-mix(in srgb, var(--muted) 60%, transparent);
}
& summary {
&::-webkit-details-marker {
display: none;
}
&::before {
content: "";
flex: none;
width: var(--twisty);
height: var(--twisty);
background: currentColor;
opacity: 0.4;
mask: var(--chevron) center / 80% no-repeat;
transition: transform var(--turn);
}
}
& details[open] > summary::before {
transform: rotate(90deg);
}
}
}#Docked or a popover
The rail is docked beside the text while the text keeps its measure beside it. The bar's icon, a checkbox's label, folds it away. Below 62rem, it's a popover of the same width under the bar. A popover gives Esc, a click outside and focus return without script.
The checkbox sits in the bar beside its label, and the page reads its state with :has(). So it
stays inside the bar's landmark, and the label shows its focus. Below 62rem, where the popover's
button takes over, the checkbox is out of the tab order.
Opening and closing both animate. The docked column's width changes, and the popover slides in
over a backdrop. @starting-style lets it animate from display: none.
@layer components {
.ctl {
position: absolute;
opacity: 0;
pointer-events: none;
@media (max-width: 62rem) {
display: none;
}
}
.shell:has(#dock:checked) {
--docked: 0rem;
& .rail {
visibility: hidden;
transition: visibility 0s var(--slide);
}
}
.shell {
@media (max-width: 62rem) {
--docked: 0rem;
}
}
.rail {
min-width: var(--rail);
&::backdrop {
background: transparent;
}
/* A popover left open while the window grows stays in the top layer, so it's pinned where the
docked tree sits. */
@media not (max-width: 62rem) {
&:popover-open {
position: fixed;
inset: var(--bar) 0 0 auto;
width: var(--rail);
height: auto;
}
}
@media (max-width: 62rem) {
position: fixed;
inset: var(--bar) 0 0 auto;
height: auto;
width: min(var(--rail), 85vw);
min-width: 0;
box-shadow: 0 0 2rem var(--shade);
transform: translateX(100%);
transition:
transform var(--slide) ease,
display var(--slide) allow-discrete,
overlay var(--slide) allow-discrete;
.layout &:not(:popover-open) {
display: none;
}
&:popover-open {
display: flex;
transform: none;
@starting-style {
transform: translateX(100%);
}
}
&::backdrop {
transition:
background var(--slide) ease,
display var(--slide) allow-discrete,
overlay var(--slide) allow-discrete;
}
&:popover-open::backdrop {
background: var(--shade);
@starting-style {
background: transparent;
}
}
}
}
.rail-toggle {
font: inherit;
color: var(--ink);
background: var(--panel);
border: 1px solid var(--rule);
border-radius: var(--radius);
padding: 0.3rem 0.45rem;
cursor: pointer;
line-height: 0;
display: inline-flex;
&.pop {
display: none;
}
@media (max-width: 62rem) {
&.dock {
display: none;
}
&.pop {
display: inline-flex;
}
}
}
/* The checkbox is hidden, so its label shows its focus. */
.bar:has(#dock:focus-visible) .dock {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
}@layer components {
.bar {
position: sticky;
top: 0;
z-index: 3;
height: var(--bar);
display: flex;
gap: 1rem;
align-items: center;
justify-content: space-between;
padding: 0 1rem 0 var(--side);
background: var(--paper);
border-bottom: 1px solid var(--rule);
font-size: var(--text-s);
& .project {
color: var(--ink);
font-size: var(--text-m);
font-weight: 600;
text-decoration: none;
white-space: nowrap;
}
}
.page {
min-width: 0;
}
.bar-mode {
margin-left: auto;
}
}@layer components {
.docs-toggle {
display: none;
@media (max-width: 62rem) {
display: inline-flex;
color: var(--ink);
padding: 0.3rem 0.4rem;
&::after {
content: "";
width: 0.7em;
margin-left: 0.3em;
background: currentColor;
opacity: 0.5;
mask: var(--chevron-down) center / contain no-repeat;
}
}
}
.bar .docs {
@media not (max-width: 62rem) {
display: flex;
gap: 1.25rem;
position: static;
inset: auto;
margin: 0 0 0 1rem;
overflow: visible;
min-width: 0;
}
& a {
color: var(--muted);
text-decoration: none;
white-space: nowrap;
}
& a:hover,
& [aria-current] {
color: var(--ink);
}
& [aria-current] {
font-weight: 600;
}
@media (max-width: 62rem) {
&:popover-open {
position: fixed;
inset: var(--bar) auto auto var(--side);
display: flex;
flex-direction: column;
min-width: 12rem;
padding: 0.4rem 0;
background: var(--paper);
color: var(--ink);
border: 1px solid var(--rule);
border-radius: var(--radius);
box-shadow: 0 0.5rem 1.5rem var(--shade);
& a {
padding: 0.4rem 1rem;
}
& a:hover {
background: var(--panel);
}
}
}
}
}#On a phone
On a phone, the name, Docs, the switch and the tree's icon share 390px. Tighter gaps and buttons keep them on one line. The switch's buttons tighten with the rest of their rule, in Around the text.
Below 22rem the name leaves the bar, since the breadcrumb just below starts with the same link. That's how the page fits 320px, a 1280px screen at 400% zoom, without scrolling sideways.
@layer components {
@media (max-width: 30rem) {
.bar {
gap: 0.5rem;
padding-right: 0.75rem;
}
.docs-toggle {
padding-inline: 0.25rem;
}
}
@media (max-width: 24rem) {
.shell {
--side: 1rem;
}
.docs-toggle::after {
display: none;
}
}
@media (max-width: 22rem) {
.bar .project {
display: none;
}
}
}#Around the text
The breadcrumb and Open in editor above the text, the rail's footer, and the controls in the bar.
The breadcrumb is small and muted, so it reads as the page's header, not as the start of its text. More space falls below it than above, which sets the bar, the breadcrumb and the title apart as three steps.
Skip to the text is the page's first link, so a keyboard reader needn't tab through the bar. It's hidden until it has focus, and then shows over the bar's corner.
@layer components {
.skip {
position: fixed;
top: 0.5rem;
left: 0.5rem;
z-index: 10;
padding: 0.4rem 0.8rem;
background: var(--paper);
border: 1px solid var(--rule);
border-radius: var(--radius);
&:not(:focus) {
translate: 0 calc(-100% - 1rem);
}
}
.where {
display: flex;
flex-wrap: wrap;
gap: 0.25rem 1rem;
justify-content: space-between;
font-size: var(--text-s);
color: var(--muted);
margin-bottom: 2.5rem;
& a {
color: inherit;
text-decoration: none;
&:hover {
color: var(--ink);
text-decoration: underline;
}
}
& .editor {
white-space: nowrap;
}
}
.rail-foot {
position: sticky;
bottom: 0;
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
margin-top: auto;
padding: 0.6rem var(--edge);
background: var(--panel);
border-top: 1px solid var(--rule);
color: var(--muted);
font-size: var(--text-xs);
& a {
color: inherit;
&:hover {
color: var(--ink);
}
}
& .snapshot {
font-family: var(--mono);
}
& .repo {
display: inline-flex;
}
}
.rail > .rail-tree {
margin-bottom: 1rem;
}
.live {
display: inline-flex;
align-items: center;
gap: 0.4rem;
& .dot {
width: 0.5rem;
height: 0.5rem;
border-radius: 50%;
background: var(--live);
.offline & {
background: var(--muted);
opacity: 0.5;
}
}
}
.crumbs {
min-width: 0;
overflow-wrap: anywhere;
& .sep {
color: var(--muted);
margin: 0 0.35rem;
}
& [aria-current] {
font-weight: 600;
color: var(--ink);
}
}
.mode {
display: inline-flex;
padding: 2px;
gap: 2px;
background: var(--panel);
border: 1px solid var(--rule);
border-radius: var(--radius);
& button {
color: var(--muted);
/* The switch's radius less its border and padding, so the corners nest. */
border-radius: calc(var(--radius) - 3px);
padding: 0.15rem 0.7rem;
white-space: nowrap;
@media (max-width: 30rem) {
padding: 0.15rem 0.45rem;
}
&:hover {
color: var(--ink);
}
&:disabled {
cursor: default;
color: var(--muted);
background: none;
opacity: 0.6;
}
}
:root:not(.prose-only) & [data-mode="code"],
.prose-only & [data-mode="prose"] {
background: var(--rule);
color: var(--ink);
}
}
.no-prose {
color: var(--muted);
font-style: italic;
margin-block: 0 0.5rem;
}
}#The column
Every page shares one centre line. Prose keeps the reading measure, and code is wider on both sides of it. Code runs widen to 100 columns, oxfmt's print width, so code wraps only on a narrow window. Prose never moves between a doc and a source file.
The text's side margins come from the page's own width. They're equal while the page is
narrow. Then the left stops growing at --lead, and the extra goes to the right.
@layer components {
main {
position: relative;
margin: 0;
/* The space above the breadcrumb, which the contents beside the page start at too. */
--top: 2rem;
padding: var(--top) var(--side) 4rem;
/* How far a table or code sample may widen; the text's width while the contents are beside it
on a doc, so they don't run under it. */
--wide: var(--code-width);
/* What the contents sit beside: the text on a doc, the code on a source file. */
--toc-after: var(--measure);
&.has-code {
--toc-after: var(--code-width);
}
& > * {
max-width: var(--measure);
margin-inline: 0;
}
}
}#Prose
Headings, lists, tables and quotes in a doc or a prose comment.
@layer components {
.prose {
overflow-wrap: anywhere;
& h1,
& h2,
& h3 {
line-height: var(--leading-tight);
text-wrap: balance;
}
& h1 {
font-size: var(--text-xl);
}
& h2 {
font-size: var(--text-l);
margin-top: 2rem;
}
& h3 {
font-size: var(--text-m);
}
& p,
& li {
text-wrap: pretty;
}
& :not(pre) > code {
background: var(--panel);
border-radius: var(--radius-s);
padding: 0.1em 0.3em;
}
}
}#Wide tables and code samples
A table or a code sample in the prose may grow past the text, as far as a code run does or the page allows. It's a grid or code to scan, not lines to read, so the measure isn't for it. Past that, a table scrolls and code wraps. A short sample keeps the text's width, as a run does.
@layer components {
.prose {
& table,
& pre.highlighted {
width: max-content;
max-width: min(var(--wide), calc(100cqw - var(--docked) - 2 * var(--side)));
}
& pre.highlighted {
min-width: 100%;
}
& table {
border-collapse: collapse;
display: block;
overflow-x: auto;
}
& th,
& td {
border: 1px solid var(--rule);
padding: 0.3rem 0.6rem;
text-align: left;
}
& blockquote {
margin: 0;
padding-left: 1rem;
border-left: 3px solid var(--rule);
color: var(--muted);
}
}
}#Code
A run of code sits in a quieter panel, with a header that opens and folds it. Lines wrap with a hanging indent instead of scrolling, and tabs are two columns. The file's line numbers sit in a gutter that isn't copied with the code.
@layer components {
pre.highlighted {
color: var(--code-ink);
background: var(--panel);
margin: 0.75rem 0;
padding: 0.75rem 1rem;
border-radius: var(--radius);
font-size: var(--text-s);
line-height: var(--leading-snug);
tab-size: 2;
& code {
display: block;
white-space: normal;
}
& .line {
display: block;
min-height: 1lh;
white-space: pre-wrap;
overflow-wrap: anywhere;
padding-left: 2ch;
text-indent: -2ch;
}
& .comment {
color: var(--code-comment);
}
& .keyword {
color: var(--code-keyword);
}
& .string {
color: var(--code-string);
}
& .function {
color: var(--code-function);
}
& .constant {
color: var(--code-constant);
}
& .punctuation {
color: var(--code-punctuation);
}
/* A sample's language, in the corner. The text wraps around it, so a long first line doesn't
run under it. */
&[data-lang]::before {
/* Shown, but not read out before the code. */
content: attr(data-lang) / "";
float: right;
margin-left: 1rem;
color: var(--muted);
font-family: var(--sans);
font-size: var(--text-xs);
/* A code line's height, so the label centres on the first line. */
line-height: calc(var(--text-s) * var(--leading-snug));
}
& .link {
color: var(--code-link);
text-decoration: underline;
}
& .strong {
font-weight: 600;
}
& .emphasis {
font-style: italic;
}
}
.code {
margin: 0.5rem 0 1.25rem;
border-radius: var(--radius);
overflow: hidden;
background: var(--panel);
font-family: var(--mono);
font-size: var(--text-s);
max-width: calc(100ch + var(--gutter) + 5ch + 2rem);
& pre.highlighted {
margin: 0;
border-radius: 0;
font-size: 1em;
& .line {
position: relative;
padding-left: calc(var(--gutter) + 5ch);
&::before {
counter-increment: line;
content: counter(line);
position: absolute;
left: 0;
width: var(--gutter);
text-indent: 0;
text-align: right;
color: var(--gutter-ink);
user-select: none;
}
}
}
}
.code-head {
display: flex;
width: 100%;
align-items: center;
gap: 0.6rem;
color: var(--gutter-ink);
border-bottom: 1px solid var(--rule);
padding: 0.45rem 1rem;
text-align: left;
&:hover {
color: var(--ink);
}
& .sep {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip-path: inset(50%);
}
& .lang {
margin-left: auto;
}
& .chevron {
width: 0.9em;
height: 0.9em;
flex: none;
background: currentColor;
transform: rotate(90deg);
mask: var(--chevron) center / contain no-repeat;
transition: transform var(--turn);
}
}
/* A run is folded when it's closed in the code view, or not opened in the prose view. */
.code {
:root:not(.prose-only) &.closed pre,
.prose-only &:not(.opened) pre {
display: none;
}
:root:not(.prose-only) &.closed .code-head,
.prose-only &:not(.opened) .code-head {
border-bottom-color: transparent;
}
:root:not(.prose-only) &.closed .chevron,
.prose-only &:not(.opened) .chevron {
transform: none;
}
}
}#Prose comments
Each prose comment on a source page is a block. The # that links to a comment's heading
sits in the margin, and shows when the block is hovered or the link has focus.
@layer components {
.block {
position: relative;
margin: 1.5rem 0 0.5rem;
/* No top or left: an absolute box keeps its place in the line, so the # sits on the heading's
baseline at the heading's size, and is moved into the margin from there. */
& .anchor {
position: absolute;
translate: calc(-100% - 1.25rem);
font-weight: 400;
color: var(--muted);
text-decoration: none;
opacity: 0;
}
&:hover .anchor,
& .anchor:focus {
opacity: 1;
}
& > .prose > :first-child {
margin-top: 0;
}
}
}@layer components {
.group {
margin: 2rem 0 0;
}
.group-label {
margin: 0 0 0.4rem;
color: var(--muted);
font-size: var(--text-xs);
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
}
.listing {
list-style: none;
margin: 0;
padding: 0 0 0 1rem;
& li {
padding: 0.35rem 0;
}
& li.folder > a {
color: var(--ink);
}
& p {
margin: 0.1rem 0 0;
color: var(--muted);
font-size: var(--text-s);
}
}
.listing li > a,
.group .names a {
font-weight: 600;
font-family: var(--mono);
font-size: var(--text-s);
}
.group .names {
margin: 0;
padding-left: 1rem;
& a {
font-weight: 400;
}
& .sep {
color: var(--muted);
}
}
}#Short notes
The line on a missing page, what's ignored in a folder, what was cut from a long file, and a binary file's description.
@layer components {
.missing {
color: var(--muted);
}
.ignored {
color: var(--muted);
font-size: var(--text-s);
margin: 1rem 0;
opacity: 0.75;
& code {
margin-inline-end: 0.5ch;
}
}
.more {
color: var(--muted);
font-style: italic;
}
.binary {
color: var(--muted);
}
.binary-image {
display: block;
max-width: 100%;
height: auto;
margin-block: 1rem;
}
}#On this page
The table of contents folds under the breadcrumb. Where there's room, it's a column beside the page instead. On a doc it sits past the text, and on a source file past the code. The room depends on whether the tree is docked, so each width is given twice.
Beside the page, its label lines up with the breadcrumb. It starts at the same --top and has
the same line box, so the two stay level as either changes.
@layer components {
.prose :is(h2, h3, h4) {
scroll-margin-top: calc(var(--bar) + 1rem);
}
.toc {
& ul {
list-style: none;
margin: 0;
padding: 0;
}
& a {
display: block;
color: var(--muted);
text-decoration: none;
&:hover,
&.here {
color: var(--ink);
}
}
& .toc-3 a {
padding-left: 0.75rem;
}
}
.toc-top {
margin: -1.5rem 0 1.5rem;
font-size: var(--text-s);
& summary {
cursor: pointer;
color: var(--muted);
width: max-content;
&:hover {
color: var(--ink);
}
}
& ul {
margin: 0.5rem 0 0;
padding-left: 0.75rem;
border-left: 1px solid var(--rule);
}
& a {
padding: 0.15rem 0;
}
}
.toc-side {
display: none;
position: absolute;
inset-block: 0;
left: calc(var(--side) + var(--toc-after) + 3rem);
width: 13rem;
font-size: var(--text-s);
line-height: var(--leading-snug);
& > div {
position: sticky;
top: calc(var(--bar) + var(--top));
max-height: calc(100vh - var(--bar) - 2 * var(--top));
overflow-y: auto;
}
& p {
margin: 0 0 0.5rem;
color: var(--muted);
font-size: var(--text-xs);
/* The breadcrumb's line box, so the two centre on one line. */
line-height: calc(var(--text-s) * var(--leading));
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
}
& a {
padding: 0.25rem 0 0.25rem 0.75rem;
border-left: 2px solid var(--rule);
}
& .toc-3 a {
padding-left: 1.5rem;
}
& a.here {
border-left-color: var(--accent);
}
}
@container (min-width: 68rem) {
.shell:has(#dock:checked) main:not(.has-code) {
--wide: var(--measure);
& .toc-side {
display: block;
}
& .toc-top {
display: none;
}
}
}
@container (min-width: 84rem) {
main:not(.has-code) {
--wide: var(--measure);
}
main:not(.has-code) .toc-side,
.shell:has(#dock:checked) main .toc-side {
display: block;
}
main:not(.has-code) .toc-top,
.shell:has(#dock:checked) main .toc-top {
display: none;
}
}
@container (min-width: 100rem) {
main {
& .toc-side {
display: block;
}
& .toc-top {
display: none;
}
}
}
}#Forced colours
In a contrast theme, like Windows', the browser replaces every colour with a few of its own, and drops shadows and background colours. Anything the page marks only that way would vanish.
So the chevrons, which are a mask over a background, keep the text's colour. The current file,
the switch's choice and the section being read get an outline or a border in Highlight.
@layer components {
@media (forced-colors: active) {
.rail summary::before,
.code-head .chevron,
.docs-toggle::after {
forced-color-adjust: none;
background: CanvasText;
}
.rail [aria-current],
:root:not(.prose-only) .mode [data-mode="code"],
.prose-only .mode [data-mode="prose"] {
outline: 2px solid Highlight;
outline-offset: -2px;
}
.toc-side a.here {
border-left-color: Highlight;
}
}
}