Overview#
Profile is a complete personal website for engineers and designers, built as a WordPress block
theme. The fictional owner is Sam Lindqvist, a senior frontend engineer in Stockholm. The theme has
a home page with a /now section, highlights, selected work and the latest writing, a printable
résumé, a project gallery filtered by technology with GitHub-style stats and a contribution graph,
a project case study, a writing archive grouped by year with instant search, a blog post, a /uses
page, a contact page with a validated form and a copy-email button, and a 404 page, in light and
dark themes. A Ctrl/⌘ K command palette jumps to any page. It is version 1.0.1 (see
template.json and style.css), works with WordPress 7.1 and PHP 7.4 or newer, and is the
WordPress version of the HTML template of the same name. See it live at
freeframe.dev/templates/profile.
Everything you see is made of core WordPress blocks arranged in block patterns, so you edit text,
links, buttons, lists and images in the block editor without touching code. Colours, the fonts
and font sizes are design tokens in theme.json: change them in Appearance > Editor > Styles and
the whole site follows.
Quick start#
Requirements: WordPress 7.1 or newer and PHP 7.4 or newer. No plugins are needed.
- Download
profile.zipfrom freeframe.dev/templates/profile (the WordPress tab). - In your WordPress admin go to Appearance > Themes > Add New Theme > Upload Theme, choose the zip and click Install Now, then Activate. The theme is listed as Freeframe Profile.
- Go to Appearance > Profile setup and click Create the demo pages. This creates the Home, Résumé, Projects, case study, Writing, blog post, Uses and Contact pages from the theme's page patterns, sets Home as the front page (Settings > Reading) and creates the header menu. Pages that already exist with the same slug are left alone, so it is safe to click it again.
- In Settings > General, set the Site Title to your name and the Tagline to your role: the header shows the title, and the home page's browser title is "Title — Tagline".
- Visit your site. Edit any page in Pages, and the header, footer and templates in Appearance > Editor.
You can also try the theme without installing anything: the Try it in WordPress button on freeframe.dev opens it in WordPress Playground (WordPress running in your browser) with the demo pages already set up.
To work on the theme's files locally you need Node.js 24 and pnpm; WordPress itself runs in WordPress Playground, so there is no PHP, web server or database to install:
pnpm install
pnpm dev # builds the CSS, then starts WordPress with the theme at http://127.0.0.1:9400pnpm dev signs you in as admin (password password) and creates the demo pages. The site is
rebuilt from scratch every time you start it; your edits to theme files are live, edits made in the
WordPress admin are not kept.
Pages#
Each page is a page pattern (in Patterns > Profile pages) made of section patterns. Every section is also offered on its own in the block inserter under Patterns > Profile.
| Page | Slug | Page pattern | Sections (pattern files in patterns/) |
|---|---|---|---|
| Home | / |
page-home.php |
home-hero (introduction and portrait), now, highlights, selected-work, latest-writing, home-cta |
| Résumé | /resume/ |
page-resume.php |
resume-header (name, contact details, print button), resume-body (summary, experience, skills, education, certifications, languages) |
| Projects | /projects/ |
page-projects.php |
projects-header, github-activity (stats and contribution graph), project-list (technology filter and cards) |
| tinytable case study | /project/ |
page-project.php |
case-study |
| Writing | /writing/ |
page-writing.php |
writing-header, writing-search, writing-archive (entries by year), rss |
| The quiet power of the dialog element | /post/ |
page-post.php |
post-article |
| Uses | /uses/ |
page-uses.php |
uses-header, uses-list, uses-settings (a code sample) |
| Contact | /contact/ |
page-contact.php |
contact-header, contact-form (form, email, availability, links) |
| 404 | any missing URL | templates/404.html |
404 |
The case study and the blog post are examples: duplicate their page (Pages, hover the page,
Duplicate with a plugin, or copy all blocks into a new page) for each project or article, and
link to it from the Projects and Writing pages. WordPress posts use templates/single.html (see
Writing); the blog, archives and search results use
templates/index.html, templates/archive.html and templates/search.html.
Project structure#
freeframe-profile/
style.css theme header (name, version, requirements, license); no styles
theme.json design tokens: palette, fonts, font sizes, layout widths, button
functions.php loads the files in inc/
inc/setup.php stylesheet and script (with their strings and page URLs), editor
styles, dark mode script, theme colour, 404 noindex, pattern categories
inc/icons.php registers assets/icons/*.svg for the Icon block
inc/icon-labels.php the icons' names shown in the editor
inc/block-styles.php button block styles
inc/site-setup.php Appearance > Profile setup: creates the demo pages and the menu
templates/ page.html (pages), page-with-title.html, single.html, index.html,
archive.html, search.html, 404.html
parts/ header.html and footer.html (they show the header and footer patterns)
patterns/ every section and page (PHP files with block markup)
assets/css/theme.css the compiled stylesheet (do not edit: built from src/)
assets/css/editor.css the same for the block editor (generated by src/editor-css.mjs)
assets/js/main.js the site's JavaScript
assets/js/editor.js block editor tweak (removes WordPress's Outline button style)
assets/icons/ SVG icons for the Icon block
assets/img/ portrait (avatar.svg), favicon, social image
assets/fonts/ Inter and JetBrains Mono and their licenses (OFL.txt)
src/theme.css the Tailwind CSS source of assets/css/theme.css
src/icons.css icons drawn before or after text with CSS (generated)
src/editor-css.mjs writes assets/css/editor.css after each build
languages/ freeframe-profile.pot, the template for translations
blueprint.json WordPress Playground setup used by `pnpm dev`
screenshot.png the picture shown in Appearance > Themes
package.json the CSS build and the local WordPress (Node.js tooling)
readme.txt, README.md, DOCS.md, CHANGELOG.md, LICENSE.txtCustomising#
Brand and colours#
The colours are in theme.json (settings.color.palette) and in Appearance > Editor > Styles >
Colors > Palette:
| Colours | Used for |
|---|---|
| Accent 50 to Accent 900 | the teal of eyebrows, links' underlines, the contribution graph, focus rings, the "open to roles" badge |
| Zinc 50 to Zinc 950, White, Black | the neutrals of text, cards, borders and buttons |
| Base and Contrast | the page background and text; they switch to dark values in dark mode |
The theme's stylesheet reads the Accent colours from the palette, so changing Accent 700 in Styles
recolours every eyebrow and accent at once. Pick a full ramp (light 50 to dark 900) so hover
states and dark mode keep their contrast. In theme.json:
{ "slug": "accent-700", "name": "Accent 700", "color": "#0f766e" }The neutrals used by the design itself come from Tailwind's zinc scale in the stylesheet; the Zinc
palette entries are the same colours for your own blocks. The dark mode values of Base and
Contrast are set in src/theme.css (html.dark { --wp--preset--color--base: ... }).
Typography#
The fonts are Inter (body and headings, weights 400 to 700) and JetBrains Mono (dates, chips,
eyebrows and code, 400 to 500), bundled in assets/fonts/ and declared in theme.json
(settings.typography.fontFamilies), so no request goes to Google. To use other fonts, install
them in Appearance > Editor > Styles > Typography > Fonts (the Font Library) and select them,
or replace the fontFace entries in theme.json. The theme's utility classes use the families
--wp--preset--font-family--inter (font-sans) and --wp--preset--font-family--mono
(font-mono), so rename those slugs only together with src/theme.css.
Font sizes are the presets in theme.json (xs 0.75rem to 9xl 8rem, the Tailwind scale). The
stylesheet uses them too: changing Large in Styles changes every text-lg in the design.
Content#
Edit pages in Pages: click any text to change it, select a button to change its link, select
an image and use Replace. Sections are groups; open List View (the stacked lines icon at
the top left) to select a section, move it, duplicate it or delete it. To add a section from
another page, use the block inserter (+) > Patterns > Profile. Search the site for "Sam
Lindqvist", "Northwind" and sam@example.com when you make it yours: they appear in the pages,
the header (the Site Title), the footer and the case study.
Some content is in Custom HTML blocks, because blocks can't express it: edit those in the block's HTML view.
| Pattern | Custom HTML | What it is |
|---|---|---|
header |
the command palette, dark mode and menu buttons | buttons driven by assets/js/main.js |
resume-header |
the "Download / print PDF" button | prints the page |
github-activity |
the contribution graph's colour legend | decorative |
project-list |
the technology filter buttons | see Project filter |
case-study |
the table illustration | an illustration built in HTML; replace it with an Image block |
writing-search |
the search field | see Writing search |
contact-form |
the form, its thank-you message, the email address and copy button | see Forms |
A few blocks carry a class (Block settings > Advanced > Additional CSS class(es)) that the
script reads. Blocks can't store data- attributes, so the theme writes them as classes and the
first lines of assets/js/main.js turn them back into attributes: ffd-name--value becomes
data-name="value" (an underscore stands for a space), ffa-name--value becomes the attribute
name="value" (ffa-role--meter, ffa-aria-valuenow--95), and ff-hidden hides a block until
the script shows it. The ones you will meet:
ffd-tags--React,CSSon a project card (an Article group inproject-list): the technologies the filter matches, comma-separated without spaces.ffd-poston a writing entry (a list item) andffd-year-groupon each year's section: the search reads them. See Writing search.ffd-contribon the empty group the contribution graph is drawn in.revealfades a block in as it scrolls into view;link-cardon a group makes the whole group clickable through its first link.
To add a project, duplicate a card in List View (in project-list and, if you like, in
selected-work on the home page), change its name, link, description, technology chips and
stats, and set its ffd-tags--... class. Add a filter button for a new technology in the filter's
Custom HTML (data-filter="Rust", the same spelling as in the tags).
Rolling dates. "Full-time roles from January 2027" in the contact form pattern is <span data-months-ahead="n">: assets/js/main.js shows the month n months from now and its year, so
the line never goes stale. The month written in the page is shown without JavaScript. For a fixed date, replace the span
with plain text.
Images and artwork#
- Portrait:
assets/img/avatar.svgin an Image block on the home page and the blog post. Replace it with your photo from the Media Library (square, at least 448 × 448 px); the round crop comes from the block's classes. - Logo: the header shows
assets/img/favicon.svgin an Image block next to the Site Title block. Replace the image in Appearance > Editor > Patterns > Header; the name comes from Settings > General > Site Title. - Project covers are coloured gradients drawn by the card's classes (for example
[background:linear-gradient(135deg,#0f766e,#2dd4bf)]on the cover group); change the colours in Additional CSS class(es), then runpnpm buildif you use new ones, or put an Image block in the cover group instead. - Icons are Icon blocks: select one and pick another icon from the Profile collection (the
theme's icons) or WordPress's own. Icons inside a link or button (the arrows, the social icons in
the footer) are drawn by CSS: the class
icon-after-arrow-rightadds one; the available names are insrc/icons.css. - Social image:
assets/img/og-image.png(1200 × 630, sourceog-image.svg). WordPress doesn't output Open Graph tags by itself; see SEO and sharing. - Favicon:
assets/img/favicon.svgis used until you set a Site Icon in Settings > General.
Navigation, header and footer#
The header and footer are template parts (parts/header.html, parts/footer.html) showing the
header and footer patterns. Edit them in Appearance > Editor > Patterns > Header / Footer.
The header has two Navigation blocks showing the same menu: the desktop one and the one in the
mobile menu. Edit the menu once in Appearance > Editor > Navigation (the setup creates "Main
menu" with About (the home page), Résumé, Projects, Writing, Uses and Contact). A Navigation block
that doesn't point at a menu shows the most recently created one; to pin a menu, select the
Navigation block and choose it in the block settings, for both blocks. WordPress marks the link
to the current page; the case study and the blog post mark Projects and Writing (their sections
carry the classes ff-section-projects and ff-section-writing, read by assets/js/main.js).
The footer's social links are Button blocks with the Icon only style (the text stays for
screen readers) and the class icon-before-github, icon-before-linkedin,
icon-before-mastodon or icon-before-email. The copyright year is added by PHP.
Dark mode#
The switch in the header toggles the dark class on <html> and remembers the choice in
localStorage (key theme, value dark or light). Without a saved choice (or with any other
stored value) the visitor's system setting decides, and the page follows it when it changes. A
small script printed first in <head> (freeframe_profile_theme_script() in inc/setup.php)
applies the class before the page is drawn, so there is no flash. The command palette's "Toggle
dark mode" does the same as the switch.
To always show the light theme, remove the switch from the header pattern and add this to a child
theme's functions.php:
add_filter( 'freeframe_profile_dark_mode', '__return_false' );Dark mode styles are the dark: classes on blocks; colours picked in the editor's colour panels
are the same in both modes, except Base and Contrast.
Features#
Command palette#
Press Ctrl K or ⌘ K, or click the search button in the header (data-palette-open), to
open a native <dialog> listing pages and actions. Typing filters the labels and hints; Up and
Down move, Enter opens, Esc or a click on the backdrop closes it. The keyboard hints
(<span class="kbd" data-kbd> in the footer and in text) show "⌘K" on Apple devices and "Ctrl K"
elsewhere.
The entries are PAGES in the "Command palette" part of assets/js/main.js. Their links use the
site's page URLs, printed by freeframe_profile_page_urls() in inc/setup.php (by the HTML
template's file names: resume.html is the page with the slug resume):
var PAGES = [
{ label: __('About'), hint: __('Home page'), href: pageUrl('index.html') },
{ label: __('Résumé'), hint: __('Experience, skills, education'), href: pageUrl('resume.html') },
// ...
{ label: __('Toggle dark mode'), hint: __('Action'), action: toggleTheme }
];Add an entry with href for a page (a full URL works too), or action for a command, and add its
label and hint to freeframe_profile_script_strings() to make them translatable. Make the change
in a child theme so updates don't overwrite it (see Child themes).
Printable résumé#
The "Download / print PDF" button (data-print) prints the page. The print styles at the end of
the components in src/theme.css hide the header, footer and anything with the class no-print,
and the résumé's print: classes give a clean one- or two-page CV. Add print-break-avoid to
blocks that shouldn't be split across pages. Opening /resume/#print prints after the page
loads; the palette's "Print résumé" opens it from other pages and prints straight away on the
résumé.
The skill bars are groups with ffa-role--meter and ffa-aria-valuenow--95 classes (the
script turns them into role="meter" and aria-valuenow="95") and an inner group whose width is
the class [width:95%]. Change both numbers together.
Project filter#
On the Projects page, the filter buttons (data-filter, aria-pressed) show only the cards whose
ffd-tags--... class includes the technology, and update the line "Showing 2 projects tagged
React" (ffd-filter-status, announced to screen readers). The contribution graph is decorative
sample data drawn by assets/js/main.js in the empty ffd-contrib group; replace it with your
own picture or remove the section.
Writing search#
On the Writing page, typing in the search field hides the entries that don't match, hides empty
years and shows the "No posts match that search." message (ffd-post-empty). Each entry's search
text is its title and topic, read from the entry when the page loads (the HTML version writes it
in a data-search attribute): the title is the first text in the entry's link, the topic is the
text before "·" in the last one ("HTML · 6 min" → "html"). So an entry you add or rename is found
without any extra setting. Curly apostrophes (WordPress typography) are searched as straight
ones.
To add an entry, duplicate a list item in a year's list, change the date, the title, the topic and reading time, and the link. For a new year, duplicate a year section and change its heading.
Writing: pages or posts#
The writing archive and the blog post are pages, edited like any other content. This keeps the demo exactly like the HTML version (an archive grouped by year with instant search, entries with dates and topics) and keeps everything in one place. Each article is its own page: duplicate the blog post page, write the article, and add an entry to the Writing page (and to Latest notes on the home page).
If you prefer WordPress posts (categories, tags, comments, feeds, the post list in the admin),
publish them in Posts: templates/single.html uses the same article layout as the demo post
(breadcrumb-style category, title, author and date, the same text styles, tags as chips). Show
them with a Query Loop block, for example on the Writing page in place of the archive, or set a
posts page in Settings > Reading (it uses templates/index.html). The instant search only
works with the Writing page's list.
Copy email#
On the Contact page, the copy button (data-copy-email, in a Custom HTML block with the address)
copies the address and says so under it (ffd-copy-status); where copying isn't allowed it selects
the address and suggests Ctrl+C / ⌘C.
Scroll reveals, mobile menu and back to top#
- Blocks with the
revealclass fade in as they scroll into view, and everything above them is revealed too. They show immediately with reduced motion, without JavaScript and when printing. - The menu button (
data-menu-toggle) opens#mobile-menu, updatesaria-expandedand its label, closes with Esc (returning focus to the button) and closes itself on wide screens. - A "Back to top" button appears after about one screen of scrolling; it scrolls to the top
(instantly with reduced motion) and moves focus to the main content. It is created by the "Back
to top" part of
assets/js/main.js; to remove it, delete that part and the.back-to-topstyles insrc/theme.css, then runpnpm build.
Block styles#
In the block settings' Styles panel:
| Block | Styles |
|---|---|
| Button | Fill (default, dark), Secondary (outlined), Light (on dark), Outline (on dark), Icon only (WordPress's own Outline style is replaced by Secondary) |
Code samples are Code blocks (they can be scrolled with the keyboard: the class ffa-tabindex--0
makes them focusable), and the 404 page's terminal is a Preformatted block.
Forms#
The contact form (a Custom HTML block in contact-form) has these fields:
| Field | Rule |
|---|---|
name |
Required |
email |
Required, must look like an email address |
topic |
Required choice |
message |
Required, at least 20 characters (with a live character count) |
The rules and messages are in the rules object in the "Contact form validation" part of
assets/js/main.js (the messages are translated in inc/setup.php). Each field shows its message
under it and gets aria-invalid; the first problem is focused. On success the form is replaced by
the thank-you message (focused), and "Write another" brings the empty form back. Nothing is
sent: it is a demo form until you connect it.
Option 1: a form plugin#
The usual way on WordPress. Install a form plugin (WPForms, Gravity Forms, Fluent Forms, Contact
Form 7, Jetpack Forms...), build the form there, then on the Contact page delete the two Custom
HTML blocks of the form and its thank-you message and insert the plugin's block or shortcode in
their place. The plugin sends the messages and stores them. To make its fields look like the
theme's, add the class field to fields and btn btn-primary to the submit button where the
plugin allows custom classes.
Option 2: a form service#
Keep the theme's form and send it to a service such as Formspree:
-
Create a form on the service and copy its endpoint URL.
-
Edit the Contact page, open the form's Custom HTML block and add the endpoint to the form tag:
html<form data-contact-form novalidate class="grid gap-5" action="https://formspree.io/f/your-id" method="POST"> -
In
assets/js/main.js(in a child theme, see below), in the form's submit handler, post the data before showing the thank-you message:jsfetch(form.action, { method: 'POST', body: new FormData(form), headers: { Accept: 'application/json' } }) .then(function (res) { if (!res.ok) throw new Error('Request failed'); form.hidden = true; success.hidden = false; success.focus(); });Make this change in a child theme (copy
assets/js/main.jsthere and load it instead), or it will be overwritten when you update the theme.
Saved data (localStorage)#
| Key | Value | Used for |
|---|---|---|
theme |
"dark" or "light" |
the visitor's dark mode choice; any other value is ignored and the system setting is used |
The key is generic; if other sites share your domain, rename it (for example profile-theme) in
assets/js/main.js and in freeframe_profile_theme_script() in inc/setup.php. Clear it from
the browser's developer tools (Application > Local Storage).
SEO and sharing#
Page titles come from WordPress: the page title, an em dash and the site name (for example
"Projects — Sam Lindqvist"); the home page shows the site name and tagline (Settings >
General). The 404 page tells search engines not to index it (freeframe_profile_robots() in
inc/setup.php). The theme doesn't output meta descriptions or Open Graph tags; use an SEO plugin
such as Yoast SEO, Rank Math or The SEO Framework, and give it assets/img/og-image.png (or your
own 1200 × 630 image) as the default social image. WordPress already generates a sitemap at
/wp-sitemap.xml and, for posts, a feed at /feed/ (point the RSS button on the Writing page at
it if you publish posts).
Accessibility#
Built in: a skip link to the main content (added by WordPress, styled by the theme), landmarks
(header, navigation with labels, main, footer), one h1 per page, visible focus rings, a labelled
dark mode switch with aria-pressed, a command palette with combobox and listbox roles, the mobile
menu button with aria-expanded, filter buttons with aria-pressed and a live status, skill bars
as meters, focusable code samples, form errors announced through aria-describedby and
aria-invalid, alt text on the portrait, and reduced-motion support for every animation. The demo
passes axe (WCAG 2.1 A and AA) in light and dark mode.
When you edit: keep one Heading 1 per page, give images alt text (or mark them decorative), keep
link text meaningful, and label any field you add to a form (<label for="...">).
Building and deploying#
You only need this if you change the theme's files. The stylesheet assets/css/theme.css is
compiled with Tailwind CSS from src/theme.css and the classes used in patterns/, parts/,
templates/, inc/ and assets/js/:
pnpm install
pnpm build # compile assets/css/theme.css and assets/css/editor.css
pnpm watch # recompile assets/css/theme.css on every change (run pnpm build before you commit)Run pnpm build after adding a utility class to a pattern or template, otherwise the class has no
style. Classes you type in the editor's Additional CSS class(es) field are only styled if the
stylesheet already contains them.
To deploy, zip the theme folder (named freeframe-profile, without node_modules/) and upload it
in Appearance > Themes, or copy the folder to wp-content/themes/ on your server.
Translations#
The theme is translation-ready with the text domain freeframe-profile. All text in patterns,
templates and PHP goes through WordPress's translation functions; the script's strings (the
command palette, labels, form errors) are translated in PHP (freeframe_profile_script_strings()
in inc/setup.php). Create a translation with Loco Translate, or with WP-CLI and Poedit:
wp i18n make-pot wp-content/themes/freeframe-profile wp-content/themes/freeframe-profile/languages/freeframe-profile.potSave freeframe-profile-de_DE.mo (for example) in wp-content/languages/themes/ or in the theme's
languages/ folder. Page content created by Profile setup is copied into your pages in the site's
language at that moment; translate it in the editor like any other content. The filter buttons
keep their English data-filter values; translate only their text.
Child themes#
Use a child theme for changes you want to keep when Profile is updated. Create
wp-content/themes/profile-child/style.css:
/*
Theme Name: Profile Child
Template: freeframe-profile
*/Templates, parts, patterns and theme.json in the child theme override the parent's files of the
same name. The parent's stylesheet only contains the classes the parent uses: if your child theme's
patterns use new utility classes, compile a stylesheet for the child (copy package.json and
src/theme.css, point the @source lines at the child's folders) and enqueue it. To change the
script (the palette's entries, the form), copy assets/js/main.js into the child theme and load
it in place of the parent's with wp_deregister_script( 'freeframe-profile' ) and
wp_enqueue_script() on wp_enqueue_scripts (priority 30), keeping the inline data the parent
attaches (window.freeframeProfileL10n, window.freeframeProfilePages).
Troubleshooting#
- The pages look unstyled or new classes do nothing. The stylesheet only contains classes it
found when it was built. Run
pnpm buildafter editing patterns, then reload. - "This block contains unexpected or invalid content". A pattern or page was edited by hand with markup that doesn't match the block. Click Attempt recovery, or re-insert the section from Patterns > Profile.
- The header menu shows other pages, or an old menu. A Navigation block without a chosen menu shows the most recent one. Select it and choose "Main menu" (or yours) in the block settings, in both the desktop and the mobile Navigation block.
- A new project doesn't show up in a filter. Its
ffd-tags--...class must contain the filter'sdata-filtervalue exactly (same capitals, commas without spaces). - The command palette opens a missing page. The palette links to the pages with the demo's
slugs (
/resume/,/projects/...); if you renamed a page, change its slug back or editfreeframe_profile_page_urls()in a child theme. - Profile setup says the pages exist, but they are empty. It never overwrites existing pages. Delete (and empty the trash of) the pages with those slugs, or open each page and insert its page pattern from Patterns > Profile pages.
- The contact form doesn't send anything. It is a demo form; see Forms.
- The theme can't be installed on an older WordPress. Profile uses the Icon block's icon collections from WordPress 7.1; update WordPress first.
- A page shows the title twice or no title. Pages use a template without a title, because the page patterns start with their own heading. For a plain text page (privacy policy, terms), choose the Page with title template in the page's settings.
Differences from the HTML version#
The pages, copy, design and interactions are the same. What WordPress changes:
- Straight apostrophes typed in text blocks are shown curly ("Northwind’s"), as WordPress does everywhere.
- The footer's social links keep their names as hidden text for screen readers (the HTML version
uses
aria-label), and their icons are drawn with CSS masks. - Data and ARIA attributes on content blocks are written as classes and restored by the script (see Content); the writing entries' search text is read from their text.
- A few labels the HTML version puts on lists (
aria-label="Technologies") are not kept on List blocks.
License#
MIT, which is compatible with the GPL that WordPress requires (see LICENSE.txt in the theme
folder). Free for personal and commercial use. The portrait and other artwork are included under
the same license; the GitHub, LinkedIn and Mastodon marks belong to their owners. Inter and
JetBrains Mono are licensed under the SIL Open Font License 1.1 (assets/fonts/*/OFL.txt). Sam
Lindqvist is fictional.