Madhav
Design system v2.1.0. Professional enough for recruiters, fun enough for kids.
Two modes, one system
Pages follow the visitor's system setting by default. The round sun or moon button in the header switches scheme and remembers the choice; switching back to match the device clears it, so the page follows the device again. Both modes are shown here side by side, whatever this page is set to.
Daybreak
Light. Cool slate-white ground, coral and teal accents, Fredoka headings.
Deep Space
Dark. Calm slate night, soft coral and teal accents, Space Grotesk headings.
How it works: tokens.css sets Daybreak on :root, switches to Deep Space under prefers-color-scheme: dark, and lets <html data-theme="light|dark"> override both. Add class="mode-daybreak" or class="mode-deep-space" to pin any block to one mode.
Category colours
Six distinct hues for things that need telling apart: belts, categories, legend keys. Fills carry on-accent text; the -strong shades are for text and marks. Every pairing meets AA in both schemes.
Type scale
Each style is a token: font: var(--text-title) plus letter-spacing: var(--text-title-tracking), or the class md-text-title. Headings use Fredoka in Daybreak and Space Grotesk in Deep Space through var(--display). Body is Nunito Sans; code is JetBrains Mono.
Spacing
Corners
Depth, layers and motion
Shadows and effects have a value per scheme; sizes, layers and motion are the same in both.
Shadows and effects
| Token | Use (a value per scheme, see tokens.json) |
|---|---|
--shadow-card | Cards and panels. A soft lift in both modes. |
--shadow-ledge | Raised controls (buttons, chips, game boards use 5px). Hover: 5px and translateY(-2px). Active: 1px and translateY(2px). |
--shadow-ledge-hover | Raised control on hover, with translateY(-2px). |
--shadow-ledge-press | Raised control while pressed, with translateY(2px). |
--shadow-modal | Dialogs, sheets, toasts and popovers: anything floating above the page. |
--shadow-glow | A soft amber ring around the current or selected item (the current level, a hinted key). Decorative: never the only sign of state. |
--bolt-glow | filter for Bolt and space illustrations: none in Daybreak, a faint coral glow in Deep Space. |
Sizes
| Token | Value | Use |
|---|---|---|
--header-height | 66px | Height of the shared site header row. |
--control-height | 40px | Round icon buttons, the Explore button, the theme button. |
--control-height-sm | 36px | The same controls on phones (600px and narrower). |
Layers (z-index)
| Token | Value | Use |
|---|---|---|
--z-base | 1 | Page content above the space backdrop. |
--z-header | 50 | The sticky site header. |
--z-popover | 60 | Menus and panels that open from the header (the music panel). |
--z-overlay | 100 | Non-modal overlays and sheets that are not a <dialog>. |
--z-toast | 120 | The toast stack, above overlays. |
--z-skip | 400 | The skip link, above everything. |
Motion
| Token | Value | Use |
|---|---|---|
--duration-fast | .12s | Press and lift of raised controls. |
--duration-medium | .25s | Switches, fades, small reveals. |
--duration-slow | .4s | The theme button spin, dialogs and toasts entering. |
--ease-spring | cubic-bezier(.5,1.6,.5,1) | Springy UI: switch knobs, the theme button, level nodes. |
--ease-out | cubic-bezier(.2,.8,.2,1) | Things entering: dialogs, toasts, sections fading up. |
Components
Live, from components.css. The header at the top of this page is the shared site header. Switch scheme with the sun or moon to see each one change.
Button
Chip
Tag
Switch
Stat
Since 2009
Engineers, QA, design
Bubble
Raised tile
Text input
Music pill
Round icon button and link button
Progress stays here.
Progress bar
Dialog and toast
Segmented control and kicker
Screen-reader-only text sits here: read aloud, never shown.
Icons and mascot
32 by 32 line icons with 2.4px round strokes. Files live in icons/. Shown on a Daybreak ground in both schemes, since they are drawn in navy.
How to use it
The design system behind sbmadhav.com: professional enough for recruiters, fun enough for kids. Every new page should feel like part of the same site, in both color schemes, in both grown-up and kid mode.
Start a new page
- Copy
template.htmlto the new page's folder (for example/projects/index.html). - Keep its
<head>as is: the theme script, the font preloads, then the pinned/design-system/v/<version>/tokens.cssandcomponents.css, in that order. Never add a Google Fonts link or@font-facerules;tokens.cssalready declares the self-hosted faces. - Keep the shared header between
<!-- md-header -->and<!-- /md-header -->. Put the page's own parts in its slots (brand, nav, music, end) and runpython3 compose.py header.html page.htmlwith the pinnedheader.html(see Header below). Load the pinnedheader.jswithdefer. - Write every piece of copy twice:
<span class="pro">…</span><span class="kid">…</span>. - Build from the components below; add page-specific styles with tokens only (
var(--coral),var(--space-6),var(--radius-lg),font: var(--text-title)), never raw hex or raw font sizes. - Add the page to the Explore menu: one entry in
PAGESat the top of/assets/explore.jsin the sbmadhav.github.io repo (path, icon, grown-up and kid title and one-line description). - Check both schemes (sun/moon button), kid mode, a 390px-wide phone, and reduced motion before publishing.
Versions
Every release is published twice: at /design-system/<file> (the latest, which changes with every release) and at /design-system/v/<version>/<file>, which never changes once published. The current version is 2.1.0; versions.json lists every version.
- Pin a version in every page:
/design-system/v/2.1.0/tokens.css,components.css,header.html,header.js. A design-system release then changes nothing on a page until that page moves to the new version on purpose, in its own change, and each site can roll back alone. - Pinned files per version:
tokens.css,tokens.json,components.css,header.html,music.html,header.jsandcompose.py. - Version numbers follow semver: a new token, class or component is a minor version; a fix that changes nothing a page relies on is a patch; renaming or removing a token or class is a major version. Existing token names and classes keep working within a major version.
- The unversioned paths stay published for older pages, and the main site's worker precaches them. New pages should not use them.
The decision and its alternatives are in docs/adr/0001-versioned-releases-and-shared-header.md in the repo.
Files
| File | What it is |
|---|---|
tokens.json |
The source of truth: every color (both schemes), type style, spacing step, radius, shadow, size, layer and motion token, each with a usage note. Its version is the release version. |
tokens.css |
The same tokens as CSS custom properties, with Daybreak as default and Deep Space for prefers-color-scheme: dark or data-theme="dark". Generated by build.py. |
components.css |
The md- components, the shared header, page basics (md-page, md-wrap, md-section, md-band, md-grid), type classes (md-text-*) and the kid-mode .pro / .kid swap. |
header.html |
The shared site header as markup with slots. Pages compose it in at build time. |
header.js |
The header's color scheme button (and nothing else). |
music.html |
The Music pill's markup, to put in the header's music slot. |
compose.py |
Puts header.html into a page and keeps it in step: python3 compose.py header.html page.html. Standard library only. |
template.html |
A starter page wired up with the shared header, the theme button and kid mode. |
index.html |
The reference page at /design-system/: color modes side by side, category colours, type, spacing, tokens, live components, icons and these guidelines. Generated. |
v/<version>/, versions.json |
The published, immutable copy of each release, and the list of releases. Generated. |
build.py |
Regenerates tokens.css, index.html, the header in template.html and v/<version>/, then checks contrast, token coverage and raw colours: python3 build.py. |
icons/ |
The 18 line icons and Bolt the mascot as SVG files. |
This repo
The design system lives in its own repo, sbmadhav/design-system, and GitHub Pages publishes it as a project site. Under the user site's custom domain that is sbmadhav.com/design-system/, the same URLs every page already links. _config.yml keeps README.md, CLAUDE.md and build.py out of the published site; tokens.json is published so other repos can read it.
- Build.
pip install markdown, thenpython3 build.py. Commit the generated files (tokens.css,index.html,template.html,v/<version>/,versions.json) with the change that produced them. - Release. Any change to a published file needs a new
versionintokens.json(semver, see Versions).build.pywritesv/<version>/; CI fails if a version already onmainwould gain, lose or change a file (python3 build.py --check-immutable origin/main). Unit tests for that check and forcompose.pylive intests/:python3 -m unittest discover -s tests. - Checks.
build.pyfails if a category colour misses AA in either scheme, if a token is missing from a scheme block oftokens.css, or if a hand-written file uses a raw colour. A pre-commit hook in.githooks/and the workflow in.github/workflows/build.ymlboth run it and fail if it changed a file. Enable the hook once per clone:git config core.hooksPath .githooks. - Shared files from the site. Fonts (
/assets/fonts/), the profile photo, the app icon, the Explore menu (/assets/explore.js) and analytics (/assets/analytics.js) are served by the sbmadhav.github.io repo. Keep those paths there. - Preview locally. Absolute
/assets/and/design-system/paths need the whole site. Runnode e2e/local-server.mjsand open http://127.0.0.1:8123/design-system/: it serves this checkout and takes everything else from the live site. The end-to-end tests run against it withBASE_URL=http://127.0.0.1:8123. - The site's color check. The home page inlines its colors for a fast first paint. sbmadhav.github.io keeps a pinned copy of
tokens.jsonand compares against it; after changing a color here, refresh that copy there withpython3 scripts/check-tokens.py --refresh.
The interactive version of this system, with live previews of every component, is kept privately in Claude.
Principles
- Two audiences, one page. Every piece of copy has a grown-up version and a kid version. The layout never changes between them; only the words do.
- Two schemes, two personalities. Daybreak (light) is a clean, cool slate-white page. Deep Space (dark) is a calm slate night sky. They should surprise people with how different they feel, while every component stays in the same place.
- Friendly, never childish. Rounded shapes, chunky borders and a solid "ledge" under controls make things feel touchable. Restraint in copy and layout keeps it professional.
- Play is opt-in. Music, motion and games never start on their own. Everything respects reduced-motion settings.
- Show, then tell. Prefer an interactive demo (Brain Lab, Design Lab) over a paragraph that explains an idea.
Content fundamentals
- Voice: first person, warm, plain words, dry humor in grown-up mode ("Zero-defect release. Principal engineer energy."), wonder in kid mode ("Whoa! We just blasted into deep space!").
- Never use em dashes. Use a period, comma or colon instead.
- Short. Ledes are one or two sentences. Card descriptions fit in two lines.
- Grown-up copy names real things: titles, metrics, technologies. Kid copy names what it does: "builds robot helpers for a really big bank."
- Sentence case for headings and buttons. Uppercase only for kickers (
kickerstyle). - Numbers in stats are big and specific:
17,2 → 25,+25%,$70M+.
Color
Use tokens, never raw hex. Each token has a Daybreak and a Deep Space value; switching scheme swaps every value at once. Every pairing below meets WCAG 2.1 AA in both modes: 4.5:1 for text, 3:1 for control borders, focus rings, icons and chart marks.
| Role | Token | Daybreak | Deep Space |
|---|---|---|---|
| Page ground | bg |
cool slate-white | near-black slate |
| Section bands | bg-alt |
pale slate | deep slate |
| Cards | card |
white | dark slate, slightly translucent (blur behind) |
| Text | ink / ink-muted |
navy / slate | soft white / cool gray |
| Hairlines (decorative) | line |
light gray | slate |
| Control border | edge |
navy | slate gray |
| Control ledge | ledge (in shadow-ledge) |
navy | dark slate |
| Signature fill | coral |
toned coral | soft coral |
| Success fill | teal |
teal | aqua |
| Highlight fill | sun |
amber | soft amber |
| Info fill | sky |
same as teal |
same as teal |
| Play fill | plum |
same as coral |
same as coral |
| Text, icons, marks | coral-strong, teal-strong, sun-strong, sky-strong, plum-strong |
deep shades | lighter shades of the fill |
| Text on accent fills | on-accent |
navy | near-black |
| Focus ring | focus |
alias of sky-strong |
alias of sky-strong |
| Behind dialogs | scrim |
navy at 45% | black at 62% |
| Behind video and media | media-black, media-ink |
fixed black, fixed white | the same |
| Categories and belts | cat-red, cat-orange, cat-yellow, cat-green, cat-blue, cat-purple (fills) and cat-*-strong |
mid tones / deep shades | soft tones / light shades |
| Bolt the mascot | bolt-body, bolt-screen, bolt-accent, bolt-belly, bolt-eye, bolt-line |
fixed | the same (fixed) |
The palette is called Harbor: three hues. Coral leads, deep teal supports, and amber (sun) is only for highlights. sky shares teal's values and plum shares coral's; they keep their own names so a page can still say what a color means (thinking, play) and the five tag and fill classes keep working. Because they look the same, never use sky and teal (or plum and coral) to tell two things apart: use the category colours.
Category colours
Six distinct hues for anything that must be told apart at a glance: belts, categories, balloon or tile colours in games, legend keys. They are the one place a page may show more than three hues.
- Fill (
cat-redtocat-purple): belts, tiles, chips, legend swatches. Always withon-accenttext, and with a 2pxedgeborder when the fill is the only boundary. - Strong (
cat-*-strong): text, icons, chart lines and marks onbg,bg-altorcard. - Belts: white and black stay as the page's own fixed colours; yellow, orange, green, blue and purple use
cat-yellow,cat-orange,cat-green,cat-blueandcat-purple. - Always pair colour with a word or shape (a belt name, a legend label). Colour alone never carries meaning.
Measured contrast (Daybreak / Deep Space). build.py and the end-to-end tests fail if any falls under 4.5, or if two of the six get too alike (CIE76 distance under 20).
| Colour | on-accent on the fill |
-strong on its worst ground |
|---|---|---|
| red | 6.5 / 8.2 | 5.6 / 8.3 |
| orange | 8.2 / 10.0 | 5.4 / 9.6 |
| yellow | 11.0 / 12.6 | 5.7 / 11.1 |
| green | 8.2 / 10.4 | 5.3 / 9.9 |
| blue | 7.7 / 9.4 | 5.5 / 9.2 |
| purple | 7.4 / 9.2 | 5.8 / 9.1 |
Page background is a gradient, not a flat fill: a faint amber glow top right and a faint teal glow left in Daybreak; faint coral, aqua and amber haze in Deep Space. It ships as --page in tokens.css; use background: var(--page); background-attachment: fixed; on body.
How the colors stay accessible
The system follows the approach Stripe describes in Designing accessible color systems: every hue gets the same few lightness levels, so any color at a given level has the same contrast as every other hue at that level. Pick a level by job, not by hue, and contrast takes care of itself.
| Level | Tokens | Job | Rule |
|---|---|---|---|
| Fill | coral, teal, sun, sky, plum |
Buttons, tags, chips, highlights, tile backgrounds | Always carries on-accent text (at least 6.2:1). Never text on bg. |
| Strong | *-strong |
Text, links, icons, chart marks, status labels | At least 4.5:1 on bg, bg-alt and card. |
| Soft | color-mix(in srgb, var(--x) 14%, var(--card)) |
Tinted panels, badges, icon tiles | Text on it is ink or the matching *-strong. |
Measured contrast (Daybreak / Deep Space, on bg):
| Pairing | Daybreak | Deep Space |
|---|---|---|
ink |
16.0 | 16.0 |
ink-muted |
6.3 | 8.2 |
coral-strong |
5.3 | 8.8 |
teal-strong |
5.9 | 10.5 |
sun-strong |
6.3 | 11.2 |
sky-strong |
5.9 | 10.5 |
plum-strong |
5.3 | 8.8 |
edge (controls) |
16.0 | 5.2 |
focus |
5.9 | 10.5 |
on-accent on the lightest fill / darkest fill |
9.4 / 6.2 | 11.2 / 7.8 |
The tightest pairs are on bg-alt: coral-strong 4.9 in Daybreak and edge 4.6 on card in Deep Space.
State rules
- Hover: buttons lift (transform and a deeper ledge); colors stay the same. Chips fill with sun and switch text to on-accent. Links keep their color and gain an underline.
- Pressed or selected: fill with ink, text flips to bg (16.0:1 / 16.0:1).
- Focus: 3px focus outline with a 3px offset, on every interactive element.
- Disabled: 45% opacity. Disabled controls are exempt from contrast rules, but never rely on disabled styling alone to explain why something is unavailable.
Accent rules
- Fills carry on-accent text; text, icons and chart marks use *-strong. White text on any fill fails and is not allowed.
- Deep Space is calm by default: accents are soft coral and aqua, not neon glows or gradients.
- Three hues in one composition. Coral leads, teal supports, amber only highlights.
- Section tags take one accent each: sun for About, sky for Brain Lab, coral for Bug Hunt, plum for games, teal for Journey.
- line is decorative. A control's boundary must come from edge (or a fill with at least 3:1 against its ground).
Typography
Four families, self-hosted as WOFF2 in /assets/fonts/ so pages make no third-party font requests. tokens.css declares every @font-face (generated by build.py), so linking it is enough. Do not repeat the @font-face rules in a page. Preload the two faces that render first:
<link rel="preload" href="/assets/fonts/fredoka-latin-600-normal.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/assets/fonts/nunito-sans-latin-wght-normal.woff2" as="font" type="font/woff2" crossorigin>
| Family | File | Weights |
|---|---|---|
| Fredoka | fredoka-latin-600-normal.woff2, fredoka-latin-700-normal.woff2 |
600, 700 |
| Space Grotesk | space-grotesk-latin-wght-normal.woff2 |
300 to 700 (variable) |
| Nunito Sans | nunito-sans-latin-wght-normal.woff2 |
200 to 1000 (variable) |
| JetBrains Mono | jetbrains-mono-latin-500-normal.woff2 |
500 only: never ask for 600 or 700, the browser fakes the bold |
To add a weight, put the WOFF2 file in /assets/fonts/ (in the sbmadhav.github.io repo) and add it to FONT_FACES in build.py.
- Display is Fredoka in Daybreak and Space Grotesk in Deep Space.
tokens.cssswaps it through one variable,--display; every heading usesfont-family: var(--display). This font swap is a big part of why the two schemes feel so different. - Nunito Sans for all body text, buttons and labels.
- JetBrains Mono for agent thoughts, code, costs and disclaimers.
- Headings are weight 600, tracked in slightly:
--tracking-headingis -0.01em in Daybreak and -0.025em in Deep Space, applied bycomponents.cssto.md-pageheadings. Body is 17px at 1.6. - Highlight one word in a hero heading with a
sununderline band (Daybreak) or a coral-to-teal gradient fill (Deep Space). Use it once per page.
Type tokens
Every style in the scale is a set of tokens, so a page never writes its own font size, and a change here reaches every page:
| Token | What it holds |
|---|---|
--text-<style> |
The whole font shorthand: weight, size, line height and family. Use font: var(--text-title); |
--text-<style>-size, -weight, -leading, -tracking |
Each part on its own, for when only one changes (font-size: var(--text-small-size)). |
.md-text-<style> |
A class that sets the font and the tracking (and uppercase for kicker). |
Styles: display-xl (hero name), display-l (page titles below the hero size, new in 2.1), heading (section titles), stat, title, card-title, lede, body, small, nav (header links and app tabs), label, label-sm (compact controls), kicker, code. Aliases by role: --text-display is display-xl, --text-h1 is display-l, --text-h2 is heading, --text-h3 is title, --text-h4 is card-title.
display-xl, display-l and heading are fluid (clamp()), so they shrink on phones without a media query. Set letter-spacing from the matching -tracking token: the font shorthand does not include it. Kickers also need text-transform: uppercase (the md-kicker and md-text-kicker classes do both).
Layout and spacing
- Content max width
content-max(1120px), gutterspace-6(24px),space-4(16px) on phones. - Every section:
space-section(72px) top and bottom padding; alternate plain sections withbg-altbands that have a 1pxlinetop and bottom. - Section header: a tag, a
heading, then aledeinink-mutedat most 40em wide, thenspace-7(28px) before content. - Cards in grids: 4 across on desktop, 2 under 980px, 1 under 560px.
- No horizontal scroll at 390px (or 320px). The shared header hides its section links under 1180px and keeps Explore, the scheme button, music and the end controls.
| Token | Value | Token | Value |
|---|---|---|---|
space-1 |
4px | space-1-5 |
6px |
space-2 |
8px | space-2-5 |
10px |
space-3 |
12px | space-3-5 |
14px |
space-4 |
16px | space-4-5 |
18px |
space-5 |
20px | space-6 |
24px |
space-7 |
28px | space-9 |
48px |
space-section |
72px | content-max |
1120px |
The half steps (space-1-5 to space-4-5, new in 2.1) cover the 6, 10, 14 and 18px gaps that dense controls need. Prefer the whole steps for layout.
Sizes: header-height (66px), control-height (40px: round icon buttons, Explore, the theme button) and control-height-sm (36px, the same controls on phones).
Shape and depth
- Radius ladder:
radius-2xs4,radius-xs8,radius-sm12,radius-md16,radius-bubble18 (speech bubbles),radius-lg22,radius-xl28,radius-pill, andradius-round(50%, circles). - Raised controls (buttons, chips, tiles, palette blocks, game boards) get a 2px
edgeborder and a solidshadow-ledge. They lift on hover (translateY(-2px)withshadow-ledge-hover) and press down on click (translateY(2px)withshadow-ledge-press). - Cards and panels get a 1px
lineborder,shadow-card, and lift 4px with a tiny tilt (-0.4deg) on hover. - Anything floating above the page (dialogs, sheets, toasts, popovers) gets
shadow-modal. A soft amber ring around the current or selected item isshadow-glow. - Deep Space adds
backdrop-filter: blur(8px)(--glass) to cards and controls so the starfield shows through softly.
Layers
Use the z-index tokens, never a number: z-base (1, content above the space backdrop), z-header (50), z-popover (60, panels that open from the header), z-overlay (100, sheets that are not a <dialog>), z-toast (120) and z-skip (400, the skip link). A <dialog> opened with showModal() sits in the top layer and needs none.
Motion
- Gentle and looping: Bolt bobs (3.4s), planets drift (45 to 130s per crossing), stars twinkle, the hero blob morphs (12s).
- Springy UI: toggles, the theme button and level nodes use
--ease-spring(cubic-bezier(.5,1.6,.5,1)). - Durations:
--duration-fast(.12s, press and lift),--duration-medium(.25s, switches and fades),--duration-slow(.4s, the theme button, dialogs and toasts entering). Things entering use--ease-out. - Sections fade up 18px on first scroll into view.
prefers-reduced-motion: reduceturns all of it off. Nothing essential may depend on motion.- Pause motion in the Explore menu does the same on every page (WCAG 2.2.2). It sets
class="motion-paused"on<html>and remembers it;components.cssstops CSS animation under it. Give every page-level reduced-motion rule anhtml.motion-pausedtwin, and have scripts that animate decoration checkwindow.siteMotion.paused()and listen for thesitemotionevent ondocument.
Interaction patterns
- Kid mode: write both versions side by side as
<span class="pro">…</span><span class="kid">…</span>;body.kidmodeswaps them. Remember the choice;?kidin the URL starts in kid mode. - Color modes: Daybreak (light) and Deep Space (dark). Pages follow the visitor's system setting by default. The round sun/moon button in the header (
md-theme, run byheader.js) is the one theme control on every page: a click switches scheme; a choice that differs from the system is saved in localStoragetheme(lightordark), and switching back to match the system clears it. Setdata-themefrom a tiny inline script in<head>to avoid a flash. Pin any block to one mode withclass="mode-daybreak"orclass="mode-deep-space". There is no System / Daybreak / Deep Space picker any more. - Explore menu: every page has an Explore button (Star map in kid mode) that opens a panel listing every page as a card, jump links to the home page's sections, and LinkedIn, GitHub and Install app. It slides in from the right on desktop and up from the bottom on phones. The page list lives in one place,
/assets/explore.js. - Bolt (the robot mascot) reacts to actions in a speech bubble under him. A tap-hint pill and a halo show until the first click.
- Music is generated in the browser, never auto-plays, and pauses when the tab is hidden. Every site uses the same Music pill in its header; each brings its own tracks and panel. Games may take over the music while they run and give it back after.
Iconography
- Inline SVG
<symbol>sprites on a 32×32 grid, 2.4px round-capped strokes, no fills except small solid details; stroke usescurrentColorin code. - Shown on icon tiles: a 54px
radius-mdtile filled with the accent at 18% and the icon in the full accent. - Illustrations (planets, rockets, astronaut, Bolt) use flat fills with a 3 to 5px navy outline.
- No emoji in the UI. No brand logos of other companies.
Accessibility
- Text meets 4.5:1 in both schemes:
inkandink-mutedonbg,bg-altandcard. - Focus:
outline: 3px solid var(--focus); outline-offset: 3px. - Toggles use
role="switch"andaria-checked; chips and toggle buttons usearia-pressed; live results usearia-live="polite". - Every game and animation has a still, keyboard-usable path.
- Pages with repeated navigation start with
<a class="md-skip" href="#main">Skip to content</a>and a<main id="main" tabindex="-1">. - Anything that scrolls sideways (code samples, wide tables) is focusable and named:
tabindex="0" role="region" aria-label="…". - At 320px wide nothing scrolls sideways: header controls wrap to a second row, and a control whose label is hidden on phones keeps an
aria-label. - Text that is only for screen readers uses
md-sr-only. Toasts are announced from arole="status"stack and never block taps.
Components
Button
Classes: md-btn, md-btn--primary, md-btn--sm.
A pill with a 2px edge and a solid ledge underneath that lifts on hover and presses down on click.
- Primary (
md-btn md-btn--primary): one per view, the main call to action. Coral in both modes. Label inon-accent. - Secondary (
md-btn): card background. Use for every other action. - Small (
md-btn--sm): inside tools and games (Undo, Clear, Reset). - Labels are verbs in sentence case and can have kid and grown-up versions ("See how my agents think" / "Peek inside a robot brain").
- Icons go before the label at 18px.
- The consumer provides the label, the click handler and
disabledwhen the action is not available.
Chip
Classes: md-chip with aria-pressed.
A pill-shaped choice for picking one option from a small set: Brain Lab questions, Design Lab scenarios.
- Use
aria-pressed="true"on the selected chip; it fills withinkand its text flips tobg. - Hover fills with
sunandon-accenttext. - Keep labels under about 30 characters; they wrap as a row with
space-2gaps.
Tag
Classes: md-tag, md-tag--sky|coral|plum|teal.
The small colored label that opens every section, above the heading.
- One accent per section:
sun(default),sky,coral,plum,teal. Pick by meaning: play is plum, debugging is coral, thinking is sky, career is teal. - Text is
on-accentin the display face at 14px. It can have kid and grown-up versions ("Design Lab" / "Robot Maze"). - One tag per section; never inline in body text.
Card
Classes: md-card, md-icon-tile.
The default container: white (Daybreak) or dark slate (Deep Space), 1px line border, radius-lg, shadow-card, space-5 padding.
- Optional icon tile on top: 54px,
radius-md, filled with the card's accent at 18% (color-mix(in srgb, var(--teal) 18%, transparent)) with the icon in the matching*-strongcolor. - Heading in
card-title; description insmall,ink-muted, two lines or fewer. - Lifts 4px and tilts -0.4deg on hover. Use in grids of 2 to 4.
Stat
Classes: md-stat with md-kicker, md-value, p.
A metric tile: kicker label, a big number in the stat style, and an optional one-line note.
- Numbers are specific and short (
17,2 → 25,$70M+). Put the unit in the number, not the label. - Use 2 to 4 in a row. Scoreboards in games use the same tile.
- The note is
ink-mutedat 14px; give kid mode its own wording.
Switch
Classes: md-switch with md-track and md-knob, role="switch".
The on/off pill used for kid mode, with a springy knob.
role="switch"andaria-checkedcarry the state; the track fills withsunwhen on.- The label names the mode, not the action. Shorten it on phones ("Kid mode").
- Turning kid mode on swaps every
.prospan for its.kidtwin and gets a confetti burst.
Bubble
Classes: md-bubble.
Bolt's speech bubble: card background, 2px edge, 18px corners, ledge shadow and a pointer toward the speaker.
- Place it under Bolt with the pointer up, never over content or photos.
- One or two short sentences, in the current mode's voice. Auto-hide after about 4 seconds.
- Announce it with
role="status"andaria-live="polite".
Header
Files: header.html (markup), components.css (md-header and friends), header.js (the theme button).
The sticky bar at the top of every page on sbmadhav.com, the same everywhere: Bolt's head and the name (a link home), the page's own nav, the Explore button, the sun/moon theme button, the Music pill and the page's end controls. 66px tall, bg at 88% with a blur, a 1px line under it.
<!-- md-header -->
<!-- slot:nav --><nav class="md-nav" aria-label="Sections"><a href="#work">About</a><a href="#lab">Brain Lab</a></nav><!-- /slot:nav -->
<!-- slot:end --><button class="md-switch" id="kidBtn" role="switch" aria-checked="false" aria-label="Kid mode">…</button><!-- /slot:end -->
<!-- /md-header -->
- Compose it at build time, never by hand (a malformed, duplicated or unclosed slot marker is an error, so page content is never dropped):
python3 compose.py header.html page.html(with the pinnedheader.html) fills the block from the partial and keeps the page's slots. Run it again after moving to a new version;--checkfails when a page's header differs from the partial, for CI. - Slots:
brand(the name next to the logo, default "Madhav"),nav,music(the Music pill) andend(a kid mode switch, a Player chip). Everything outside the slots is fixed. - Nav:
<nav class="md-nav">with plain links; mark the current onearia-current="page"(or"true"for app routes) and it fills withink. Section links hide under 1180px (the Explore menu lists them). For app tabs that must always show, addmd-header--tabsto<header>: under 1180px the tabs move to their own rows and wrap. - Phones: under 600px the controls shrink to 36px and Explore and Music show their icon only; under 420px the end controls wrap to a second row; under 360px the name hides and the logo keeps the link's name.
- No Shadow DOM: the header is plain markup, so kid mode, Pause motion and the fonts work in it like anywhere else.
Theme button
Classes: md-icon-btn md-theme (in the header).
The round sun (Daybreak) or moon (Deep Space) button. header.js handles the click, the saved choice, theme-color, and the button's name ("Switch to Deep Space (dark) scheme"). Pages can listen for the sitetheme event on document (to have Bolt say something), and set data-track="<area>" on the button to record the switch with siteTrack.
Music pill
Markup: music.html. Classes: md-music, md-music-btn, md-music-lbl, md-eq, md-music-next.
A pill with a note icon, the word "Music" and a three-bar equalizer, plus an optional next-track button. It never plays on its own.
- Play and pause (the home page): the button has
aria-pressed;"true"while playing fills the pill withinkand makes the equalizer dance. - Open a panel (Vedic Math): the button has
aria-expandedandaria-controls; add the classis-onto.md-musicwhile music plays. - On phones only the note shows; the button keeps its
aria-label.
Raised tile
Classes: md-tile, md-tile-t, md-tile-s, md-tile--fill, md-tile--center.
A card-sized control with a button's edge, ledge, lift and press: grade and game pickers, mode cards, keypad keys, belts.
- A
<button>or<a>. Title inmd-tile-t(card-title), an optional line inmd-tile-s. - Selected:
aria-pressed="true"(oraria-current) tints it withsun. md-tile--fillfills it with--tile-fill(defaultsun) and switches text toon-accent:style="--tile-fill: var(--cat-blue)". A selected fill tile keeps its fill and shows aninkring instead.
Text input
Classes: md-input (on <input>, <select> or <textarea>), md-label, md-hint.
- Always with a visible
md-labeltied byfor; put help inmd-hintand link it witharia-describedby. - 2px
edgeborder,radius-sm, 40px tall.aria-invalid="true"turns the bordercoral-strong; say what is wrong in words too.
Dialog
Classes: md-dialog on a <dialog> opened with showModal(), md-dialog-head, md-dialog-foot.
- A
scrimbehind it,radius-xl,shadow-modal, solidcard(never see-through). Name it witharia-labelledbypointing at its<h2>. - A round close button in the head (
md-icon-btn md-icon-btn--sm,aria-label="Close"); Escape closes it too.
Progress bar
Classes: md-progress, md-progress--sm (6px), md-progress--lg (16px).
<div class="md-progress" role="progressbar" aria-label="…" aria-valuemin="0" aria-valuemax="100" aria-valuenow="60"><span style="--value:60%"></span></div>, or a native<progress class="md-progress">.- The track is
line, the fillteal-strong; change it with--bar(for example--bar: var(--sun-strong)for a timer).
Toast
Classes: md-toasts (the stack), md-toast.
- One stack per page:
<div class="md-toasts" role="status" aria-live="polite">. Append amd-toast, remove it after about 3 seconds (addis-leavingfirst to fade it). - The stack and the toasts have
pointer-events: none, so a toast never blocks a tap on the page under it. A link or button inside a toast still works.
Round icon button
Classes: md-icon-btn, md-icon-btn--sm.
A 40px circle (32px small) with one 20px icon: close, flip back, play. Always give it an aria-label.
Link button
Classes: md-link.
A <button> that reads as a link, for small inline actions ("Reset play mode"). Underlined in coral, so it is told apart by more than colour.
Segmented control
Classes: md-seg (or the older name md-modes) with <button aria-pressed>.
One choice from two to four: leaderboard periods, units. Not for the color scheme, which is the theme button.
Kicker and screen-reader-only text
Classes: md-kicker, md-sr-only.
md-kickeris the uppercase label (kickerstyle,ink-muted) and works anywhere, not only in a stat.md-sr-onlyhides text visually but keeps it for screen readers.
Icons
The line icons from sbmadhav.com, exported from its inline SVG sprite. 32×32 grid, 2.4px round-capped strokes, drawn here in ink navy (#151B2B).
- On the site they live in a
<svg><defs><symbol>sprite and usecurrentColor, so they take the accent color of their tile or text. Copy the paths into a sprite rather than loading the files inicons/with<img>when the color must change. - Pair each with a meaning, not a decoration: agent (agentic AI), voice, interface, shield (responsible AI), briefcase (consulting), hardware, sneaker, health, bank, mystery (what's next), bug, star (goal or current level).
Mascot
Bolt, the site's original robot mascot: teal body and arms, pale mint face, coral antenna and cheeks, amber chest, navy outlines.
- The header logo is Bolt's head, drawn with the same tokens.
- Bolt speaks through the Bubble component and reacts to what people do (switching schemes, playing music, winning games).
- Keep Bolt's colors fixed in both schemes with the
bolt-*tokens:bolt-body,bolt-screen,bolt-accent,bolt-belly,bolt-eyeandbolt-line. Neverwhiteorcardfor his eyes (card is dark in Deep Space). - For the faint Deep Space glow, use
filter: var(--bolt-glow)(or the classmd-bolt): a soft coral drop shadow in Deep Space and nothing in Daybreak, whichever way the scheme was chosen. - Never put words on Bolt or change his shape; poses come from small transforms (bob, tilt, eyes following the pointer).