git-ents.gitmain
⌘K
foforge
commit e1e69ec
roots: port the pre-redo stylesheet and page shell

Ports the pre-redo look (pre-redo:crates/git-ents-server/src/web/style.css) onto the current markup: a header bar with the git-ents wordmark, a tab-style page nav (dashboard/members/account/effects/redactions/ toolchains/comments/inbox, current page highlighted via a new Tab enum), and a card/table shell wrapping the generic reflection-driven views in render.rs (view/list_table/string_list) so no per-page code needs its own styling hook. Bare dl/ul/form markup in the two custom page families (toolchains, comments) is styled generically too. Dropped from the ported sheet: syntax-highlight tokens, diff views, the clone-URL copy button, and every chrome class for a page family this crate does not have.

Assisted-by: Claude:claude-sonnet-5

Joseph D. Carpinelli · 1 month ago

Reviews

No reviews of this commit yet — record a verdict below.

Start a review

verdict

crates/cli/ents-web/src/lib.rs @@ -106,6 +106,7 @@ //! # }); //! ``` +pub(crate) mod assets; pub mod error; pub mod identity; pub mod pages;
crates/cli/ents-web/src/render.rs @@ -99,10 +99,12 @@ pub fn view<T: Facet<'static>>(value: &T) -> Markup { let rows = fields(value); html! { - dl.entity-view { - @for (name, rendered) in &rows { - dt { (name) } - dd { (rendered) } + div.card { + dl.entity-view { + @for (name, rendered) in &rows { + dt { (name) } + dd { (rendered) } + } } } } @@ -136,21 +138,23 @@ .map(|(_, entity)| fields(entity).into_iter().map(|(name, _)| name).collect()) .unwrap_or_default(); html! { - table.entity-list { - thead { - tr { - th { (id_header) } - @for name in &field_names { - th { (name) } + div.card { + table.entity-list { + thead { + tr { + th { (id_header) } + @for name in &field_names { + th { (name) } + } } } - } - tbody { - @for (id, entity) in rows { - tr { - td { a href=(href_for(id)) { (id) } } - @for (_, rendered) in fields(entity) { - td { (rendered) } + tbody { + @for (id, entity) in rows { + tr { + td { a href=(href_for(id)) { (id) } } + @for (_, rendered) in fields(entity) { + td { (rendered) } + } } } } @@ -165,9 +169,11 @@ #[must_use] pub fn string_list(rows: &[String], href_for: impl Fn(&str) -> String) -> Markup { html! { - ul.string-list { - @for row in rows { - li { a href=(href_for(row)) { (row) } } + div.card { + ul.string-list { + @for row in rows { + li { a href=(href_for(row)) { (row) } } + } } } }
crates/cli/ents-web/src/router.rs @@ -19,10 +19,11 @@ use axum::extract::{Request, State}; use axum::http::{HeaderValue, header}; use axum::middleware::{self, Next}; -use axum::response::Response; +use axum::response::{IntoResponse, Response}; use axum::routing::get; use gix_object::{Find, Write}; +use crate::assets; use crate::pages; use crate::session; use crate::state::AppState; @@ -57,6 +58,7 @@ ) .route("/comments/{id}", get(pages::comments::show::<O>)) .route("/inbox", get(pages::inbox::list::<O>)) + .route("/style.css", get(style)) .layer(middleware::from_fn_with_state( Arc::clone(&state), session_middleware::<O>, @@ -107,6 +109,19 @@ response } +/// `GET /style.css`: the one stylesheet [`pages::layout`]'s `head` links -- +/// the hand-rolled, ported pre-redo sheet (`crate::assets::OVERRIDES`). No +/// session or CSRF gating applies here (the session middleware only ever +/// attaches a session, never rejects a request), and it must not: every +/// page, including one reached before a session exists, needs this to +/// render styled. +async fn style() -> impl IntoResponse { + ( + [(header::CONTENT_TYPE, "text/css; charset=utf-8")], + assets::OVERRIDES, + ) +} + /// Bind a loopback-or-otherwise socket for [`serve_on`], returning the /// listener before any request is served so a caller can read back /// [`std::net::TcpListener::local_addr`] (necessary for `addr`'s port `0`,
crates/cli/ents-web/tests/router.rs @@ -85,6 +85,47 @@ assert_eq!(response.status(), StatusCode::NOT_FOUND); } +/// `GET /style.css` is reachable with no session at all (every other route +/// runs behind the session middleware, but that middleware only ever +/// attaches a session -- it never gates), and serves this crate's own +/// hand-rolled stylesheet. +#[tokio::test] +async fn style_css_is_served_with_no_session_required() { + let state = build_state(FixtureIdentity { + name: "local-user", + key: Keypair::from_seed(1), + }); + let router = ents_web::router(state); + + let response = router + .oneshot( + Request::get("/style.css") + .body(Body::empty()) + .expect("request"), + ) + .await + .expect("in-process call"); + assert_eq!(response.status(), StatusCode::OK); + assert_eq!( + response + .headers() + .get(header::CONTENT_TYPE) + .expect("content-type") + .to_str() + .expect("ascii"), + "text/css; charset=utf-8" + ); + let body = response + .into_body() + .collect() + .await + .expect("body") + .to_bytes(); + let body = String::from_utf8(body.to_vec()).expect("utf8 css"); + assert!(!body.is_empty()); + assert!(body.contains("--color-bg")); +} + /// `roots.web-agnostic`: the dashboard actually renders real content /// in-process, with no socket bound anywhere in this test -- reading the /// body back (rather than only checking the status) is what makes this
crates/cli/ents-web/src/pages/account.rs @@ -52,6 +52,7 @@ .unwrap_or_else(|| html! { p { "no account created yet" } }); Ok(super::layout( + super::Tab::Account, "account", html! { (view)
crates/cli/ents-web/src/pages/comments.rs @@ -34,6 +34,7 @@ { let rows = comment::list(state.refs.as_ref(), &*state.objects())?; Ok(super::layout( + super::Tab::Comments, "comments", html! { ul { @@ -83,6 +84,7 @@ &query.rev, )?; Ok(super::layout( + super::Tab::Comments, &id, html! { dl {
crates/cli/ents-web/src/pages/dashboard.rs @@ -28,16 +28,19 @@ let toolchains = state.refs.iter_prefix("refs/meta/toolchains/")?.count(); Ok(super::layout( + super::Tab::Dashboard, "dashboard", html! { - ul { - li { a href="/members" { "members" } " (" (members) ")" } - li { a href="/account" { "account" } } - li { a href="/effects" { "effects" } " (" (effects) ")" } - li { a href="/redactions" { "redactions" } " (" (redactions) ")" } - li { a href="/toolchains" { "toolchains" } " (" (toolchains) ")" } - li { a href="/comments" { "comments" } " (" (comments) ")" } - li { a href="/inbox" { "inbox" } } + div.card { + ul.string-list { + li { a href="/members" { "members" } span.badge { (members) } } + li { a href="/account" { "account" } } + li { a href="/effects" { "effects" } span.badge { (effects) } } + li { a href="/redactions" { "redactions" } span.badge { (redactions) } } + li { a href="/toolchains" { "toolchains" } span.badge { (toolchains) } } + li { a href="/comments" { "comments" } span.badge { (comments) } } + li { a href="/inbox" { "inbox" } } + } } }, ))
crates/cli/ents-web/src/pages/effects.rs @@ -28,6 +28,7 @@ { let rows = read_all(&state)?; Ok(super::layout( + super::Tab::Effects, "effects", crate::render::list_table(&rows, "name", |id| format!("/effects/{id}")), )) @@ -56,6 +57,7 @@ Err(error) => format!("does not parse: {error}"), }; Ok(super::layout( + super::Tab::Effects, &name, html! { (crate::render::view(&effect))
crates/cli/ents-web/src/pages/inbox.rs @@ -30,6 +30,7 @@ } } Ok(super::layout( + super::Tab::Inbox, "inbox", crate::render::string_list(&rows, |_| "/inbox".to_owned()), ))
crates/cli/ents-web/src/pages/members.rs @@ -24,6 +24,7 @@ { let rows = read_all(&state)?; Ok(super::layout( + super::Tab::Members, "members", crate::render::list_table(&rows, "username", |id| format!("/members/{id}")), )) @@ -47,7 +48,11 @@ .ok_or_else(|| Error::NotFound { what: format!("member {username}"), })?; - Ok(super::layout(&username, crate::render::view(&member))) + Ok(super::layout( + super::Tab::Members, + &username, + crate::render::view(&member), + )) } fn read_all<O: Find>(state: &AppState<O>) -> Result<Vec<(String, Member)>> {
crates/cli/ents-web/src/pages/mod.rs @@ -50,31 +50,61 @@ Ok(commit.tree()) } +/// The tab-nav page families this crate exposes -- one variant per tab in +/// [`layout`]'s nav bar, so a handler can name which tab it renders behind +/// without `layout` re-deriving it from the request path (mirrors +/// `pre-redo:crates/git-ents-server/src/web/pages.rs`'s own `Tab` enum, +/// trimmed to this crate's page families). +#[derive(Clone, Copy, PartialEq, Eq)] +pub(crate) enum Tab { + Dashboard, + Members, + Account, + Effects, + Redactions, + Toolchains, + Comments, + Inbox, +} + /// Wrap `title` and `body` in the one page shell every route renders -/// through -- navigation to every generic and custom page family this -/// crate exposes. -pub(crate) fn layout(title: &str, body: Markup) -> Markup { +/// through -- the pre-redo header bar and tab nav +/// (`pre-redo:crates/git-ents-server/src/web/style.css`'s `.site-nav`/ +/// `.tabs` rules), `active` naming which tab is current. +pub(crate) fn layout(active: Tab, title: &str, body: Markup) -> Markup { html! { (maud::DOCTYPE) - html { + html lang="en" { head { meta charset="utf-8"; + meta name="viewport" content="width=device-width, initial-scale=1"; + meta name="color-scheme" content="light dark"; title { "git ents: " (title) } + link rel="preconnect" href="https://fonts.googleapis.com"; + link rel="preconnect" href="https://fonts.gstatic.com" crossorigin; + link rel="stylesheet" href=(crate::assets::FONTS_HREF); + link rel="stylesheet" href="/style.css"; } body { - nav { - a href="/" { "dashboard" } " | " - a href="/members" { "members" } " | " - a href="/account" { "account" } " | " - a href="/effects" { "effects" } " | " - a href="/redactions" { "redactions" } " | " - a href="/toolchains" { "toolchains" } " | " - a href="/comments" { "comments" } " | " - a href="/inbox" { "inbox" } + nav.site-nav { + div.nav-inner { + a.nav-logo href="/" { span.nav-mark { "✳" } "git-ents" } + } + } + nav.tabs { + a.tab.active[active == Tab::Dashboard] href="/" { "dashboard" } + a.tab.active[active == Tab::Members] href="/members" { "members" } + a.tab.active[active == Tab::Account] href="/account" { "account" } + a.tab.active[active == Tab::Effects] href="/effects" { "effects" } + a.tab.active[active == Tab::Redactions] href="/redactions" { "redactions" } + a.tab.active[active == Tab::Toolchains] href="/toolchains" { "toolchains" } + a.tab.active[active == Tab::Comments] href="/comments" { "comments" } + a.tab.active[active == Tab::Inbox] href="/inbox" { "inbox" } + } + main.content { + div.page-header { h1.page-title { (title) } } + (body) } - hr; - h1 { (title) } - (body) } } }
crates/cli/ents-web/src/pages/redactions.rs @@ -23,6 +23,7 @@ { let rows = read_all(&state)?; Ok(super::layout( + super::Tab::Redactions, "redactions", crate::render::list_table(&rows, "id", |id| format!("/redactions/{id}")), )) @@ -46,7 +47,11 @@ .ok_or_else(|| Error::NotFound { what: format!("redaction {id}"), })?; - Ok(super::layout(&id, crate::render::view(&redaction))) + Ok(super::layout( + super::Tab::Redactions, + &id, + crate::render::view(&redaction), + )) } fn read_all<O: Find>(state: &AppState<O>) -> Result<Vec<(String, Redaction)>> {
crates/cli/ents-web/src/pages/toolchains.rs @@ -29,6 +29,7 @@ { let names = toolchain::list(state.refs.as_ref())?; Ok(super::layout( + super::Tab::Toolchains, "toolchains", crate::render::string_list(&names, |name| format!("/toolchains/{name}")), )) @@ -51,6 +52,7 @@ let (toolchain, recipe) = toolchain::view(state.refs.as_ref(), &*state.objects(), &name)?; let log = toolchain::log(state.refs.as_ref(), &*state.objects(), &name)?; Ok(super::layout( + super::Tab::Toolchains, &name, html! { dl {
crates/cli/ents-web/src/assets.rs @@ -1,0 +1,14 @@ +//! Static assets embedded at compile time so the built binary stays +//! self-contained -- no runtime fetch, no separate asset bundle to ship +//! alongside `git-ents`. `ents.css` is the hand-rolled pre-redo stylesheet +//! (`pre-redo:crates/git-ents-server/src/web/style.css`), ported rather +//! than vendored. [`FONTS_HREF`] is this crate's one exception: the +//! pre-redo brand type stack is only available from Google Fonts, so it is +//! loaded at request time rather than embedded. + +pub(crate) const OVERRIDES: &str = include_str!("assets/ents.css"); + +/// Google Fonts stylesheet URL for the pre-redo brand type stack (DM Sans, +/// IBM Plex Mono, Lora) -- mirrors +/// `pre-redo:crates/git-ents-server/src/web/assets.rs`'s `FONTS` const. +pub(crate) const FONTS_HREF: &str = "https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500;600&family=Lora:wght@500;600;700&display=swap";
crates/cli/ents-web/src/assets/ents.css @@ -1,0 +1,182 @@ +/* Hand-rolled stylesheet, ported from the pre-redo web UI + * (`pre-redo:crates/git-ents-server/src/web/style.css`) and reverified + * against the markup `crate::pages::layout` and `crate::render` actually + * emit. Dropped from the pre-redo sheet: syntax-highlight token consumers + * (`.code .keyword` and friends -- no highlighter is ported, see + * `crate::pages::files`), diff views (`.diff`, `--diff-add`/`--diff-del`), + * the clone-URL copy button (`.copy-btn`, `.clone`), live-check output + * (`.checks-grid`, `.status-*`, `.terminal-*`), and every chrome class for a + * page family this crate does not have (releases, issues, settings, + * account-strip sign-in). The `--s-*` syntax tokens are dropped with their + * only consumers. + */ +:root { + --font-sans: "DM Sans", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + --font-serif: "Lora", Georgia, "Times New Roman", serif; + --font-mono: "IBM Plex Mono", ui-monospace, "Cascadia Code", "Source Code Pro", Menlo, monospace; + --max-width: 78rem; + --color-bg: #faf8f4; + --color-surface: #fff; + --color-text: #2a2518; + --color-text-muted: #8a7e6a; + --color-link: #b07d10; + --color-link-hover: #96690a; + --color-border: #ede9de; + --color-code-bg: #f5f3eb; + --color-accent: #b07d10; + --color-accent-subtle: #b07d100f; + --shadow-sm: 0 1px 3px #0000000d; + --shadow-md: 0 4px 16px #0000000f; + --radius-sm: 10px; + --radius-pill: 100px; +} +@media (prefers-color-scheme: dark) { + :root { + --color-bg: #171510; + --color-surface: #211f17; + --color-text: #ede8d8; + --color-text-muted: #a89e88; + --color-link: #d4a030; + --color-link-hover: #e4b850; + --color-border: #383324; + --color-code-bg: #211f17; + --color-accent: #d4a030; + --color-accent-subtle: #d4a03012; + --shadow-sm: 0 1px 3px #00000040; + --shadow-md: 0 4px 16px #0000004d; + } +} +*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } +html { font-size: 17px; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } +body { + font-family: var(--font-sans); + background: var(--color-bg); + color: var(--color-text); + line-height: 1.7; + min-height: 100vh; + display: flex; + flex-direction: column; + background-image: radial-gradient(58rem 30rem at 50% -10rem, var(--color-accent-subtle), transparent 72%); + background-attachment: fixed; +} +h1, h2, h3, h4, h5, h6 { font-family: var(--font-serif); } +a { color: var(--color-link); text-decoration: underline; text-decoration-color: color-mix(in srgb, var(--color-link) 25%, transparent); text-underline-offset: 2px; transition: color .15s, text-decoration-color .15s; } +a:hover { color: var(--color-link-hover); text-decoration-color: currentColor; } +a:focus-visible, button:focus-visible, input:focus-visible, textarea:focus-visible, select:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; border-radius: 2px; } +.icon { flex-shrink: 0; fill: currentColor; vertical-align: -0.125em; } + +/* Header band: site wordmark, then the tab-style page nav. */ +.site-nav { position: sticky; top: 0; z-index: 100; background: color-mix(in srgb, var(--color-bg) 82%, transparent); backdrop-filter: blur(10px); border-bottom: 1px solid var(--color-border); } +.nav-inner { max-width: var(--max-width); margin: 0 auto; height: 58px; padding: 0 1.5rem; display: flex; align-items: center; gap: 1.25rem; } +.nav-logo { display: inline-flex; align-items: center; gap: .5rem; font-family: var(--font-mono); font-weight: 700; font-size: 1.02rem; color: var(--color-text); letter-spacing: -.01em; text-decoration: none; white-space: nowrap; transition: color .15s; } +.nav-mark { color: var(--color-accent); font-size: 1.1rem; } +.nav-logo:hover { color: var(--color-accent); } + +.tabs { display: flex; gap: .15rem; max-width: var(--max-width); margin: 0 auto; padding: 0 1.5rem; border-bottom: 1px solid var(--color-border); overflow-x: auto; } +.tab { display: inline-flex; align-items: center; gap: .4rem; padding: 10px 14px; font-size: .88rem; color: var(--color-text-muted); text-decoration: none; white-space: nowrap; position: relative; transition: color .15s; } +.tab:hover { color: var(--color-text); text-decoration: none; } +.tab.active { color: var(--color-text); font-weight: 600; } +.tab.active::after { content: ""; position: absolute; left: 0; right: 0; bottom: -1px; height: 2px; background: var(--color-accent); } + +.content { max-width: var(--max-width); width: 100%; margin: 0 auto; padding: 2.25rem 1.5rem 3rem; flex: 1; } +.page-header { margin-bottom: 1.75rem; padding-bottom: 1.25rem; border-bottom: 1px solid var(--color-border); position: relative; display: flex; align-items: center; gap: .75rem; flex-wrap: wrap; } +.page-header::after { content: ""; position: absolute; bottom: -1px; left: 0; width: 3rem; height: 2px; background: var(--color-accent); border-radius: 1px; } +.page-title { font-family: var(--font-serif); font-size: 1.5rem; font-weight: 700; letter-spacing: -.01em; line-height: 1.3; } + +/* Cards: the container every generic (reflection-driven) view and every + * bare list/definition list a custom page renders lands inside. */ +.card { background: var(--color-surface); border: 1px solid var(--color-border); border-radius: var(--radius-sm); box-shadow: var(--shadow-sm); margin-bottom: 1.5rem; overflow: hidden; } +.card-header { display: flex; align-items: center; gap: .5rem; font-family: var(--font-mono); font-size: .72rem; font-weight: 600; text-transform: uppercase; letter-spacing: .06em; color: var(--color-text-muted); background: var(--color-code-bg); padding: .55rem 1.1rem; border-bottom: 1px solid var(--color-border); } +.card-row { display: flex; align-items: center; gap: .65rem; padding: .7rem 1.1rem; font-family: var(--font-mono); font-size: .9rem; } +.card-row + .card-row { border-top: 1px solid var(--color-border); } +.card-row .icon { color: var(--color-text-muted); } +.card-row.is-dir .icon { color: var(--color-accent); } +.card-row a { color: inherit; text-decoration: underline; text-decoration-color: color-mix(in srgb, currentColor 25%, transparent); flex: 1; min-width: 0; word-break: break-all; } +.card-row a:hover { color: var(--color-accent); } +.card-row a.row-link { display: flex; align-items: center; gap: .65rem; text-decoration: none; } +.card-row:has(.row-link):hover { background: var(--color-code-bg); } + +/* The generic reflection-driven views (`crate::render`): a definition list + * for one entity, a table for a list of entities, a plain list for bare + * name lists (toolchains, inbox). */ +.entity-view { font-family: var(--font-mono); font-size: .9rem; } +.entity-view dt { padding: .7rem 1.1rem .1rem; font-size: .72rem; text-transform: uppercase; letter-spacing: .05em; color: var(--color-text-muted); } +.entity-view dd { padding: 0 1.1rem .7rem; word-break: break-all; } +.entity-view dt + dt { padding-top: .1rem; } + +.entity-list { width: 100%; border-collapse: collapse; font-family: var(--font-mono); font-size: .85rem; } +.entity-list th, .entity-list td { padding: .6rem 1.1rem; text-align: left; word-break: break-all; } +.entity-list thead { background: var(--color-code-bg); font-size: .72rem; text-transform: uppercase; letter-spacing: .05em; color: var(--color-text-muted); } +.entity-list tbody tr + tr { border-top: 1px solid var(--color-border); } +.entity-list td a { color: inherit; text-decoration: underline; text-decoration-color: color-mix(in srgb, currentColor 25%, transparent); } +.entity-list td a:hover { color: var(--color-accent); } + +.string-list { list-style: none; font-family: var(--font-mono); font-size: .9rem; } +.string-list li { display: flex; align-items: center; gap: .65rem; padding: .7rem 1.1rem; } +.string-list li + li { border-top: 1px solid var(--color-border); } +.string-list a { color: inherit; text-decoration: underline; text-decoration-color: color-mix(in srgb, currentColor 25%, transparent); } +.string-list a:hover { color: var(--color-accent); } + +/* Bare `dl`/`ul`/`form` markup a custom page (toolchains, comments) emits + * directly, styled the same as the generic views above so a page needs no + * per-page CSS hook to look consistent. */ +dl { font-family: var(--font-mono); font-size: .9rem; margin-bottom: 1.5rem; } +dl dt { font-size: .72rem; text-transform: uppercase; letter-spacing: .05em; color: var(--color-text-muted); margin-top: .6rem; } +dl dd { word-break: break-all; } +ul:not(.string-list) { list-style: none; font-family: var(--font-mono); font-size: .9rem; margin-bottom: 1.5rem; } +ul:not(.string-list) li { padding: .35rem 0; } + +.badge { font-family: var(--font-mono); font-size: .68rem; color: var(--color-text-muted); background: var(--color-code-bg); border: 1px solid var(--color-border); border-radius: var(--radius-pill); padding: 0 .5rem; margin-left: .5rem; } + +/* Forms: the one write flow this crate demonstrates (`account`, `comments`). */ +form { display: flex; flex-direction: column; gap: .5rem; max-width: 40rem; margin-bottom: 1.5rem; } +form label { display: flex; flex-direction: column; gap: .25rem; font-family: var(--font-sans); font-size: .82rem; font-weight: 600; color: var(--color-text-muted); } +form input, form textarea { font-family: var(--font-mono); font-size: .85rem; color: var(--color-text); background: var(--color-surface); border: 1px solid var(--color-border); border-radius: var(--radius-sm); padding: .42rem .7rem; } +form input:focus, form textarea:focus { border-color: var(--color-accent); } +form textarea { resize: vertical; } +form button { align-self: flex-start; font-size: .88rem; font-weight: 600; color: var(--color-bg); background: var(--color-accent); border: none; border-radius: var(--radius-sm); padding: .45rem 1rem; cursor: pointer; transition: background .15s; } +form button:hover { background: var(--color-link-hover); } + +/* Breadcrumbs and the file browser (`crate::pages::files`). */ +.crumbs { font-family: var(--font-mono); font-size: .92rem; margin-bottom: 1.25rem; display: flex; flex-wrap: wrap; align-items: center; gap: .3rem; word-break: break-all; } +.crumbs a { text-decoration: underline; text-decoration-color: color-mix(in srgb, currentColor 25%, transparent); } +.crumbs .sep { color: var(--color-text-muted); opacity: .55; } +.crumbs .here { color: var(--color-text-muted); } + +.blob { background: var(--color-surface); border: 1px solid var(--color-border); border-radius: var(--radius-sm); box-shadow: var(--shadow-sm); overflow: auto; margin-bottom: 1.5rem; } +.blob pre { font-family: var(--font-mono); font-size: .82rem; line-height: 1.55; margin: 0; padding: 1rem 1.25rem; white-space: pre; overflow-x: auto; } +.binary { padding: 2.5rem; text-align: center; font-family: var(--font-mono); font-size: .85rem; color: var(--color-text-muted); } + +/* Rendered Markdown/AsciiDoc documents (`crate::markdown`, `crate::asciidoc`). */ +.doc-body { padding: 40px 48px 52px; max-width: 44rem; overflow-wrap: break-word; } +.doc-body > :first-child { margin-top: 0; } +.doc-body h1, .doc-body h2, .doc-body h3, .doc-body h4 { font-family: var(--font-serif); font-weight: 700; letter-spacing: -.01em; line-height: 1.25; margin: 1.8rem 0 .9rem; } +.doc-body h1 { font-size: 2.4rem; letter-spacing: -.02em; margin-top: 0; } +.doc-body .doc-subtitle { font-family: var(--font-serif); font-style: italic; font-size: 1.18rem; color: var(--color-text-muted); margin: -.4rem 0 1.2rem; } +.doc-body h2 { font-size: 1.4rem; font-weight: 600; position: relative; padding-bottom: .55rem; } +.doc-body h2::after { content: ""; position: absolute; left: 0; bottom: 0; width: 3rem; height: 2px; background: var(--color-accent); border-radius: 1px; } +.doc-body h3 { font-size: 1.15rem; font-weight: 600; } +.doc-body p, .doc-body ul, .doc-body ol { margin: 0 0 1rem; } +.doc-body ul, .doc-body ol { padding-left: 1.4rem; list-style: revert; font-family: inherit; } +.doc-body li { margin: .25rem 0; padding: 0; } +.doc-body a { font-weight: 500; } +.doc-body code, .doc-body .literal { font-family: var(--font-mono); font-size: .86em; background: var(--color-code-bg); padding: .1rem .35rem; border-radius: 5px; } +.doc-body pre { font-family: var(--font-mono); font-size: .82rem; line-height: 1.55; background: var(--color-code-bg); border: 1px solid var(--color-border); border-radius: var(--radius-sm); padding: 1rem 1.2rem; overflow-x: auto; margin: 0 0 1rem; } +.doc-body pre code { background: none; padding: 0; font-size: inherit; } +.doc-body blockquote { border-left: 3px solid var(--color-accent); padding: .2rem 0 .2rem 1.1rem; margin: 0 0 1rem; color: var(--color-text-muted); } +.doc-body table { border-collapse: collapse; margin: 0 0 1rem; font-size: .92rem; } +.doc-body th, .doc-body td { border: 1px solid var(--color-border); padding: .4rem .7rem; text-align: left; } +.doc-body th { background: var(--color-code-bg); font-weight: 600; } +.doc-body img { max-width: 100%; height: auto; } +.doc-body hr { border: none; border-top: 1px solid var(--color-border); margin: 1.8rem 0; } + +@media (max-width: 640px) { + html { font-size: 16px; } + .content { padding: 1.5rem 1.25rem 2.5rem; } + .nav-inner { gap: .75rem; padding: 0 1.25rem; } + .tabs { padding: 0 1.25rem; } + .page-title { font-size: 1.3rem; } + .doc-body { padding: 1.75rem 1.5rem 2rem; } + .doc-body h1 { font-size: 1.9rem; } + .doc-body h2 { font-size: 1.25rem; } +}