rafaelborges.design · design system reference
Lime on Deep Green
The whole portfolio runs on two colours, two typefaces and one idea: every component inherits its palette from the surface it sits on. This is what exists today, documented from the source — plus an audit of where the system holds and where it leaks. All fourteen findings were fixed on 2 September and are marked Fixed below.
:root, none unused01Foundations
Everything derives from two HSL values. There is no third brand colour, no neutral ramp and no elevation scale — depth comes from borders and surface inversion instead of shadow.
Colour
#F7FDA0
#15220B
Aliases and derived tokens
| Token | Resolves to | Role | Uses |
|---|---|---|---|
| --color-bg-dark | --color-secondary | Ground of .dark and body | 3 |
| --color-text-on-dark | --color-primary | Ink on dark surfaces | 3 |
| --color-bg-light | --color-primary | Ground of .light | 2 |
| --color-text-on-light | --color-secondary | Ink on light surfaces | 2 |
| --focus-ring | --color-primary, flipped by .light | Surface-aware focus outline | 5 |
| --card-border | --color-primary, flipped by .light | Surface-aware card border | 1 (every card) |
| --opacity-hover | 0.8 | Pointer over the control | 6 |
| --opacity-active | 0.65 | Pressed | 6 |
| --opacity-disabled | 0.5 | Unavailable. 4.82:1 on the dark ground — WCAG exempts inactive controls, but a disabled label you can't read is still a bad label. | 4 |
| --color-negative | #fd6b6b | A metric that moved the wrong way. No --color-positive: good news already reads in the brand yellow. | 1 |
Contrast
| Pair | Ratio | WCAG AA | AAA |
|---|---|---|---|
| #F7FDA0 on #15220B — body text on dark | 15.42:1 | Pass | Pass |
| #15220B on #F7FDA0 — text on the lime hero | 15.42:1 | Pass | Pass |
| #FD6B6B on #15220B — the one-off negative-result red | 5.92:1 | Pass | Fail |
Surfaces
.dark and .light are the system's central
mechanism. They are not themes in the usual sense — both appear on the same
screen. A component doesn't choose its colours; it inherits them from
whichever surface class wraps it.
bg --color-bg-dark · ink --color-text-on-dark
Default. Applied to body, all case-study containers except the intro, and every icon button.
bg --color-bg-light · ink --color-text-on-light
Hero banner, contact banner, case intro, modal panel. Also flips --focus-ring to the dark green so the ring stays visible.
.btn deliberately sets no
background. .btn and .dark have the
same specificity, so a background declared on .btn would win by
source order and blank out every themed button. Keep backgrounds on the
surface classes only.
Typography
Two faces. Tanker (self-hosted, single 400 weight) carries every
display moment; Sora (variable, Google Fonts) does everything else
across six weights. Tanker loads with
font-display: block rather than swap — the hero
wordmark is sized to Tanker's metrics, so painting a fallback at that size
would overflow the banner for a few frames.
line-height 100%
72px @ ≤768
line-height 100%
40px @ ≤425
line-height normal
line-height 100%
line-height 100%
line-height 28px
16px @ ≤425
line-height 28px
24px @ ≤425
line-height 24px
line-height normal
The scale, now tokenised
Named by role, not size — the same step is reused at different breakpoints, so a size-based name would go stale the first time one moved.
| Token | Value | Used by |
|---|---|---|
| --text-caption | 14px | .btn-menu.small (breadcrumbs) |
| --text-body | 16px | body, .btn, hero paragraphs, insight subtitle & description |
| --text-body-lg | 20px | .btn-menu, p.case lead, contact heading ≤600 |
| --text-title-s | 24px | .casesubtitle, insight title, .subhead |
| --text-title-m | 32px | h3, .case-content h2, p.special |
| --text-title-l | 40px | blockquote, a.logo, .casetitle ≤425 |
| --text-display-s | 60px | .casetitle |
| --text-display-m | 72px | h2 ≤768 |
| --text-display-l | 120px | h2 |
| --leading-display | 100% | the seven display headings |
Four literals remain, all deliberate: 1.4rem /
0.95rem in the case modal (rem-based so it scales with the
root — though neither resolves to a scale step), the 18px
close-button glyph, and an 18px nudge on the contact heading
at ≤425. Px line-heights stay literal: they're tied to the font-size beside
them and don't generalise.
Weights
| Weight | Where | Declarations |
|---|---|---|
| 200 · ExtraLight | Body copy, insight-card subtitle, work note | 5 |
| 300 · Light | Case lead paragraphs, small menu links | 3 |
| 400 · Regular | Tanker display, buttons, card descriptions | 9 |
| 600 · SemiBold | strong, menu links, hero paragraphs | 5 |
| 700 · Bold | Insight-card title only | 1 |
| 800 · ExtraBold | Section headings, blockquote | 3 |
wght@200..800 — 100 was never used. Narrow
the range rather than listing discrete weights: for a variable family
a list of values can produce more files, not fewer.
Spacing
The number is the multiple of 4, so gaps in the sequence are meaningful rather than missing. 127 references across the two stylesheets. The bar shows each step to scale; the figure on the right is how often it is used.
The scale was rebuilt from usage data rather than guessed:
--space-1 and --space-5 were added because 4px
and 20px were already in use with no token, --space-9 (36px)
was deleted because nothing referenced it, and 28px is absent because
nothing uses it.
width or height on an interactive control. It is
--control-height now, and it is 44px because that is the WCAG
2.5.5 minimum touch target — not because it is a step of the scale. Filed
as --space-11 it made the scale look like it had a step
nothing ever used.
Where the scale stops
It governs component rhythm — padding, gaps and margins inside buttons, cards, modals and content blocks. Section-scale numbers stay literal on purpose.
| Still literal | Uses | Why |
|---|---|---|
| 56 · 60 · 64 · 80 · 92 · 100 · 120 · 176 · 208 | 1–4 each | Each appears once or twice in an unrelated place — a column gap here, a section padding there. Tokenising them would invent a shared rhythm that doesn't exist. |
| 10 · 18 | 1 each | Off the 4px grid. A modal paragraph margin and the contact menu gap. Left alone rather than silently nudged to 8/16 — that would be a visible change, so it's a decision to make rather than a cleanup. |
| 0 | 14 | Not a scale value. |
The rule for new work: if a value repeats three or more times across components, add it to the scale. Otherwise leave it literal.
Radius & border
Fifteen further radii are hardcoded: 100px for every pill
control (×5), 24px for cards (×3), 50% for modal
close buttons (×2), plus 40px, 16px,
8px, 4px and 40px 40px 0 0. The pill
and card radii are real, recurring decisions and belong in the scale.
Border weight
1.5px is the system weight — buttons, icon buttons, cards,
insight cards and the outro rule all use it. Two deliberate exceptions:
the About portrait frame at 6px, and the image-modal close
button at 1px. Sub-pixel borders round inconsistently across
browsers at some zoom levels, so 1.5px is a choice to keep, not to
rediscover.
Motion
| Token | Value | Uses | Applied to |
|---|---|---|---|
| --ease | cubic-bezier(0.2, 0.7, 0.2, 1) | 8 | every transition |
| --t-fast | 150ms | 7 | hover feedback, both modal close buttons |
| --t-med | 300ms | 1 | the .btn-menu.active pill background |
Two speeds, deliberately. Opacity is direct feedback for the
pointer, so it is fast. The one use of --t-med is the nav
pill background, which the scroll-spy toggles as the page moves
rather than in response to a click — ambient state reads better a little
slower. .btn-menu therefore carries both.
:hover,
so it eases in and out. Only opacity and
background-color animate — focus outlines are deliberately
excluded, because focus should be instantaneous.
The one substantial animation is the case page's scroll-driven parallax:
each .parallax-section translates its content by
(scrollY − sectionTop) × (1 − speed), with speed 0.5 on
desktop and 0.8 below 1024px. The script returns early under
prefers-reduced-motion: reduce and binds no listeners;
case.css also clears any applied transform, so a preference
change mid-session settles the page rather than freezing it mid-parallax.
Reduced motion
style.css carries a global guard — * with
transition-duration: 0.01ms !important and the same for
animations and scroll-behavior. Because it loads on both
pages it covers the homepage too, which previously had none at all.
Near-zero rather than none so transitionend
still fires and nothing waiting on it can hang.
scrollIntoView({behavior: "smooth"})). That reads the
preference itself and passes "auto" instead — checked per
click rather than cached, so it follows the setting if it changes
mid-session.
02Components
Eleven components, previewed below with their real declarations. Each sits on the surface it was designed for — which, since the card merge, is the only thing that decides how any of them are coloured.
Button · .btn
The primary action. A 44px pill with a 1.5px border and an optional trailing arrow icon. Colour comes entirely from the surface class.
Both rows take their border from currentColor, so the edge
follows the surface. Until 2 Sep it was hardcoded to the yellow, which is
why the lime row's first button had no visible edge at all.
| Variant | Use when | Notes |
|---|---|---|
| .btn.dark | Any action on the green ground | The default. Used 5×. |
| .btn.light | Actions on the lime hero or banner | Now usable — not yet used in markup. |
| .btn.secondary | The lower-priority action beside a primary | Drops the surface background only; the border comes from .btn. |
States — all five, on every control
var(--color-primary), so a .btn on a lime surface
drew a lime-on-lime border and read as a floating text label — which is why
.btn.light was declared but never usable. Taking the ink the
surface class already set makes the border follow the surface like
everything else, with identical rendering on dark surfaces.
:disabled, [aria-disabled="true"]
because most of these "buttons" are anchors, which cannot carry the
disabled attribute. Both forms resolve identically —
verified in the browser. Focus is deliberately excluded from the
transition: it must be instantaneous.
Icon button · .btn-icon
A 44×44 circle holding a single SVG. Two sizes: 44px everywhere, 56px in
the contact banner (dropping back to 44px below 600px). Always carries an
aria-label; the icon inside is alt="".
Menu link · .btn-menu
Navigation only. The .active state is applied by the scroll-spy
script and pairs with aria-current="true". The
.small modifier is the case-page breadcrumb.
Text button · .btn-link
A link-shaped control that sits inside running copy. Underlined at 4px offset, transparent, inherits its font. Two uses, and they are different elements:
| Element | Where |
|---|---|
| <button> | The "more on the way" trigger in the work note — it opens a dialog, so it is a button. |
| <a> | The design system link in the footer colophon — it navigates, so it is an anchor. |
<a> this class silently loses its underline.
The reset sets a:link { text-decoration: none }, which is
(0,1,1) and beats the bare class at (0,1,0) — so the only affordance the
link had disappears. a.btn-link restores it at matching
specificity, later in the file. Any component class used on an anchor
needs the same check.
Card · .card
An icon-plus-label block for impact figures. One class, both grounds.
The border comes from --card-border, which defaults to the
yellow and is flipped to the dark green by .light — the same
mechanism as --focus-ring. A card nested inside a
.light block inherits the flip, so it is correct by
construction rather than by remembering which class to type.
+404k
policies sold
€605k
additional revenue
40%
growth in adoption
+404k
policies sold
€605k
additional revenue
rgb(21, 34, 11), the overview and learnings cards to
rgb(247, 253, 160) — identical to the two-class version.
This used to be .card plus .card-light,
byte-for-byte identical except for border-color, with inverted
names: .card-light was the variant for dark surfaces.
Insight card · .insight-card
The case study's signature block — a behavioural-design principle, tagged,
named and explained. The only component with a gradient fill: a 45°
wash from transparent to rgba(247,253,160,0.15).
Insight
When users are choosing between alternatives, the addition of a less attractive option can influence their perception of the choices.
line-height: normal, not a number.
It was 16px against a 16px font — a 1.0 ratio, so
the two-line subtitles had their ascenders and descenders nearly touching.
normal is what .description directly below
already uses, so the two text blocks in the card share one rhythm, and it
is Sora's own metric (measured: 20px at 16px) rather than a magic number
that would go stale if the face changed.
Case figure · .case-figure
The one image component a case section uses: a mockup sitting beside the
copy. Two variants — .case-figure is zoomable and opens the
lightbox; .case-figure.is-static is not, and is used once for
the intro cover.
Layout lives on the <figure>, not on the button
or the image. That is the point: both variants occupy an identical box
whether or not there is a button in the middle, so a section reads the
same either way.
<figure class="case-figure"> <button type="button" class="case-figure__zoom js-zoom-image" aria-label="Enlarge image: <describe it>"> <picture> <source type="image/webp" srcset="/assets/images/Name.webp" /> <img class="case-figure__img" src="/assets/images/Name.png" alt="<describe it>" decoding="async" /> </picture> </button> </figure>
alt string is the only machine-readable
version of them. For a new case study, prefer real captions: this component
has room for a <figcaption> under the image, and the
labels would then be actual content.
.mockup, which
is also a homepage class (#work .case-cover .mockup) —
two unrelated components sharing one name across two stylesheets.
Modal · .case-modal and .image-modal
Two modals, built twice. The case modal is a proper dialog: focus trap,
role="dialog", aria-modal,
aria-labelledby, Escape to close, and focus returned to the
trigger — deliberately skipped when the modal closes on its way to
#contact, so restoring focus doesn't fight the smooth scroll.
Just one case for now
For now, this is the only case study available here. I am working on new ones and will publish them soon.
| .case-modal | .image-modal | |
|---|---|---|
| role / aria-modal | Yes | Yes |
| Focus trap | Yes | Yes |
| Focus returned on close | Yes | Yes |
| Escape closes | Yes | Yes |
| Backdrop closes | Yes | Yes |
| Body scroll lock | .case-modal-open | .modal-open |
| Opened by | button | .case-figure__zoom button |
img.currentSrc, so it enlarges
the WebP the <source> actually served rather than
re-fetching the PNG, and copies the source image's alt across.
Two lock classes still exist for one behaviour —
.case-modal-open and .modal-open. One modal
component with a variant would collapse this table into a single row; worth
doing when a third modal appears, not before.
03Patterns
Recurring solutions that aren't components — the conventions that make the pages behave consistently.
Responsive images
Every photograph ships as WebP with a PNG fallback via
<picture>. Three rules make it work invisibly:
| Rule | Why |
|---|---|
| picture { display: contents } | Keeps the <img> as the flex/grid item, so every existing layout rule still targets it. |
| picture source { display: none } | <source> computes to display:block; contents promotes it to a grid item, silently adding a row to every container holding an image. |
| %20 in srcset | A raw space in a srcset is a descriptor separator. Filenames with spaces must be percent-encoded or the WebP is silently dropped. |
Above the fold: fetchpriority="high", no lazy loading. Below:
loading="lazy" — but never on a parallax element, whose
transform depends on a known offsetHeight.
Section heights
Homepage sections cover the viewport rather than being fixed to it:
min-height: calc(100svh − navHeight), with the
100vh line first as the fallback. Nav height is 100px above
1024px and 60px below, and scroll-margin-top matches it so
anchor landings sit just under the bar.
Scroll spy
Both the homepage nav and the case breadcrumbs highlight the last section whose top has crossed a line 40% down the viewport, with the final item forced active at the bottom of the page.
Hero wordmark sizing
The hero name is CSS-sized, not measured by JavaScript.
#hero-banner is a container
(container-type: inline-size) and the title is
clamp(28px, 19.5cqw, 320px). "Rafael Borges" in Tanker at
-0.03em advances ≈4.92× its font-size, so 20.3cqw would fill
the line exactly; 19.5cqw is the largest value that never overflows once
letter-spacing rounds to whole pixels — worst-case fill 98.0% across
320–2000px.
Do
- Let container queries size display type
- Keep Tanker on
font-display: block - Break the wordmark to two lines only on tall phones (≤600px, ≤3/5 aspect, ≥700px tall)
Don't
- Reintroduce a JavaScript text fitter
- Two-line the wordmark on tablets — it only reaches ~65% of the line and halves the portrait
- Switch to
swap— the fallback overflows the banner mid-load
Focus
--focus-ring defaults to the yellow and is flipped to the dark
green by .light and by .case-modal__content, so
the ring is always visible against whatever it is sitting on. Rings are
2px with a 2px offset (4px on .btn-link, 6px on the email
link).
.case-figure__zoom.
.btn-link and the contact email link used to hardcode
--color-primary and --color-secondary, bypassing
the mechanism built for exactly this.
Alt text
The convention is alt="" for decorative icons, with the
meaning carried by an adjacent aria-label or visible text.
Both pages now follow it throughout — 21 decorative images on the case
page, 8 on the index — and only the five content mockups carry descriptive
alt. The case page previously broke it in 12 places, where icons sitting
beside their own visible label were announced as "Idea Icon", "Shield
Icon", "Trade Up Icon", and twice as "Shiedl User Icon".
04Responsive
Eleven media queries across two stylesheets, with two shape-aware queries that width alone couldn't express.
| Query | File | What changes |
|---|---|---|
| ≤1440px | both | Outer padding 48→32px; case section padding; nav max-height 80px |
| ≤1224px | case only | Case container becomes a column; mockups to 70% and centred |
| ≤1024px | style only | Hero to 3/1.65 and horizontal 50/50; nav 100→60px |
| ≤768px | style only | h2 120→72px; hero to 3/2; About stacks |
| ≤600px | both | Hero to 3/4 portrait; work section stacks; logo → "RB" |
| ≤425px | both | Hero to 3/5; body line-height 28→24px; contact menu stacks |
| 601–1024px portrait | style only | Portrait tablets stack fully and hand the photo the full width |
| ≤600px, ≤3/5, ≥700px tall | style only | Two-line wordmark at clamp(28px, min(38cqw, 18svh), 320px) |
style.css after them will silently outrank them.
case.css's 1224px breakpoint is the odd one out: it matches
neither 1024 nor 1440, and there is no shared list of breakpoints for a
third stylesheet to follow. A set of custom media values, or simply
aligning 1224 → 1024, would make the two files agree.
05Audit
The foundations were sound and unusually well commented to begin with —
the inline comments explaining why are what made this document
possible to write from the source alone. Five rounds of fixes on
2 September closed all fourteen findings and took the score from 52 to 96.
Every token in :root is now referenced by something.
Token coverage
| Category | Defined | Referenced | Hardcoded in CSS |
|---|---|---|---|
| Colour | 11 | 11 | 5 (rgba blacks, gradient) |
| Spacing | 9 | 127 | 11 (9 section-scale, 2 off-grid) |
| Radius | 3 | 3 | 15 |
| Typography | 12 | 12 | 4 (all deliberate) |
| Motion | 3 | 16 | 0 |
| Interaction opacity | 3 | 16 | 0 |
| Breakpoints | 0 | — | 11 |
Findings
A <p class="case"> is closed with </h1>. Browsers recover, but the parse tree isn't what the markup says, and it will confuse a screen reader's heading list and anything that scrapes the page.
Fixed: the closing tag is now </p>.
The four mockups open a lightbox on click, but they are plain <img> elements — not focusable, not announced as interactive, no keyboard path in. The lightbox itself has no role="dialog", no focus trap and doesn't return focus on close.
Fixed: each is now a .case-figure__zoom button, and .image-modal has the dialog treatment .case-modal already had.
Every case section transforms its content on scroll, at every viewport, with no opt-out. Scroll-coupled parallax is a known vestibular trigger and is exactly what the media feature exists for.
Fixed: the script returns early on matchMedia("(prefers-reduced-motion: reduce)"), with a CSS guard on .parallax-content and a global one in style.css.
All nine --space-* tokens had zero references against 123 spacing declarations (165 individual values).
Fixed: rebuilt from usage data — added --space-1/--space-5, deleted the unreferenced --space-9, moved 44px out to --control-height, converted 126 values. Section-scale one-offs stay literal by design.
Font sizes, families and line-heights are literals everywhere, including inside media queries. Changing the base reading size means finding every one by hand. The scale that emerges is coherent — 14/16/20/24/32/40/60/72/120 — it just wasn't written down anywhere.
Fixed: --font-display / --font-body, nine role-named --text-* steps and --leading-display. 29 literals down to 4 deliberate ones.
Eight identical declarations each; only border-color differs. The name is also inverted — .card-light is the variant for dark surfaces, the opposite of what .light means everywhere else.
Fixed: one .card driven by --card-border, flipped by .light exactly as --focus-ring is.
Icons sitting next to their own visible label are announced twice — "Idea Icon" followed by "Insight". Two are misspelled as "Shiedl User Icon". index.html gets this right in all 8 cases, so the two pages disagree.
Fixed: alt="" on all 12. Both pages now follow the convention throughout.
.btn-link hardcodes --color-primary, so if that button is ever placed on a lime surface its focus ring becomes invisible. The email link hardcodes --color-secondary — correct today only because it happens to sit on the light banner.
Fixed: both now use var(--focus-ring); all five focus rules go through the token.
The border is var(--color-primary) rather than currentColor, so a .btn.light on the lime hero renders lime-on-lime with no visible edge. The variant is declared and has never been used — this is why. .secondary already works around it locally.
Fixed: .btn now borders with currentColor. Verified identical on dark surfaces, and both other variants behave.
color: #fd6b6b is the only semantic colour in the project and it lives in an HTML attribute. font-size: 24px appears twice as an ad-hoc sub-heading that isn't in the type scale.
Fixed: the red is now --color-negative applied via .metric-down, and the two headings use .subhead. The page has zero inline styles.
Every hover snapped instantly to opacity: 0.8, and the one transition in the codebase used 0.2s ease rather than the tokens defined for it.
Fixed: all seven hover states transition through the tokens, --t-med drives the nav pill, and a global prefers-reduced-motion guard collapses everything to instant — including the homepage's JS anchor scroll.
16px on 16px. All three subtitles in the case study wrap, so ascenders and descenders nearly collided.
Fixed: now line-height: normal, matching the .description beneath it — Sora's own metric, measured at 20px, rather than a magic number.
.hide only works inside #hero-banner below 1024px. .hide-m lives in case.css and fires at 1224px. .hide-mobile and .hide-desktop are hero-scoped and swap at 600px — and again in the portrait-tablet block. None of the names says which breakpoint it means.
Fixed: two real utilities named for their breakpoints, plus .hero-cue--inline / --overlay for the pair that were never utilities. See Patterns §3.6.
Comments switched between Portuguese and English mid-file. .case-modal declared display: none twice; .case-outro__actions and #work .case-cover .content were each split across two adjacent blocks. --radius-l and --transparency-secondary were declared and never used.
Fixed: comments are English throughout both stylesheets and both scripts, the split blocks are merged, and the two dead tokens are gone.
What's working
Worth stating plainly, because an audit reads as a list of faults: the
contrast is excellent in both directions, the surface-inversion model is a
genuinely good idea cleanly executed, the responsive work handles cases most
sites ignore (portrait tablets, short phones, container-relative display
type), and the inline comments explain why — the letter-spacing
rounding sweep, the display:contents trap, the
IntersectionObserver threshold, the font-display choice. That
commentary is better than most production codebases carry, and it is what
made this document possible to write from the source alone.
06What was done, and what is left
Every audited finding is closed. This is the record, and the short list of what is deliberately still open.
Completed 2 September 2026
The unclosed <p>, 12 alt attributes, two focus rings, .btn to currentColor.
The four mockups became .case-figure__zoom buttons; .image-modal gained a focus trap, focus restore and role="dialog"; the parallax bails out under prefers-reduced-motion.
Nine role-named steps plus the two families and --leading-display.
Driven by --card-border, flipped by .light.
126 values converted, 11 left literal by design.
With a global prefers-reduced-motion guard that also covers the homepage's JS anchor scroll.
A zoomable and a static variant, with layout on the <figure> so both occupy an identical box.
To normal, matching the description beside it — Sora's own metric, 20px at 16px.
And the hero cue pair renamed for what it is rather than when it disappears.
English throughout both stylesheets and both scripts.
Active and disabled on every control, with the scroll-spy class renamed .is-current to clear the way.
#intro /
#overview became .case-intro /
.case-overview. Watch the specificity if you touch those
rules: the generic .case-content section .container .content
is (0,3,1), so a bare .case-intro .container .content (0,3,0)
would lose. The rules keep .case-content in front, making
them (0,4,0). Don't shorten them.
Still open
Either your call, or genuinely not worth doing.
| Item | Why it is still here |
|---|---|
| A loading state | No component has one, because nothing on the site is asynchronous. The first form that posts will need it. |
Two off-grid spacing values — 10px, 18px | Nudging them to 8/16 would put the whole codebase on the 4px grid, but it is a visible change. Your call, not a silent cleanup. |
.case-modal__content uses 1.4rem / 0.95rem | 22.4px and 15.2px, neither a step of the type scale. Same reasoning as above. |
A <figcaption> on .case-figure | The honest place for a "click to enlarge" cue, and for the before/after labels currently baked into the image pixels. Blocked until case #2's assets are exported with the labels out of the artwork. |
| The scroll cue exists twice in the markup | One copy is always hidden, so it is harmless — but it is duplicated content. A single element repositioned by CSS would be cleaner if the hero is reworked. |
| This document is synced by hand | Three copies: the repo markdown, the published page, and myPortfolio/design-system.html. The weakest part of the setup, and why Documentation sits at 8/10. |
font-display: swap (breaks the hero mid-load),
aligning scroll-behavior (case.css overrides it
intentionally), and the 1.5px borders. These look like inconsistencies
and are decisions.
Type set in Tanker (self-hosted) and Sora; data in IBM Plex Mono.
Component previews use the source declarations verbatim.