Skip to content

feat: Upgrade to Bootstrap 5 and bring the brand colours onto the site - #195

Merged
magnus-madsen merged 11 commits into
masterfrom
chore/upgrade-bootstrap-5
Aug 7, 2026
Merged

feat: Upgrade to Bootstrap 5 and bring the brand colours onto the site#195
magnus-madsen merged 11 commits into
masterfrom
chore/upgrade-bootstrap-5

Conversation

@magnus-madsen

@magnus-madsen magnus-madsen commented Aug 7, 2026

Copy link
Copy Markdown
Member

Upgrades Bootstrap from 4.6.2 — deprecated on npm, unsupported since 2023 — to 5.3.8, then follows through on the colour decisions the upgrade forced.

The upgrade

Bootstrap was only ever used here as CSS, because its v4 JavaScript needed jQuery. v5 dropped that dependency, so the two hand-rolled widgets that imitated Bootstrap by toggling its classes are gone, replaced by importing the collapse and carousel modules. jquery and popper.js fall out of the lockfile.

That also retired a coupling that was about to break twice over, both times silently — no build error, no console error:

  • Carousel.astro hardcoded carousel-item-left / -right in a TypeScript ternary. v5 renamed those to -start / -end while leaving -next / -prev alone, which is exactly what makes it easy to miss.
  • Its <ol><li> indicators would have rendered invisible, because v5 styles indicators through the attribute selector .carousel-indicators [data-bs-target].

The rest is renamed utility classes, plus a small compatibility layer in global.css for the three things v5 removes outright: .card-columns, which lays out four sections of the principles page, .text-justify, and v4's link decoration. .container is pinned to 1140px so v5's new xxl breakpoint doesn't widen every page by 16%.

What looks different

  • The navbar is dark (bg-dark with data-bs-theme="dark") rather than bg-info. That class is the slot Bootstrap reserves for informational messages, and v5 retuned its cyan to a far lighter #0dcaf0 on which white text reaches only about 2:1.
  • The Flix mark is in the navbar, lifted out of design/og.svg. The site previously carried no logo anywhere.
  • The current page is marked in brand red, and links move from Bootstrap's blue to #a63839.
  • The green is more saturated. Bootstrap's $success sits at hue 152, already halfway to teal, at 69% saturation. #008938 is fully saturated at hue 145 and exactly as dark — it cannot be lighter, since inline spans in body copy hold it to 4.5:1.
  • The social card is redrawn in that green. Both design/og.svg and the rendered public/og.png had kept painting "effect-oriented" in v4's #28a745, which left the card as the last place the old green survived — and the one a visitor meets before the site itself.
  • The carousel indicator dots are visible for the first time. They inherited white, on screenshots that are also white.

Every colour pairing was measured against WCAG; the ratios are recorded in the individual commit messages.

Fixed along the way

  • Open Sans was loading weight 400 only, while Bootstrap sets every heading to 500 and the pages use fw-bold, <b> and <strong> in more than fifty places. All of it was browser-synthesised. Loading the right weights and trimming to the Latin subsets took the build from eighteen font files at 217 KB down to eight at 126 KB.
  • @shikijs/themes was imported by CodeSnippet.astro but never declared, resolving only because npm hoists it out of astroshiki.
  • design/og.svg briefly stopped being well-formed XML, when the comment added to name the Bootstrap success custom property put a double hyphen inside it. Nothing here catches that: the file sits outside src/ and public/, so Astro never parses it, and CI runs only astro check and the build.

Verification

astro check and npm run build are clean. The emitted HTML was diffed page by page against a Bootstrap 4 baseline build, and both widgets were exercised against the real Bootstrap 5 JavaScript in a jsdom harness — indicator selectors, the -start / -end rename, slide/dot/aria-current synchronisation, and the collapse toggle's aria-expanded.

The card was rendered with rsvg-convert. Its code line carries explicit per-token x offsets that assume Monaco's 0.6001 em advance, so a substituted monospace would open a gap before every token — quietly, since the render still succeeds. Open Sans is not a system font either; the latin 400 and 700 faces were converted from the @fontsource/open-sans .woff files with fontTools and supplied through FONTCONFIG_FILE, and fc-match confirmed all three faces resolved to real files rather than substitutes.

Diffing the result against the previous PNG then settles it from the other end: the only pixels that moved lie inside (276, 230)(538, 259), exactly the word "effect-oriented". 3784 pixels changed, none outside that line, and the glyph coverage is identical at 2313 core pixels with matching antialiasing counts — only the fill differs. The mark, "Flix", the whole Monaco code line and "flix.dev" come out byte-identical to the old render.

🤖 Generated with Claude Code

magnus-madsen and others added 9 commits August 7, 2026 17:16
npm marks 4.6.2 deprecated and Bootstrap 4 has been out of support since
2023.

Bootstrap was only ever used here as CSS, because its v4 JavaScript needed
jQuery. v5 dropped that dependency, so the two hand-rolled widgets that
imitated Bootstrap by toggling its classes are gone, replaced by importing
the collapse and carousel modules. That also removes the coupling those
scripts had to v4 internals, which was about to break twice over: the
carousel hardcoded carousel-item-left/-right, renamed to -start/-end in v5,
and its <ol><li> indicators would have rendered invisible because v5 styles
them via .carousel-indicators [data-bs-target]. Neither failure would have
produced an error. The carousel deliberately omits data-bs-ride so it keeps
its current click-to-advance behaviour rather than autoplaying.

The rest is renamed utility classes, plus a compatibility layer in
global.css for the three things v5 removes outright: .card-columns, which
lays out the principles page, .text-justify, and v4's link decoration. The
navbar's bleed margin now derives from the container gutter instead of
hard-coding 15px, and .container is pinned to 1140px so v5's new xxl
breakpoint does not widen every page by 16%. Heading utility classes are
paired with their elements so the stats tables keep fixed sizes rather than
opting into v5's fluid type.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
bg-info was never a brand choice: it is the slot Bootstrap reserves for
informational messages, and v5 retuned that cyan from #17a2b8 to a much
lighter #0dcaf0. The navbar cannot be light here, because the page sits on
a white card and Bootstrap's default light-grey navbar would not read as a
header at all, so take the other conventional option.

bg-dark with data-bs-theme="dark" keeps this to stock utilities rather than
a custom colour. The attribute has to sit on the .navbar element itself:
Bootstrap keys the light-on-dark link colours and the white-stroke toggler
icon off .navbar[data-bs-theme="dark"], so putting it on a wrapper would
leave a dark hamburger on a dark bar.

Contrast goes from roughly 2.3:1 to 5.7:1 for nav links and 15.4:1 for the
active one, which combined with its existing fw-bold may now overstate the
current page.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
design/og.svg is the only place the identity has been deliberately composed,
and it settles on red for the mark, near-black for the wordmark, and green
for the one phrase that matters. The site used none of the red, and carried
no logo at all -- Flix was recognisable everywhere except its own pages.

Put the mark in the navbar, underline the current page in red, and hand the
links over from Bootstrap's blue, which was the last colour here that
belonged to the framework rather than to Flix. That keeps red on identity
and navigation and leaves green doing content emphasis, so the two never
compete for the same job.

The red needs two strengths, because no single value covers both: #cf4647
is the mark's own colour and is right in shapes, but as text on white it
only reaches 4.5:1, exactly the limit. Darkening it 20% gives 6.5:1, so
anything made of words takes that instead. Bootstrap's primary moves with
the links, since .link-primary reads --bs-primary-rgb rather than the link
variables and would otherwise have left the documentation card titles blue
while every other link turned red.

The active underline waits for the expand breakpoint: in the collapsed menu
the links stack, and a rule under one of them reads as a divider. Every link
carries it transparent so the active one does not shift the row.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two problems, both surfaced by adding the brand.

The navbar's container is justified space-between. With only the toggler on
that line it sat at the start and never met the fork-me ribbon; giving
space-between a second item pushed the toggler hard right, into the 149px
the ribbon claims in the corner. Hide the ribbon below the breakpoint rather
than move the toggler -- on a phone it was covering a good part of the
screen to begin with, and above the breakpoint it stays where it has always
been.

The bar also collapsed later than it should. The brand and nine links need
roughly 807px, against the 720px container md gets, so the row was cramped
well before the toggler appeared. lg gives it 960px.

That leaves three things keyed to the same breakpoint, and they have to move
together: the expand class, the ribbon's visibility, and the media query
around the active-page underline. Left at md, that last one would have drawn
a red rule under one item of the stacked menu between 768 and 991px, where
it reads as a divider rather than a marker.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Bootstrap's success green reads flat, and the reason is not that it is dark.
It sits at hue 152, already halfway from green to teal, at 69% saturation.
Replacing it with a fully saturated green at hue 145 changes how vivid it
looks without changing how dark it is.

It cannot get lighter. Most of the sixteen uses are large and bold and only
owe 3:1, but seven are inline spans inside body paragraphs at 16px, plus a
card title on /documentation/, and those hold the colour to 4.5:1. #008938
clears that by 0.03, the same margin Bootstrap left itself with #198754.

Note that the value is 4.53:1 as written, not as derived: rounding a colour
to the nearest of 256 steps per channel moves contrast by around 0.06, which
at this threshold is the difference between passing and failing. Measure the
hex, not the colour that produced it.

Setting it on --bs-success means all sixteen call sites pick it up without
touching any markup, the same way the links took the brand red.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
og.svg still painted "effect-oriented" in Bootstrap 4's #28a745, so the card
and the homepage have shown different greens since the upgrade -- and a third
one once the site moved to #008938. Bring the card into line, and say in the
header that this colour tracks --bs-success, since nothing previously
connected the two.

Also corrects a reference to InlineEditor.astro, renamed to CodeSnippet.astro
in #187.

public/og.png is NOT regenerated by this commit and still shows the old
green. Re-rendering needs a machine with Monaco: the code line's per-token x
offsets assume its 0.6001em advance, and a substituted monospace opens a
visible gap before every token. On macOS:

    brew install librsvg
    brew install --cask font-open-sans
    rsvg-convert design/og.svg -w 1200 -h 630 -o public/og.png

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CodeSnippet.astro imports @shikijs/themes/light-plus to extend the Light+
theme with the magenta the Flix grammar uses for keywords, but the package
was never declared. It resolved only because npm hoists it out of
astro -> shiki, so the homepage's syntax highlighting was one Astro bump or
hoisting change away from failing to resolve.

Pinned to shiki's own 4.4.1 rather than the newer 4.4.2 that ^4.4.1 selects
on its own: taking the newer one leaves two copies in the tree, which is the
situation this commit exists to avoid.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Only the 400 weight was imported, while Bootstrap sets every heading to 500
and the pages lean on fw-bold, <b>, and <strong> in more than fifty places.
All of it was synthesised -- the browser algorithmically smearing the
regular face -- which shows most at display sizes, on the h1 and the motto.

Switching to the Latin subsets at the same time more than pays for the extra
weight: the build drops from eighteen font files at 217 KB to eight at
126 KB, having gained a whole weight. Open Sans also ships Cyrillic, Greek,
Hebrew, math, symbols and Vietnamese, none of which this site can reach --
the only characters above U+00FF anywhere in the rendered output are the
trademark sign and the two dashes, all inside the Latin range.

latin-ext is included even though nothing currently needs it. It carries the
Polish, Czech and Turkish letterforms, and a contributor or sponsor list is
exactly the sort of content that acquires one of those quietly; 30 KB is
cheap next to a name silently falling back to a system font.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two rules inverted the caption and the controls so they would read against
the light VSCode screenshots. Neither touched the indicators, which inherit
--bs-carousel-indicator-active-bg -- white, on screenshots that are also
white. The dots have not been visible.

Bootstrap 5.3 has carousel-dark for precisely this, and it covers all three:
black indicators, a black caption, and an inverted control icon. That
retires both rules, and scopes the inversion to the icon rather than
wrapping each whole control in a filter that also created a stacking
context.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@magnus-madsen magnus-madsen changed the title Upgrade to Bootstrap 5 and bring the brand colours onto the site feat: Upgrade to Bootstrap 5 and bring the brand colours onto the site Aug 7, 2026
magnus-madsen and others added 2 commits August 7, 2026 18:28
7cb8fdc added a comment naming the Bootstrap success custom property, which
put a double hyphen inside an XML comment. XML forbids that outright, so the
file has not parsed since -- rsvg-convert would have failed on the very
render that commit was asking for.

Nothing caught it. design/og.svg lives outside src/ and public/, so Astro
never parses it, and CI only runs astro check and the build. The file has no
validation at all.

Name the property without its prefix, and say why in the comment so the next
person does not reintroduce it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
7cb8fdc brought design/og.svg's "effect-oriented" onto #008938 but could not
re-render the card, so public/og.png has kept shipping Bootstrap 4's #28a745
-- the one place a visitor still saw the old green after the upgrade.

Rendered with rsvg-convert. Open Sans is not a system font, so the latin 400
and 700 faces were converted from the @fontsource/open-sans .woff files to
.ttf with fontTools and supplied through FONTCONFIG_FILE; Monaco came from
the system. fc-match confirmed all three resolved to real files rather than
substitutes, which is what the offsets in the code line depend on.

Diffing against the previous PNG shows the only pixels that moved are inside
(276, 230)-(538, 259), exactly the word "effect-oriented": 3784 pixels, none
outside that line, and the glyph coverage is unchanged at 2313 core pixels
with identical antialiasing counts. The mark, "Flix", the whole Monaco code
line and "flix.dev" are byte-identical to the old render, so the substituted
monospace that 7cb8fdc warned about -- a gap opening before every token --
demonstrably did not happen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@magnus-madsen
magnus-madsen merged commit c777ec4 into master Aug 7, 2026
2 checks passed
@magnus-madsen
magnus-madsen deleted the chore/upgrade-bootstrap-5 branch August 7, 2026 17:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant