The My First Bitcoin brand as code: a Tailwind preset, CSS variables, the supergraphics primitives, the raw design tokens and the written brand specification. Every file is generated by scripts/build-design-package.mjs in MyFirstBitcoin/mfb-design, from that repository's tokens.json, which mirrors the Brand Book in Figma and changes only by pull request. Do not edit generated files by hand.
"dependencies": { "@myfirstbitcoin/design": "github:MyFirstBitcoin/mfb-design#v1.5.0" }Pin a tag. A new release reaches your project only when you raise the pin (and refresh the lockfile).
Make these changes in the same commit that raises your pin:
- Delete any local
.highlighterband (for example alinear-gradient(transparent 80%, ...)background on.highlighter), and any local rule that changes itsdisplayortext-align. The package now draws the Brand Book's line itself, and a local band would draw on top of it. On orange surfaces it no longer paints a white band or turns the word white: it draws a white line, and the word keeps the heading's color. - Remove any padding your project adds around
.sg-para-patternto make room for the slant. Each row now insets itself by half its height times tan(13°), so its first shape is no longer clipped; extra padding moves the rows further right. - Set
--sg-aspecton parallelogram frames too. The photo scale in.sg-para-framewas a fixed 1.12; it is now1 + tan(--sg-angle-base) / --sg-aspect, the smallest scale that covers the frame, so it depends on the frame's real shape. supergraphics.csssets more variables in:root: the font families, sizes and weights, the halftone colors, the color roles (--mfb-<element>-on-<surface>) and the line height and letter spacing per level (--mfb-leading-<level>,--mfb-tracking-<level>), besides the palette and the geometry. If you override any of them, do it after importingsupergraphics.css, or import the file into a cascade layer..sg-cover__titlereads--mfb-size-h2. The--mfb-font-h2variable it used to read was never defined by this package; setfont-sizeon the title instead.- Tailwind 4 projects that restrict a theme namespace, for example with
--font-weight-*: initialor--radius-*: initial, must put that reset in its own@themeblock before importingtheme.css: placed after the import, it removes the package's values too, and no utility is generated for them.
@import "tailwindcss";
@theme { --font-weight-*: initial; }
@import "@myfirstbitcoin/design/theme.css";- Headings and body text on light backgrounds are black. The written spec said purple-300 headings and gray-900 text; the Brand Book draws both in black (Type Relationships, 918:2990), and the spec and the color descriptions in
tokens.jsonnow say so. No variable changed value: a project that colors its headings purple-300 or its text gray-900 changes it when it chooses to. - Text on orange is black, buttons included, and the supergraphics classes now set it.
.sg-ctasetsvar(--mfb-link-on-orange),.sg-bg-orangevar(--mfb-body-on-orange)and.sg-cover--orange(with its.sg-cover__title)var(--mfb-heading-on-orange): black, 9.2:1 on orange-300, where they set white (2.3:1, below the 4.5:1 that body text and buttons need). Delete any local whitecoloron them, which would bring the old contrast back; a local black one can go too. - Text on the light supergraphics surfaces is black.
.sg-bg-white,.sg-bg-grayand.sg-info-bar--on-lightsetvar(--mfb-body-on-light)(black) instead of gray-900. - The info bar's date follows its surface, and the line under it is a solid color. On
.sg-info-bar--on-light,.sg-info-bar__dateisvar(--mfb-heading-on-light)(black) instead of orange-300 (2.3:1 on white). A bar without a variant has no background of its own: its date stays orange-300 on purple surfaces and photos, and is black when the bar sits directly in.sg-bg-whiteor.sg-bg-gray(where orange-300 fails the contrast rule) or in one of the package's orange surfaces,.sg-bg-orange,.sg-cover--orangeand the orange spotlight, book and halftone frames (where an orange date could not be seen). On any other light or orange surface, give the bar a variant. The date'ssmallloses its 75% opacity: it isvar(--mfb-muted-on-light)(gray-700, 5.1:1) on the light bar andvar(--mfb-muted-on-dark)(gray-400, 8.3:1 on purple-400, where it was orange-300 at 75%, 4.2:1) on the dark bar; without a variant it keeps the date's color, fully opaque. .sg-headlineand.sg-cover__titleset line height 1 and no letter spacing (--mfb-leading-h1and--mfb-tracking-h1;h2for the cover title), the Brand Book's, instead of 1.05 and -0.03em. Line height 1 is the book's for H1 to H4 and quotes..sg-headlinesets no size, so a headline set at the H5 or H6 size also takes.sg-headline--h5or.sg-headline--h6, new in this release, which set the book's 1.1 (--mfb-leading-h5,--mfb-leading-h6). Lines of a headline sit a little closer, and each headline is a little wider (0.03em per letter): check headlines that only just fitted their box..sg-labelis Regular (400), not Medium, with line height 1.2 and no letter spacing (--mfb-leading-label,--mfb-tracking-label), instead of 1.5px. Its size stays 18px..sg-bodyreads--mfb-leading-body, the same 1.2 as before, and now sets--mfb-tracking-body(none), so it no longer takes a letter spacing from its parent.- A line height for long reading,
--mfb-leading-body-long(leading-body-long), 1.7. It is declared from the website's article text, because the Brand Book only sets body copy next to a heading (--mfb-leading-body, 1.2). It is under review, and nothing uses it until a page adopts it. - The supergraphics classes that still differ from the Brand Book are listed in
brand-spec.md("Supergraphics classes that differ from the Brand Book"). - A test that snapshots the
--mfb-*variables ofbrand.csswhose value is a hex must refresh its snapshot: this release adds--mfb-halftone-highlight,--mfb-halftone-highlight-full,--mfb-halftone-shadow. The other new variables carry no hex; they reference the palette withvar(). - Everything else is added, not changed. Every new Tailwind utility is
mfb-prefixed, a type level (leading-h1,tracking-h1,text-h1-fluid), a color role (text-heading-on-light) orfont-regular. The weightsfont-normal,font-mediumandfont-semiboldrestate Tailwind's own values (400, 500, 600), and the build refuses a weight that would change one. None of them replaces a Tailwind default or an existing key. See "Layout, motion and color roles" below.
// tailwind.config.mjs
import mfb from '@myfirstbitcoin/design/tailwind';
export default { presets: [mfb], content: ['./src/**/*.{astro,html,js,ts}'] };/* global.css */
@import '@myfirstbitcoin/design/theme.css';@import '@myfirstbitcoin/design/brand.css'; /* var(--mfb-purple-400), var(--mfb-weight-medium) ... */@import '@myfirstbitcoin/design/supergraphics.css';The signature My First Bitcoin shapes as ready-made classes: sg-para / sg-para-frame (13° parallelograms), sg-para-pattern, sg-spotlight / sg-spotlight-frame (13°+24° corner cuts), sg-book / sg-book-frame / sg-book-stack, sg-cover, sg-halftone-cutout, and the highlighter. Never hand-roll these shapes in a page: the brand angle is exactly 13° (--sg-angle-base) and hand-rolled copies drift. The file is standalone (token prelude included), so it works with theme.css-only Tailwind 4 setups.
The classes read the Brand Book's color roles and type relationships, so text on orange is black and headlines have no tracking. Some still carry values the Brand Book does not have, and a later release will change them: .sg-cta's 1px letter spacing and 40px radius, .sg-label's 18px size, the type sizes and SemiBold date of .sg-info-bar, the 6px corners of .sg-para-frame and .sg-para-accent, the pattern's spacing and the logo's height. brand-spec.md lists every such class and what to set.
sg-spotlight-frame, sg-book-frame, sg-halftone-cutout and sg-para-frame compute their cuts (and the parallelogram frame its photo scale) from --sg-aspect, the frame's width divided by its height. The default, 1.5, is right only for a frame of that shape. Set it to the frame's real ratio, for example style="--sg-aspect: 0.75" on a 3:4 frame; otherwise the cuts are not at 13° and 24°, and a parallelogram frame's photo may not cover the frame.
Wrap the one word you emphasize in a heading: <h2>Open Source <span class="highlighter">Education</span></h2>. It draws the Brand Book's highlighter (Figma node 918:2874): a thin line at the baseline, drawn beneath the glyphs (not a band), under 80% of the word, centered. The line is orange-300, and white inside the orange surfaces sg-bg-orange, sg-spotlight--orange, sg-cover--orange, sg-spotlight-frame--orange, sg-book-frame--orange and sg-halftone-cutout--orange-bg. The word keeps the heading's color. On another orange surface, or on a mid grey such as gray-500, set --sg-highlighter-color: var(--mfb-white) on the section. On a light grey such as gray-200 (sg-bg-gray) keep the orange line: a white one cannot be seen there. One word per heading, never more. The span is an inline-block, and the line's length and thickness are computed from the span's own width: do not override its display or text-align. Its measurements are the --sg-highlighter-* variables (coverage, aspect, offset, shape), from the geometry tokens of the same names.
Where the Brand Book defines a value, the token takes it. Where the book is silent (spacing, radius, shadow, motion, widths), the value is declared from the live website, myfirstbitcoin.org, which was built following the Brand Book; each such token's $extensions.mfb.source names the website commit and the CSS that carries the value in what the site serves (a custom property, a selector and property, or a property set inline), for example myfirstbitcoin.org@6368b2a, --r-lg. A website value that contradicts the Brand Book is never imported. brand-spec.md lists every value with its source.
| What | brand.css | Tailwind 3 and 4 |
|---|---|---|
| Color roles | --mfb-<element>-on-<surface>: heading-on-light, body-on-light, muted-on-light, link-on-light, link-underline-on-light, border-on-light, highlighter-on-light, logo-on-light, heading-on-dark, body-on-dark, muted-on-dark, link-on-dark, highlighter-on-dark, logo-on-dark, heading-on-orange, body-on-orange, link-on-orange, highlighter-on-orange, logo-on-orange |
text-heading-on-light, decoration-link-underline-on-light, border-border-on-light ... |
| Dark theme (derived from the palette, for an interface with a dark mode) | --mfb-dark-surface, --mfb-dark-surface-raised, --mfb-dark-text, --mfb-dark-text-muted, --mfb-dark-line, --mfb-dark-link, --mfb-dark-accent, --mfb-dark-on-accent, --mfb-dark-focus, --mfb-dark-primary, --mfb-dark-on-primary, --mfb-dark-primary-edge |
bg-mfb-dark-surface, text-mfb-dark-text-muted, border-mfb-dark-primary-edge ... |
| Line height per level | --mfb-leading-<level>, and --mfb-leading-body-long for long reading (declared from the website, under review) |
leading-<level>, for example leading-h1; leading-body-long |
| Letter spacing per level | --mfb-tracking-<level> |
tracking-<level> |
| Fluid sizes for web pages | --mfb-size-<level>-fluid (h1, h2, h3, h4, h5, h6, quote, label) |
text-<level>-fluid |
| Spacing scale | --mfb-space-1, --mfb-space-2, --mfb-space-3, --mfb-space-4, --mfb-space-6, --mfb-space-8, --mfb-space-12, --mfb-space-16, --mfb-space-24, --mfb-space-32 |
Tailwind's own p-1 to p-32: the same values, so the package adds none |
| Semantic spacing | --mfb-space-heading-to-body, --mfb-space-quote-to-label, --mfb-space-gutter, --mfb-space-section, --mfb-space-section-tight, --mfb-space-header-gap, --mfb-space-block-end |
py-mfb-section, px-mfb-gutter, mt-mfb-heading-to-body ... |
| Radius (interface elements only, never brand shapes) | --mfb-radius-sm, --mfb-radius-md, --mfb-radius-lg, --mfb-radius-pill |
rounded-mfb-sm, rounded-mfb-md, rounded-mfb-lg; rounded-full for the pill |
| Shadow | --mfb-shadow-media, --mfb-shadow-media-control, --mfb-shadow-overlay |
shadow-mfb-media, shadow-mfb-media-control, shadow-mfb-overlay |
| Easing | --mfb-ease-out, --mfb-ease-in-out |
ease-mfb-out, ease-mfb-in-out |
| Duration | --mfb-duration-fast, --mfb-duration-med, --mfb-duration-slide, --mfb-duration-reveal |
duration-mfb-fast, duration-mfb-med, duration-mfb-slide, duration-mfb-reveal |
| Container widths | --mfb-container-page, --mfb-container-wide, --mfb-container-measure (px) |
max-w-mfb-page, max-w-mfb-wide, max-w-mfb-measure (rem) |
| Logo size and clear space | --mfb-logo-min-width, --mfb-logo-min-width-print, --mfb-logo-aspect, --mfb-logo-min-height, --mfb-logo-clearspace, --mfb-logo-edge-margin |
none |
| Shape tones (a shape one step lighter than its base) | --mfb-shape-on-purple-300, --mfb-shape-on-orange-300, --mfb-shape-on-gray-700 |
none |
| Layers | --mfb-z-header, --mfb-z-dropdown, --mfb-z-overlay |
none |
| Media ratios | --mfb-ratio-video, --mfb-ratio-photo, --mfb-ratio-portrait, --mfb-ratio-square |
none (Tailwind has aspect-video and aspect-square) |
| Interface | --mfb-ui-touch-target, --mfb-ui-link-underline-offset |
none |
- New utilities never replace Tailwind's. Keys are
mfb-prefixed (rounded-mfb-md, notrounded-md;ease-mfb-out, notease-out, which is a different curve), type levels or color roles. The build refuses a key that equals a Tailwind 3 or 4 default, and a font weight that would change Tailwind's value for its name. - In 1.x,
text-h1sets only the size. Addleading-h1yourself (andtracking-h1where something else sets tracking). Folding line height intotext-h1would change every existing page, so it waits for a major release. - New
brand.cssvariables reference the palette withvar()and add no hex: roles arevar(--mfb-black)and the like, shadowscolor-mix(in srgb, var(--mfb-purple-400) 25%, transparent).theme.cssandtailwind.jscarry the same colors as hex, so Tailwind's opacity modifiers work. - Tailwind 4 emits a theme variable only when something uses it. To read
var(--mfb-radius-md)or another new variable in your own CSS, importbrand.cssas well, or use the utility. - Shadows in Tailwind 4: prefer the
shadow-mfb-*utilities. Tailwind compiles them to a hex color with alpha. Avar(--mfb-shadow-*)frombrand.cssgoes through Tailwind 4's CSS compiler, which gives it a fallback withoutcolor-mix()for browsers that lack it (Chrome before 111, Safari before 16.2), and that fallback draws the shadow fully opaque. - index.js exports
colorRoles,shapeTones,darkTheme,fontSizeFluid,lineHeight,letterSpacing,logo,spacing,space,radius,shadow,easing,duration,container,zIndex,mediaRatio,uinext to the existingcolors,fontFamily,fontSizeandfontWeight.
brand-check.mjs ships in this package: a guard that a project runs after its build, with Node.js and nothing else. It reads every value from this package's tokens.json, so it follows the version you pin.
node node_modules/@myfirstbitcoin/design/brand-check.mjs [options]| Option | What it does |
|---|---|
--src DIR |
Source to read, repeatable or comma-separated (default src). Comments are ignored. |
--dist DIR |
Build output whose CSS (style sheets, <style> blocks and the --sg-angle-* in style attributes of built HTML) is read, repeatable (default dist). |
--source-only |
Skip the build output. |
--allow FILE |
The allowlist (default brand-check.allow.json when it exists), below. |
--warn RULE[,RULE] |
Report these rules without failing. |
--error RULE[,RULE] |
Fail on rules that only warn by default (raw-spacing, text-color, typed-caps, translucent-text, letter-spacing). |
--root DIR |
The project root the paths above are relative to (default: the current directory). |
--help |
List the rule ids, with what each reports. |
It exits 0 when nothing fails (warnings are printed), 1 when something fails and 2 when it cannot run (an unknown option or rule, a missing --src, an allowlist that is not valid). Run it in your build, for example "build": "astro build && node node_modules/@myfirstbitcoin/design/brand-check.mjs". To find its path from a script, resolve @myfirstbitcoin/design/brand-check.mjs (it is in the package's exports). It fails when the source has:
- a color literal (hex from
#RGBto#RRGGBBAA, also inside an arbitrary utility such asshadow-[0_0_0_3px_#f7931a80],rgb(),hsl()and the other color functions; text between markup tags, such as a block number written#840000, is prose and is not read), or a named color in a color property, a custom property, a style object (style={{ ... }}, or an object named like a style or typedCSSProperties), a canvasfillStyle, an SVG color attribute or an arbitrary utility (bg-[teal]) - a font family other than
var(--mfb-font-heading),var(--mfb-font-body)orvar(--mfb-font-sans)(in Tailwind 4,var(--font-sans)too), infont-family, thefontshorthand or a--font-*theme variable, a family name, or the deprecated mono family. Inside@font-face, which describes a face rather than using one, family names and weights are allowed - a skew, a rotation or an angle literal that is not a quarter turn (in CSS, a style object, an animation prop such as
rotate: -15, a Tailwind class such asrotate-3orrotate-x-12, an SVGtransform,patternTransformorgradientTransformattribute, or therotateattribute of SVG text), a transform matrix that rotates or skews, or slant geometry computed from--sg-angle-*: the 13° slant comes only from the supergraphics classes - a hand-rolled
clip-pathshape or an SVGclipPathdrawn with a polygon, or a hand-rolled highlighter: alinear-gradient(transparent 80%, ...)band, an orange pseudo-element drawn as a marker (sized inemor%, put behind the text with a negativez-index, or named like a highlight or a mark), an orangemarkelement, an orange insetbox-shadowunder the text, an orange underline 0.2em or 4px thick or more, or orangeafter:orbefore:utilities - a redefinition of a variable the package defines (
--mfb-*, or the--sg-*geometry such as--sg-angle-base); pages set only--sg-aspectand--sg-highlighter-color, which must be a palette variable text-transform: uppercase(in any case, and astextTransformin a style object), theuppercaseclass (in a class attribute, or as a string such asclsx('uppercase')orclass:list={['uppercase']}), small caps or small-cap font features- a gradient, in CSS or as an SVG
linearGradientorradialGradient(the deprecated brand gradient and.sg-photo-zoneincluded), a smooth color filter or a blend mode (Rule 4: halftone, never duotone). An alpha-only mask is allowed, and so is a gradient whose colors meet at hard stops without blending (a two-color split, a dashed line), unless it draws in orange - a Tailwind color utility outside the palette, such as
bg-teal-500or Tailwind's owntext-purple-500 - a weight above 600, or a translucent brand shape: opacity on a supergraphics shape or the highlighter (a class,
opacity-[.85]or an inline style), or an orange or purple fill that is not solid (opacity,fill-opacityorfilter: opacity()beside it, an alpha modifier such asbg-orange-200/10, or a fill or border that mixes orange or purple withtransparentincolor-mix()). Opacity 0 (hidden), states such ashover:opacity-90, and rules for a disabled element (:disabled,[aria-disabled]) are left alone - a raw value that has a token: a radius, duration, easing, container width, font size, font weight, shadow or semantic spacing literal equal to a token value (
border-radius: 12px,180ms,clamp(64px, 9vw, 128px)), with the token to use instead
and when the built CSS lacks --sg-angle-base with the token value (import brand.css or supergraphics.css), or any declaration of --sg-angle-base or --sg-angle-alt has another value (a rule, a nested rule, an @property initial-value, or a style attribute in built HTML); defines a --color-* that is not a palette color or a color role; sets uppercase or small caps; or gives a font-family the mono or serif family. A utility class definition is not a use: Tailwind 3 emits .uppercase or .font-mono when its content scanner sees the word anywhere, a comment included, and the source check reports the class where a page uses it. Nor is a variable definition or the base style of code, kbd, samp and pre: Tailwind 4's base styles always set --default-mono-font-family: var(--font-mono), and theme.css ships --font-mono for the deprecated mono family. A rule where the utility class shares its compound selector with another class, such as .eyebrow.uppercase, is the page's own rule, and it is reported.
Five rules only warn, because the right answer depends on what the check cannot see or because the drift is widespread: raw-spacing (a px or rem spacing literal on the scale, such as padding: 24px: Tailwind's own steps are the same values), text-color (orange text, which is right only on purple-300 and purple-400; a purple heading; gray-900 text, where the Brand Book draws text on light backgrounds black: color: var(--mfb-gray-900), text-gray-900, or a value given to a name such as textColor), translucent-text (a text color mixed with transparent, or an alpha modifier such as text-white/85: use a solid palette color, for example var(--mfb-muted-on-dark)), letter-spacing (a non-zero letter-spacing or letterSpacing, or a tracking-* utility other than tracking-normal and the type levels: no brand text style has tracking) and typed-caps (two or more words typed in capitals). --error RULE[,RULE] makes them fail.
What it does not check. It reads how values are written, not how the page looks: it cannot measure contrast, tell which background a text sits on, or read text that a script or a content system supplies. It does not check line height, the one-word highlighter rule, which shape fits which content, or photos themselves, and it does not read Markdown or MDX files. Status colors for forms (error, success) are not in the Brand Book or the palette, so there is no token for them yet: a project that needs them lists them in its allowlist, with the reason.
--allow FILE (default brand-check.allow.json when it exists) lists the exceptions, each with its reason, for example a third-party mark:
[{ "file": "src/assets/partner-logo.svg", "reason": "the partner's own logo colors" },
{ "file": "src/ui/Share.astro", "rule": "color-literal", "match": "1877f2", "reason": "the network's own badge color" }]An entry without rule covers every rule in that file, and one without match every finding of the rule. --warn RULE[,RULE] reports a rule without failing, for a project that adopts the check step by step (--warn raw-value). --help lists the rule ids.
Plain CSS variables are namespaced --mfb-* (brand values) and --sg-* (geometry) to avoid collisions. brand.css has all of them; supergraphics.css repeats what its classes need.
- Colors:
--mfb-<name>, for example--mfb-purple-300,--mfb-orange-300,--mfb-gray-900 - Font families:
--mfb-font-heading,--mfb-font-body,--mfb-font-sans(IBM Plex Sans, then Arial, the Brand Book's system fallback) - Font sizes:
--mfb-size-<level>, for example--mfb-size-h1,--mfb-size-body - Font weights:
--mfb-weight-regular(400),--mfb-weight-medium(500),--mfb-weight-semibold(600). Tailwind 4 (theme.css) and Tailwind 3 (the preset) get the same names asfont-regular,font-mediumandfont-semibold, plusfont-normalwith the regular value; they extend Tailwind's scale rather than replace it - Halftone colors:
--mfb-halftone-highlight,--mfb-halftone-highlight-full,--mfb-halftone-shadow(highlight for cutout portraits, highlight for full portraits, shadow). They are book-specified values, not palette colors, so they have no Tailwind utilities - Geometry:
--sg-angle-base,--sg-angle-alt,--sg-aspect,--sg-logo-icon-h,--sg-base-h-ratio,--sg-highlighter-coverage,--sg-highlighter-aspect,--sg-highlighter-offset,--sg-highlighter-shape
Both utility conventions are served: top-level (preferred for new pages) and mfb- prefixed (legacy). Utilities use the brand palette at the top level (e.g. bg-purple-400, bg-orange-300, text-gray-700, text-h1), overriding Tailwind's default purple/orange/gray with the brand values. Other defaults (red, blue, etc.) are untouched.
These are not in the Brand Book. They stay until a major release so that pages using them keep working; do not use them in new work.
- The brand gradient:
--mfb-gradient-brand(brand.css),--mfb-gradient(supergraphics.css),bg-brand-gradient(Tailwind 3 preset) and.sg-bg-gradient. Use a solid palette fill instead, for example purple-300 or purple-400 (.sg-bg-purple). .sg-photo-zone: a gradient placeholder. Put the photo itself (animg) in the frame, or a solid palette fill while it is missing.- The mono font:
--mfb-font-mono,--font-mono(Tailwind 4) andfont-mono(Tailwind 3). IBM Plex Mono is not in the Brand Book. Use the brand sans (--mfb-font-body,font-sans), whose system fallback is Arial (Brand Book 918:2375). Tailwind 4's base styles setcode,kbd,sampandprein--default-mono-font-family, which reads--font-mono; to keep them in the brand sans, add@theme { --default-mono-font-family: var(--font-sans); }after importingtheme.css.
brand-spec.md ships in this package: the written brand specification, generated from the same tokens. Read it at node_modules/@myfirstbitcoin/design/brand-spec.md. The Brand Book in Figma wins any conflict with it.
Logos, badges, and the brand book PDF live in MyFirstBitcoin/mfb-brand.