Skip to content

Say what we do in words people use — and fix what was under the copy - #25

Open
drmowinckels wants to merge 57 commits into
mainfrom
service-copy
Open

drmowinckels wants to merge 57 commits into
mainfrom
service-copy

Conversation

@drmowinckels

@drmowinckels drmowinckels commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Why

People arrive unsure whether we can help them, and the copy was a large part of
why. The site described itself in terms a reader only understands if they
already know what we do — "data engineering", "FAIR principles",
"containerization", "integrated solutions", "registry data handling 55%".

That was the starting point. The branch grew from there into the structure
around the copy, the team pages, and a set of things the audit at the end
turned up underneath.

Copy and structure

Services are problem-led, and there are five of them. Each leads with the
problem the reader arrived with, lists the situations that produce it, and says
what they get. The category name stays as a small secondary label, so someone
who does know the term can still find it.

Before   Data Engineering
         Collecting and managing data and keeping track of its lineage
         is a critical aspect of scientific research...

After    You can't work with your data yet.
         - Sixty spreadsheets with inconsistent column names.
         - A Nettskjema already running, with no plan for getting the
           data out of it.
         → one documented dataset you can actually use, and the script
           that produces it, so it reruns when more data arrives.

Seven became five, in the order people meet them: data management and sharing →
data capture and engineering → processing, analysis and computing →
applications and databases → workshops and training.

"What we run and support" is a second layer, not the spine. Barbara's four
pillars are real, but they are specific tools rather than services, so they sit
in their own block — GOAT, Nettskjema, MRI, behavioural experiments — on both
the landing page and /services/. The repetition is deliberate: it is for
anyone arriving looking for one of those by name.

Two claims came out. We do not advertise MR scanning, and we do not
advertise Bevex, which is not in beta until spring 2027. The experiments card
says what is true today: JavaScript tasks that run anywhere, the tachisto
builder, and the platforms you can actually upload them to.

/services/ is two sections. It had grown to five — services, tools, "what
working with us is like", testimonials and a closing CTA. That was the
"scattered" note applied to the page rather than to the services block. 36.9K
to 17.8K.

Skills, pricing, hero, contact. The percentage bars are gone; "registry
data handling 55%" was not sourced from anything and not something a visitor
can act on. Pricing explains that the rate depends on how close you are to the
department and that computing time is applied for separately. Contact says
explicitly that you do not need to know the technical name for your problem.

The team

Bios written from the employee pages, with summary fields so cards show a
line rather than the whole bio, and task tags at one consistent level of
granularity. Person pages lay the photo beside the text instead of under it.
Silje Forsland added; José-Luis's 2024 portrait and his statistics focus;
Markus's MRI protocol work. Employee pages are linked through an icon drawn
from the colon in the UiO lockup. Photographers are credited where we know who
they are.

Publications moved from ORCID to OpenAlex, which returns the full list in one
request and does not need a personal address in the query.

Open source

The portfolio shows everything, not just the four entries that happened to have
a logo, with project marks back on the cards — grey until you point at them.
jsPsych Studio is tachisto throughout, with its new wordmark; ggseg is now
the ggsegverse mark and an entry that describes what it is today.

Accessibility

  • Links in prose are underlined by default. Teal on body text is 1.36:1,
    where 1.4.1 wants 3:1 for colour alone. The fix existed four times over and
    had already missed .person__bio, .contact-reassure and
    .help-card__short.
  • Six labels that only a screen reader hears were hardcoded English on the
    Norwegian site — "Toggle navigation", "Back to top", " on github.com".
  • Three copies of an aria-label built by splitting an icon class on hyphens,
    which announced "github" rather than naming the person.
  • The reveal animation can no longer hide the page. It set opacity: 0 and
    waited for a script that might not arrive; the flag that hides content is now
    set by that script itself, inlined in <head>, and removed again if anything
    throws.
  • Heading order, focus ring contrast, card link destinations, and a language
    switcher that is links rather than a navigating <select>.

Infrastructure

The canonical host was wrong. baseURL said capro.dev, CNAME said
www.capro.dev, and the apex redirected to www — so every canonical, og:url,
hreflang and sitemap <loc> named a URL that 301s. The apex is canonical now
and the CNAME matches. DNS has since moved to Cloudflare; both names are
proxied, the edge certificate covers them, and /.well-known/acme-challenge/
still reaches GitHub so its certificate can renew.

The deploy was 12M, of which 6.3M was images nothing linked to. Team,
testimonial and news bundles published their originals next to the derivatives
templates actually use. With the icon fonts also gone it is 4.8M.

Both icon fonts are gone. 29 glyphs were costing 737K of font files and 93K
of CSS, including two Font Awesome faces nothing referenced and three Themify
formats no browser asks for. Icons are inline SVG now, synced from the Font
Awesome package by npm run sync:icons. This also removes the
media="print" onload trick, which was a workaround for a stylesheet too heavy
to block on.

Tailwind was scanning assets/js/vendor, so minified Alpine and Leaflet
contributed utility candidates, and the bundle depended on build order — a full
npm run build and a bare npm run build:css produced different output from
the same sources. Render-blocking CSS is 59,831 → 41,088 bytes across the branch.

Also: robots.txt (a 404 in production), hreflang="x-default", Dependabot for
both npm projects and the actions, npm ci instead of npm install so CI
cannot resolve versions you do not have, and the deletion of themes/capro-hugo
— 172 files of jQuery, Bootstrap and Slick that nothing referenced.

Still to do before merge

  • Bios need sign-off from all five people.
  • Norwegian needs a pass. DeepL from the English, which is the source of
    truth. content/offerings/_index.no.md is two revisions behind its
    English twin — its experiments card has neither the platform list nor the
    tachisto mention. The portfolio category chips are also my Norwegian.
  • Photo credits. Only José-Luis's is known (Erik Engblad).
  • GOAT wording. "Built through UiO's HUB-node initiative" is the one
    phrase an outside reader will not parse. Keep only if it earns its place
    on credibility.
  • AAAA records were not needed in the end — Cloudflare's edge provides
    IPv6 — but worth confirming nothing else in the org hardcodes
    www.capro.dev, since those now take a redirect.

Notes for review

  • content/feature and content/cta are no longer rendered anywhere. Left in
    place rather than deleted, pending a decision.
  • --color-tertiary (gold) is still defined and still unused.
  • static/webfonts/leaflet ships five PNGs that Leaflet's stylesheet looks for
    at a different path. Nothing requests them — the map uses an inline-SVG
    marker and adds no layers control — so they are dead weight rather than a
    live 404.

Test plan

CI covers formatting, the content accessibility lint, a strict build, the CSS
bundle freshness check, the link check and the axe audit. Beyond that, verified
by hand in Chrome:

  • Icons. 206 visible inline icons across both languages at 1280px and
    390px, all sanely sized. Two bugs only visible there: an auto-width SVG
    stretches to its container under display: block, and again as a flex item.
  • Reveal. Five paths — normal, JS off, no IntersectionObserver,
    constructor throwing, observe() throwing. The last four leave all sections
    at opacity: 1; the first animates and reveals on scroll.
  • Links. 167 links in the opt-out contexts render undecorated; an anchor
    injected into each of the ten prose containers comes out underlined.
  • Pruned images. All 2300 local references in the built output resolve;
    none of the removed files was reachable from any page.

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

✅ Preview deployed at https://25--capro-uio.netlify.app
Please check it looks ok before merging.

@github-actions

github-actions Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

✅ Accessibility: no issues

axe-core over 19 routes × 2 viewports (one page per layout), plus 69 manual checks.

All 69 manual checks passed.

Automated tooling catches roughly a third of accessibility problems. A clean run is a floor, not a ceiling.

drmowinckels and others added 3 commits October 18, 2026 12:31
The old entries described a category. A researcher reading one could not
tell whether their problem was in it, whether we would do the work or
advise on it, or what they would end up holding.

Each of the seven now leads with the problem the reader arrived with,
lists the situations that produce it, and says what you get. The category
name stays as a secondary label, so someone who does know the term can
still find it.

Named systems do a lot of the work: someone who has never heard "data
engineering" knows what "I have a Nettskjema and can't get the data out"
means. Every name used already appears elsewhere on the site.

Also fixes a pre-existing inconsistency where the English list had
Workshops but no Data capture, and the Norwegian had Datafangst but no
separate processing entry. The two languages advertised different
services; both now carry the same seven.

Drops the duplicated skipToContent block in both i18n files.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
"Data flow and processing 90%, Registry data handling 55%" told a reader
nothing they could act on, and the numbers were not sourced from
anything. A visitor trying to work out whether we can help them cannot
use a percentage.

It is now three plain lists — the languages and tools we use, the UiO and
national systems we work inside, and the kinds of data we handle. Someone
with MRI data on TSD can now recognise themselves on the page, which is
the whole point.

Drops the now-unused progress bar CSS.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
People kept arriving unsure whether we could help them, and the copy was
a large part of why. The site described itself in terms a reader only
understands if they already know what we do: "data engineering", "data
science team", "FAIR principles", "containerization", "integrated
solutions", "standardized data output".

Hero and contact: the hero now names the concrete things we do — tidy up
messy data, set up collection, make analyses run twice, get jobs onto
university servers, teach groups to do it themselves — rather than
"capture, process, manage and analyse". Contact says you do not need to
know the technical name for your problem, because that is the actual
barrier.

Feature section: "Integrated Solutions / Standardized Data Output /
Customization on Demand" said nothing a reader could act on. It now
answers what working with us is like — we use systems UiO already pays
for, you own what we build, we can teach instead of doing, and you can
start with a single hour.

Pricing: explains that the rate depends on how close you are to the
department, and that computing time is applied for separately. The
bullets describe what you get rather than listing line items.

CTA, portfolio, team and testimonials follow the same rule: ordinary
words, concrete claims, and an explicit invitation to ask even if your
problem is not on the list.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@drmowinckels drmowinckels changed the title Rewrite the services in problem language Say what we do in words people already use Oct 19, 2026
drmowinckels and others added 20 commits October 19, 2026 12:12
Five lines of body copy over a photographic background is hard to read,
and the list of services below it already carries the detail.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
"What you get" was a hard-coded h4 under a problem heading that is an h2
on /services/, so every item skipped h3. axe flagged 28 instances across
the two language versions.

The label now tracks the item level the same way the problem heading
does, so the pair stays one step apart whichever level the section sits
at.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The copy claimed "anyone at UiO can ask", which told a public agency or a
company that the site was not for them — while the pricing table has had
non-commercial and commercial rates all along. The two contradicted each
other, and the copy was the wrong one.

UiO stays the primary audience, since that is where most of the work
comes from, but the hero, services intro, contact, CTA and pricing now
all say plainly that public agencies and private companies can hire us.

"We use what UiO already pays for" also assumed a UiO reader; it now
covers working inside whatever systems you already have.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The hero ended on who we do and don't work for, and the contact and CTA
both ended on who gets charged. Neither is what someone reads those for,
and the "free for some, billed for others" construction read as a
disclaimer rather than an invitation.

Getting in touch is free for everyone, so saying otherwise next to the
contact form was also just wrong. The rates live on the pricing page,
which is where someone goes looking for them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Seven identically shaped blocks down the left half of the page, with the
right 40% empty, gave a reader nothing to navigate by and no sense of
where one item ended and the next began.

Each item is now a two-column grid: the problem, its category and a
numeral on the left, the evidence and the payoff on the right. The
numeral is decorative and aria-hidden — the items are not a sequence and
nothing refers to them by number; it is there to give the eye something
to count. "What you get" picks up an accent rule, since that is what a
reader is scanning for. Collapses to one column below 900px.

Also fixes the h1 promotion under `hugo server`. section-title.html
marks the page's h1 as used via Scratch, which outlives a render, so
after the first rebuild the flag stayed set and no page emitted an h1
again. Production builds render each page once and were unaffected, so
this only ever showed up locally. baseof.html now clears it where each
render starts.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
"Real projects, and what we actually did on each one. If something here
looks like your situation, say so when you get in touch." read as client
case studies. Everything listed is open source or openly released
science that anyone can pick up without hiring us, which is a stronger
thing to say and happens to be true.

The nav label "Our work" carried the same wrong implication; it is now
"Open source".

ggseg becomes ggsegverse, covering the whole family of packages rather
than the one entry point. /portfolio/ggseg/ redirects to the new URL.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
--color-primary (#c04545) is a background fill. Used as a foreground it
lands at 2.68:1 on the grey sections — under the 3:1 WCAG asks for
meaningful non-text content, and nowhere near the 4.5:1 for text. The
palette already carries --color-primary-text (#e57575, 4.58:1) for
exactly this.

axe does not catch it: it has no text node to measure on an <i> icon,
which is how all five instances were styled. Found by walking every
rendered element and comparing its computed colour against its resolved
background.

Feature and contact icons, the testimonial quote mark and two team
links move across. The quote mark also goes from opacity-40 to 60,
since 40% of an already-dim red was close to invisible.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
og:image was the site logo resized to 512px tall, paired with
twitter:card=summary_large_image. Platforms crop that to roughly 1.91:1
and render it large, so a transparent mark arrived floating on whatever
background the platform chose, at the wrong aspect ratio.

Every page now composes its own 1200x630 card on the brand's dark
ground: the page's own image when it has one — a team portrait, a
package logo, a post's figure — and the CAPRO mark when it does not.
Remote images are skipped deliberately, so a build never depends on
someone else's host being reachable.

Also adds what was missing around it: og:site_name, image width/height
and alt, og:locale:alternate for the other language, article published
and modified times, and explicit twitter:title/description rather than
leaving them to fall back.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Every card's call to action reads "Read more", so tabbing through the
news or portfolio list gives no way to tell them apart. The post title
now follows it in a screen-reader-only span.

The card image was built at 300px and stretched to roughly 400 by
w-full, so it was upscaled on every screen and soft on high-density
ones. It now ships 1x and 2x sources with sizes, plus width and height
so the card stops reflowing when the image lands.

Lighthouse on /news/: SEO 92 to 100.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
axe-core passes clean on all 58 pages, but it reports colour-contrast as
"incomplete" wherever text sits over a photograph, and it cannot see focus
indicators at all. Measuring those by hand found four AA failures:

1.4.11 Non-text contrast. The focus ring was the fill red, which is 1.00:1
against the skip link, 1.06:1 against .btn-primary and 2.68:1 against the
page. On the two most important targets on the site it was invisible. White
with a dark halo clears 3:1 on every surface the ring lands on, including
Leaflet's white map controls. `.btn` also used `transition: all`, which
animated the ring in over 200ms, so the first frame of focus was the
browser default; the transition now names its properties.

1.4.3 Contrast. The CTA band composites its photo at 70% rather than 60%,
matching what banner.html already did. The paragraph measured 3.85:1 with
42% of its area below AA; it now measures 5.67:1 with none.

3.2.2 On Input. The language `<select onchange="location=this.value">`
fired on every arrow key, so a keyboard user could never reach the second
language. It is now two links, which also drops an inline-JS dependency and
carries hreflang. Ordered by site.Languages, since AllTranslations is not
sorted and the pair swapped between pages.

2.5.8 Target size. Nav links, the nav group button and the team social
links measured 20-28px tall; all now have a 24px floor.

The byte work is the same kind of gap. banner.html resized its background
to 1600px webp; feature.html and cta.html emitted .RelPermalink, so
/services/ transferred the 3500px originals — 8.2 MB. Leaflet was gated on
site.Params.map, a site-wide param, so 144 KiB of JS shipped to all 58
pages for a map on one. The hero preload fired on every page although
baseof.html renders the banner only on the home page. The logo was a
1624x599 PNG served at 130x48 with width="auto", which is not a valid HTML
width. FontAwesome imported a 'regular' weight no glyph on the site uses.

Favicon declarations were a PNG labelled image/x-icon under the legacy
relation, leaving /favicon.ico to 404 — the only thing costing
best-practices points.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
With the oversized photographs gone, the header logo is what Lighthouse now
picks as the largest contentful paint on most interior pages, and it flagged
the missing priority hint. LCP there is bound by render-blocking CSS rather
than bytes, so this is a small effect; the local score is bimodal between
2.6s and 4.6s either way.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three placeholders that had been rendering literally.

Every team page announced "About Member" in 36px, and its two section
headings read "Posts by Member" / "Publications by Member". All three came
from `index (split .Params.single " ") 1` against the team section's
`single: About Member`. The person's name was meanwhile buried at text-xl
inside the figcaption. The name is now the page's h1, styled like every
other page's section title, with `text-transform: none` so that CSS
`capitalize` does not rewrite "José-Luis Alatorre-Warren" at the hyphen.
The portrait's alt is now empty because the name sits immediately above it;
repeating it made a screen reader say the name twice.

The footer said "Copyright © 2022" on every page of a site whose newest
post is from 2025. The year now comes from site.Lastmod rather than from a
translated string, so it tracks content instead of drifting. CI checks out
with fetch-depth 0, so enableGitInfo gives a real date there.

The home help grid drew its hairlines by painting the container and letting
the cards cover it, so seven items in three columns left cells 8 and 9
filled — a slab that read as a missing card. The hairlines move onto the
cards; empty cells now fall back to the page.

The same grid also broke the hero's promise. It headed each card with
`name` ("Large-scale computing") immediately after a hero saying you do not
need to know the technical name, while `problem` ("The analysis is too big
for your laptop.") sat unused in the same data file — the line /services/
already uses as its headline. Cards now lead with `problem` and demote
`name` to a subtitle. Not another tracked-uppercase label: the theme has
six of those already.

While in team/single.html: the social links derived their aria-label from
the last hyphen-segment of the icon class, so "ti-twitter-alt" announced as
"alt". They now name the person and the host.

axe-core still passes 0 violations across 120 runs, and every page still
has exactly one h1.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
**Contact.** The step the site exists to produce read value-then-label, sat
on the same bg-dark as the team section above it, and said nothing about
what you get for writing. The note moves above the address, the section
moves to bg-grey so it has its own ground, and three reassurances sit under
it. Each is sourced from copy already on the site — the first conversation
is priced at 0 for UiO in content/pricing, and "someone who does the work,
not an account manager" is the team page's own line. No reply-time
commitment: that would be inventing a promise.

**Stable ordering.** people.html, team.html and testimonial.html all ranged
`| shuffle`, so the home strip never matched /team/ and the order changed
on every deploy. Now weight, then title.

**The heading split.** section-title.html cut every title after word one,
which broke the compound in "Open | Source we have built" and left a lone
grey "R" on a tag page with an empty accent span beside it. The split point
is now a caller option (portfolio passes 2), and a single-word title is
accented in full.

**Term pages.** Headed by the bare term, no count, no way out. Now "Tagged
R" / "Filed under MRI" with a post count and a link to all news.

**Portfolio.** The filters had no group label, no pressed state and no
announcement, so pressing one silently removed cards; they now carry
role=group, aria-pressed and a live count, and their styling keys off
aria-pressed rather than a parallel Alpine :class that could drift. The
hover overlay held a second link to the same URL that was invisible on
touch and focusable while transparent — it is decorative now, the title
carries the link, and the overlay also reveals on :focus-within.
Descriptions were `truncate 120`, cutting mid-clause; they take the first
sentence instead.

**Testimonials.** The nav buttons sat at left/right: -20px, on top of the
first and last card's text below ~1480px. They move above the track, they
disable at the ends instead of scrolling into nothing, and the step is
measured from a real card rather than a hardcoded 392px.

**Hierarchy.** .section-title h1 and h2 were the same size, so page title
and section title were indistinguishable. Prose h2/h3/h4 now differ in size
and colour and carry top margin; they were all teal and glued to the
paragraph above.

**The chart.** content/skill/growth.png was six curves over dashed
gridlines with no axes, labels or units, beside the list of tools we
actually use, on a site whose product is trustworthy data. Removed; the
list takes the full width in three columns.

**Side-stripe.** .svc__deliver was a 2px red left border, the one genuine
instance of the pattern in the theme. It is a bordered, tinted callout now.
(.icon-box keeps its 2px sides — those are the hexagon's edges, not an
accent stripe.)

**English on a bilingual site.** 404.html and the whole ORCID publications
UI were hardcoded English. Both translated; 404 also stops being a dead end
and leads with the message rather than the status code.

**Card images.** `img.w-full` stretched a 262x302 source across a 400px
card. Capped at the intrinsic width.

axe-core: 0 violations across 120 runs. Every page has exactly one h1.
Desktop Lighthouse 99-100 across performance, a11y, best practices and SEO.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The services answer "can you help with my problem". They do not answer "do
you do the specific thing I came for", and that is how most would-be clients
arrive — asking about MR-based research, or about Nettskjema, by name.

CAPRO_services.docx frames GOAT, MRI, statistical analysis and Bevex as four
pillars. They are not a service taxonomy: two are named products, one is a
research domain, and one is already the "Processing and analysis" service
under a different name. Making them the spine would leave work that fits none
of the four homeless.

So they go in as a second layer instead. The seven problems stay the spine;
a short block under them names the things a visitor already has a word for,
on both the landing page and /services/. Nettskjema is in that block although
the document does not mention it, because it is the one name clients were
reported to ask about most.

Two of the document's claims are not ours to make yet, and are not made here:

- We do not offer MR scanning. The document describes collection through
  LCBC's MRI-trained staff at Oslo University Hospital; that is their
  capacity, not a CAPRO service. The card starts from data that already
  exists, whoever collected it.
- Bevex is not advertised. It is in testing, beta is expected in spring 2027,
  and a site should not sell what it cannot deliver. The card describes what
  we can do today — build browser-based experiments in JavaScript — and
  mentions the platform as in development, with no date.

Also fixes a bug in this branch's own CSS: markdownify returns text without
its wrapping <p>, so `.svc__deliver p` never matched and "What you get"
rendered unstyled. The paragraph is now written in the template.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The page said "Open source we have built" and listed four things. The
organisation has twelve worth showing: uiothemes, tsdrtools, neuromat,
ebrains.kg and nva in R, jsPsych Studio and the Nettskjema data inspector on
the web, and the R project-management workshop materials. Someone judging
whether we work in the open was judging us on a third of the evidence.

The reason the rest were missing is that the card template required a local
image resource, and only one of the new entries ships a logo. Rather than hunt
for or invent logos, the image panel goes: each card is now the categories,
the linked name and a sentence. The filter chips above already carried the
same categories, so they are not new information — they are the anchor the
card lost with the logo. Two new categories, Web apps and Teaching, since
R-packages and Analyses did not cover what was being added.

Four logo files are deleted with the panel. Nothing referenced them any more,
and this branch has already been removing bytes that ship without being used.

Also fixes the blurb fallback: .Plain returns the rendered HTML's entities,
so "Norway's" reached the card as the literal "&rsquo;" once an entry had an
apostrophe in it. None of the original four did.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`--panicOnWarning` turns the Hugo 0.156 deprecation of `.Site.Languages`
into a hard error, so the strict build in CI fails before it renders the
taxonomy pages. Both language switchers used it, for one property: the
ordering by configured weight, which `AllTranslations` does not give.

`hugo.Sites` is ordered the same way, so the walk is unchanged apart from
reaching the code through `.Language.Lang`. Verified the switcher still
renders English before Norsk on both language trees, with `aria-current`
on the right entry.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two sections sat on pages whose subject they did not match.

The testimonials were the closing section of /portfolio/, under the heading
"Open source we have built". The quotes are about working with us on projects
— a database built over fifteen years of collection, workshops, a data
structure someone can finally use — not about released code. They move to
/services/, straight after "What working with us is like", where they are the
evidence for the claim immediately above them.

The CTA asks "Not sure your problem is on the list?" and invites you to
describe the problem in ordinary words. On /services/ that list is right
there. On /pricing/ there is no list, and the page already closes on its own
ask — "You can always ask what something would cost before committing to
anything" — so the block was both a non sequitur and a second ask. It goes.

/portfolio/ now ends on the projects, which is what a reader came to that page
for.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Same mismatch as /pricing/: "Not sure your problem is on the list?" after a
page about who works here, where there is no list. The skills section already
closes on its own invitation — "If something you use is not on this list, ask
anyway" — so the page keeps an ask, and a better-aimed one.

The CTA now appears only on /services/, which is the page its copy was
written for.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The page was flat for structural reasons, not stylistic ones. Seven items
rendered with identical geometry, one hairline between each, nothing larger
than 1.9rem and nothing smaller than 0.72rem, and in seven screens of scroll
the only colour was a red icon and a 7%-red box.

Three things were scaffolding rather than information:

- The numbers. The old comment said it outright — "the number is rhythm, not
  information, the items are not a sequence and nothing refers to them by
  number". Rhythm is what this commit gets from the layout instead.
- The "What you get" label. Seven of them, and the box under each made the
  heaviest element on the page also the most repetitive, so the emphasis
  stopped registering as emphasis.
- The tracked-uppercase category. Two eyebrows per item, fourteen down the
  page. The category stays, in sentence case, for the reader who knows the
  term; it no longer competes with the problem above it.

What replaces them:

- The problem statement is the one piece of scale, up to 3.25rem, with the
  measure capped at 19ch so it breaks into two or three lines and reads as a
  statement rather than a table row.
- Items alternate which edge they sit on above 900px, and the hairline under
  each is only as wide as the item, so the stagger is visible before any of
  the text is read.
- The payoff is the largest body text in the item and the only teal on the
  page, marked with an arrow rather than a label and a filled box.

Gold (`--color-tertiary`) stays unused. It is defined in the tokens and
appears nowhere on the site; introducing it on one page would orphan it.

Checked at 1280 and 390, both languages. Contrast holds: the symptom text is
4.9:1 on the section background, the teal payoff and the category well above.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two pairs were describing one problem each.

"Data capture" and "Data engineering" both answer "I cannot work with my own
data". The split was ours — before collection versus after — and the reader
arriving with a Nettskjema they cannot get the data out of does not know
which side of it they are on. One item now covers both, and the payoff says
both halves: data put where it is allowed to be, and one documented dataset
out the other end.

"Processing and analysis" and "Large-scale computing" both answer "the
analysis does not work". One is "it gives a different answer each time", the
other "it never finishes", and the same packaging and version control fixes
the first while the job scripts fix the second. One item, four symptoms
covering both failures.

The order was arbitrary. It now follows when the reader actually hits each
problem: the data management plan first, because that is grant-application
stage and before any data exists; then collecting it, then getting the
analysis to run, then seeing the state of the project, then learning to do it
without us.

Norwegian carries the same five in the same order, so the two languages do
not advertise different services again.

The landing page renders these as cards, and five in a three-column grid left
a hole in the second row. At exactly five the grid runs six columns and the
last two take half the final row each; a sixth service restores the plain
three-up.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
drmowinckels and others added 29 commits October 20, 2026 12:07
His bio had him on fMRI analysis and HPC, which is the back half of the job.
He also sets up the MRI side of a study: working with the physicists at the
hospital to keep protocols valid and current, and keeping collection running
once it starts. "MRI protocols" joins his roles.

This is his expertise, not a scanning service — the services page still
starts from scans that already exist, whoever collected them. Protocol
development was already listed there, and this is the person it rests on.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Read all four UiO profiles and rewrote from them, so each bio now names real
work rather than gesturing at competence:

- Mo ran data processing and management for the EU Lifebrain consortium,
  leads CAPRO, wrote ggseg, ggseg3d and nettskjemar, and teaches research
  methods at the department.
- Inge has worked on Lifebrain, UK Biobank, the Human Connectome Project and
  ABCD — which says more about what he can handle than any adjective.
- Markus studies visual memory with fMRI and psychophysics, which is the
  research behind the MRI setup work.

José-Luis's own text is unchanged: his UiO page is an unfilled template,
still carrying literal "<Task 1>" placeholders, so there was nothing to draw
on. Worth him filling in, and then this can be revisited.

Each bio keeps the closing line naming what that person can be asked for, and
each still needs its subject's sign-off.

Also drops the comment above uio_profile. The field name says what it is.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Replaces the old landscape photo with the 2024 staff portrait from his UiO
page. The original is a 4432x6647 PNG at 28 MB; this is resampled to 1400px
on the long edge and saved as JPEG at quality 85, which lands at 728K — in
line with the other portraits in the repo and far more than the 600px square
the templates crop to.

Photo by Erik Engblad.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Silje is an adviser at LCBC. Her bio is from her own description, kept close
to her words — data collection planned end to end, a tidy and documented
process, and the participant having a good time being tested — with the title
and background from her UiO page. She needs to approve it like the others.

Photo credits are now a field. Photographers have a right to be named and a
staff portrait is someone's work, so `photo_credit` renders under the
portrait on the person's page wherever one is recorded. Only José-Luis's is
filled in, from the filename of the UiO original: Erik Engblad. The other
four need whoever took them; the field simply renders nothing until then.

Both people grids wrap and centre instead of running a fixed four columns.
At five people a four-up grid left a hole exactly the width of a card, and
the next person to join would have moved the problem rather than fixed it.

The landing page also got the no-upscale guard the team page has. It filled
to 500x500 unconditionally, and Silje's portrait is 133px on its short edge.

Her photo is the only one UiO publishes and it is small: 133x200, rendering
at roughly 2.4x upscale in the grid. It wants replacing with a proper
portrait.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The cards printed the whole bio. Each person now has a `summary` in front
matter, which .Summary picks up without a template change — one line saying
what they do, so the cards are scannable and the same height. The bio stays
in full on the person's own page, where someone has asked for it.

The tags were three different kinds of thing at once: "R" (a tool) beside
"Workflow" (a way of working) beside "Developmental and evolutionary
neuroscience" (a research field, and three lines in a card). They are now one
register — a capability someone could ask for, one to three words, no "and"
chains, at most one tool name each — and where a tag matches a service it
uses the service's own words, so "Large-scale computing" reads the same on
both pages.

Silje is a research adviser, which is the accurate title, and her photo is
the press portrait rather than the 133px thumbnail: 1063x1600, no resizing
needed.

Hugo's Smart crop anchored low on it and cut the top of her head off, so
`photo_anchor` is now an optional override, defaulting to Smart, applied in
all three places a portrait is cropped. Hers is set to Top.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The page was a narrow centred column. That put the bio in centred serif,
which is fine for a line and poor for a paragraph — five lines with a ragged
left edge — and left two thirds of a wide screen empty.

Photo on the left, everything else on the right and left-aligned. It stacks
on narrow screens.

The portrait is now square and grey, the same treatment as the card that
links here. It was round and in colour, so clicking a card landed you on
something that did not look like the thing you clicked.

`role` is a new field, because the page never actually said what the person
is. It was buried mid-sentence in the prose. No unit on it — everyone here is
at the same one.

Also gone: the two hairlines that bracketed the single bio paragraph, and the
dead end at the bottom. There is a link back to the team now.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
His page was an unfilled template last time, still carrying "<Task 1>"
placeholders, so his bio was the only one not written from source. It now
has his background and his work, and the bio follows:

- A PhD in evolutionary biology on top of applied physics and mechatronics,
  rather than the vaguer "BSc in engineering".
- Diffusion MRI across humans and animal samples, which is the specialism the
  page leads on.
- Early brain development, with collaborators at Harvard Medical School and
  Boston Children's Hospital.

The card summary moves with it, from the generic "the analysis that makes
sense of the scans" to what he actually works on.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Statistics was the last word in his tag list and a conditional at the end of
the bio, which buried the thing he is best at. It now leads in all three
places the page uses: first tag, the card summary, and the opening line.

The MRI stays, demoted to what the modelling sits on top of rather than the
subject in its own right.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ress

The data has come from OpenAlex for a while, but everything around it still
said ORCID: the file was orcid-pubs.js, every element id was orcid-pubs-*,
and the only thing ORCID still provides is the id used as the lookup key.
Anyone debugging this would have gone to the wrong API first. Renamed to
publications.js, with a comment saying where the data comes from and why —
OpenAlex indexes the whole career, including work published before someone
came to UiO, which the institutional list does not.

The OpenAlex polite-pool mailto was a personal work address, hardcoded and
shipped in the bundle on every page load. That sat badly beside the decision
to keep emails off the team pages: they were out of the HTML and one was in
the JavaScript anyway. It is the shared inbox now.

Renaming the select's id left the label's `for` pointing at the old one,
which would have broken the association. Caught before pushing.

Verified against the live API: 87 publications, sixteen years in the filter,
no error state.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Removed along with its styles. The nav and the footer both reach the team,
and the browser's back button is what people actually use.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
tachisto has a brand package now. Its LOGOS.md says the wordmark is the
primary mark for light grounds, which is what the card panel is, and that the
inks are fixed so the file has to match the ground rather than be recoloured.

It is an SVG, and Hugo cannot run Fit, Resize or Fill on one. Four templates
called those methods unguarded, so adding the file broke the build four times
over, each in a different place: the portfolio card, the article card, the
social-card partial and the default single page. Each now checks
reflect.IsImageResourceProcessable first.

The three that display the image pass the vector straight through — there is
no 2x of an SVG and no derivative worth making. The social card cannot: Open
Graph needs a raster at an exact size, so a vector subject falls back to the
site logo instead of failing the build.

This is the fix for any future SVG logo, not just this one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The card carried the ggseg hex, which is one package inside the family the
entry describes. ggsegverse has its own wordmark; this is the light-ground
file, checked by eye rather than by filename, since "light" names the ink in
some brand packages and the ground in others. Here the ink is dark, which is
what the white panel needs.

Also commits the Tailwind bundle. The previous commit added `max-w-[400px]
w-full` to the single template, which generates a new utility, and I staged
the templates without it — so CI's stale-bundle check failed, exactly as it
is meant to.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Norwegian portfolio page listed both "R-packages" and "R-pakke" as
separate filters, because the nine entries added earlier carried the English
category names into the Norwegian files while the three older ones used
Norwegian. Picking either chip hid the entries filed under the other — eight
of them under "R-packages".

The chips are user-facing text, so they are Norwegian on the Norwegian page:
R-pakker, Analyser, MR, Undervisning, Webapper. Five in each language now,
matching one to one. The wording wants a look in the translation pass; the
split was the bug.

Also tidies the "key : value" spacing the older entries used.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The entry was five years old and generic: "a family of R packages for
plotting brain atlas data", plus a line about being open source that the
page intro above it already says about everything.

It now names the pieces — ggseg for 2D with ggplot2, ggseg3d for 3D with
Three.js, and the atlas packages — and mentions the playground, which runs a
real R session in the browser and is the fastest way for a sceptic to see
whether it does what they need.

The link moves from the R-universe listing to ggsegverse.github.io. One is a
package index, the other explains the thing and lets you run it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
It was a named text link sitting apart from the social icons. As an icon it
joins them, and the mark is the colon from the UiO lockup — the same one the
CAPRO logo carries in "UiO: Life science". No icon font has it, so it is two
circles drawn inline, inheriting colour from the link like its neighbours do.

The label survives the change: it was visible text and is now the aria-label,
so a screen reader still hears "Employee page at the University of Oslo
(opens in a new tab)" rather than a shape.

The row also renders now when a person has a UiO page and no social accounts,
which was Silje's case — the wrapper was inside `with .Params.Social`, so her
link had nowhere to live once it moved.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The CNAME is www.capro.dev; baseURL said capro.dev, which 301s to it. So
every canonical, og:url, hreflang and sitemap loc named a URL that
redirects — search engines were told the canonical version of each page
was somewhere else, and unfurlers followed a hop per card.

Internal assets were paying for it on every page view: the logo, the
hero background and the hero preload all used .Permalink, so they were
fetched from the redirecting host. Those are relative now, along with
the article and language-switcher links. meta.html keeps .Permalink —
canonical, hreflang and og:image have to be absolute.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Team, testimonial and news bundles were copying their originals into
/public alongside the processed derivatives the templates actually use —
18 files, 6.3M, zero references. A 1049K portrait PNG and a 793K one
were the largest things on the site.

Portfolio already had this cascade. Derivatives are published whatever
this flag says, so nothing on the site changes; the deploy goes from
12M to 5.7M. Checked all 2564 local references in the built output
afterwards: none of the pruned files was reachable from any page.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
www.capro.dev/robots.txt was a 404, so nothing pointed crawlers at the
sitemap. The hreflang set listed en and no but no x-default, which is
what Google falls back to when a visitor matches neither.

x-default resolves through hugo.Sites, ordered by the configured
language weight, so it tracks defaultContentLanguage rather than
hardcoding "en".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Automatic source detection was walking assets/js/vendor, so minified
Alpine and Leaflet contributed utility candidates: blur, collapse,
container, inline, resize, ring, shadow, start, sticky, transform,
transition and visible were all generated from strings in third-party
JS, and none is used in any template.

It also made the bundle depend on build order. `npm run build` cleans
assets/js/vendor before compiling CSS and restores it afterwards, so a
full build emitted 42828 bytes while a bare `npm run build:css` emitted
46022 from the same sources. Anyone running the documented build got a
dirty tree and a CI staleness failure they had not caused — which is how
this was found.

source(none) leaves only the explicit @source, which is where every
class on this site lives. Both commands now produce the same 42513
bytes, 3.4K off the render-blocking bundle.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both lockfiles are committed but all three workflows ran `npm install`,
so CI could resolve versions you do not have locally. That matters here
because the committed tailwind.css is diffed against a CI rebuild: a
Tailwind patch release would fail that check with no source change.
`npm ci` installs the lockfile exactly.

Also: cache ~/.npm, drop `submodules: true` (there is no .gitmodules),
and give lint the full history it needs for enableGitInfo, which the
other two workflows already had.

Nothing was watching the dependencies. Dependabot now covers both npm
projects and the actions, monthly, grouped so it opens three PRs rather
than twenty.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Five aria-labels and one label fragment were hardcoded English, so the
Norwegian site announced "Toggle navigation", "Main navigation",
"Mobile navigation", "Footer navigation", "Back to top", and
"<name> on github.com".

profileOn takes name and host as a dict so the preposition is part of
the translation rather than concatenated around it.

The Norwegian wordings are mine and want your eye.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
site.Params.social is not defined anywhere in config/, so the row
rendered as an empty flex container with a 32px bottom margin on every
page. Its aria-label also still used the split-on-hyphen trick that
team/single.html documents as a bug — fixed there, left here.

The .footer-social-icon rules go with it.

If you want CAPRO's GitHub org in the footer, this comes back as four
lines of params.yaml plus the row; say so and I will add it with the
host-based label the team pages use.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
.reveal set opacity: 0 and waited for scroll-reveal.js to add .visible.
The noscript and prefers-reduced-motion overrides covered the cases they
were written for, but not the one that matters: JS enabled and the
script not running. A blocked request, an SRI mismatch or a throw left
every section at opacity 0 with no error anywhere.

Hiding is now gated on a .js class that the reveal script itself sets,
only once its observer exists, and removes again if anything throws. The
script is inlined in <head> rather than fetched, so there is no separate
request that can fail after the page has committed to hiding things —
and no flash, which an arm-on-DOMContentLoaded approach would have had.

Verified in Chrome over five paths: normal, JS off, no
IntersectionObserver, constructor throwing, observe() throwing. The last
four leave all 14 sections at opacity 1; the first animates as before
and reveals everything on scroll. The noscript override is gone, being
unreachable now.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Reverses the direction of the earlier fix: the two sides disagreed, and
the apex is the one we want. static/CNAME now says capro.dev, so GitHub
Pages serves the apex and redirects www to it, and baseURL goes back to
https://capro.dev/. All 1419 absolute URLs in the build are apex, none
www.

DNS already supports this: the apex carries GitHub's four A records, www
is a CNAME to capro-uio.github.io, and the live certificate already
covers both names, so nothing needs reissuing.

Ten portfolio links pointed at www.capro.dev. Those paths are other
repos' project pages, served under this site's custom domain, so they
move with the CNAME — they are apex now and will resolve without a hop
once this deploys.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CSS nothing emits: .service-block and its descendants (the services
design that .svc replaced), .person-uio (replaced by the icon in
.person__links), .section-title__name.

Config nothing reads: post_share, preloader, cookies, and a plugins.css
block holding a cdnjs Font Awesome <link> — the only reason the site was
not loading Font Awesome twice, once from a third party, is that no
template ever looks at site.Params.plugins. map keeps its coordinates,
which contact.html does use, and loses gmap_api and map_marker: the map
is Leaflet over OpenStreetMap with an inline-SVG marker. HUGO_GMAP_API
goes out of all three workflows with it.

themes/capro-hugo: 172 files, 4.3M of jQuery, Bootstrap, Slick, Shuffle
and Magnific Popup. theme is capro-tw and nothing referenced it. The
attribution in capro-tw's README and theme.toml stays.

Built output is the same file list; the CSS bundle drops 838 bytes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Teal on body text is 1.36:1, where WCAG 1.4.1 wants 3:1 for a link
marked by colour alone. The fix existed four times over — .prose,
.section-intro, the three .svc bodies, .offer__body, and a .person-uio
rule that had already gone stale — each with its own copy of the same
comment, and each needing to be remembered for every new prose
container.

One had already been missed: .person__bio holds each team member's
rendered content and was in none of the lists, so the first link in a
bio would have failed silently. So were .contact-reassure and
.help-card__short.

Underline is the base rule now, with an opt-out list for links whose
surroundings already make them unmistakable: buttons, the skip link,
nav, the language switcher, footer links, card headings and the contact
address, which carries its own border-bottom. The ORCID fallback loses
its hand-applied underline utilities too.

Checked in Chrome across seven pages: all 167 links in the opt-out
contexts render undecorated, testimonial sources and the Leaflet
attribution stay underlined, and an anchor injected into each of the
ten prose containers comes out underlined.

Also the third copy of the split-on-hyphen aria-label — the team grid's
social row, which announced "github" rather than naming the person. It
now uses profileOn like the other two.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Twenty-nine glyphs were costing 737K of font files and 93K of CSS.
Font Awesome shipped fa-regular and fa-v4compatibility, which nothing
referenced at all; Themify shipped .eot, .ttf and .svg — 382K of formats
no supported browser asks for — and no .woff2, so browsers took the
55K .woff.

Both are gone. assets/icons holds the 29 SVGs the site actually uses,
synced from the Font Awesome package by npm run sync:icons, which is
also where the list of used icons lives. A helper partial inlines one
and errors out on an unknown name, where a mistyped icon-font class
used to render nothing.

Themify had no SVG source and only nine icons in use, so those are
Font Awesome equivalents now and the whole site draws from one set:
user, pen-ruler, infinity, rocket, box, chalkboard, square-check,
location-dot, arrow-up-right-from-square.

Content keeps an icon: field but names the icon plainly — "database",
not "fa-solid fa-database" — so nothing in content refers to a font
that is no longer there.

This also removes the media="print" onload trick in head/css.html.
That was a workaround for a stylesheet too heavy to block on; with no
icon stylesheet there is nothing to defer, and icons paint with the
page rather than after it.

Render-blocking CSS: 54888 to 41162 bytes. Static assets: 815K lighter.
Font Awesome stays as a devDependency for the SVG source and its
licence; attribution is in assets/icons/README.md, since the per-file
licence comment would otherwise be inlined into every page.

Measured in Chrome at 1280px and 390px, both languages: 206 visible
icons, every one sanely sized. Two things that only showed up there —
an auto-width SVG stretches to its container under display:block, and
again as a flex item in the help cards, so those are pinned.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The tag "hub/node" made Hugo build /tags/hub/node/, so it read as two
tags and left /tags/hub/ as a bare directory. It is hub-node now, which
also matches how the offerings copy writes HUB-node.

Testimonial portraits carried the person's name as alt text, which the
line immediately beside them already gives, so a screen reader heard it
twice. The portrait is decorative there.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
/services/ stacked five sections: the services, the named tools, "what
working with us is like", the testimonial carousel and a closing CTA.
That is the "too much information and a bit scattered" note applied to
the page rather than to the services block, which was the part already
fixed. The page is the services and the tools now — 36.9K to 17.8K.

The carousel was the only thing rendering content/testimonial, so
cutting it would have dropped nine named testimonials off the site
entirely. They move to the landing page instead, between the team and
the contact section, which is where social proof does the most work.
Verified: nine render on both language home pages, portraits included.

"What we run and support" stays on both / and /services/ by choice: the
repetition is the point for anyone arriving looking for GOAT or
Nettskjema by name.

Nothing renders sections/feature or sections/cta any more, so
content/feature and content/cta are unreferenced. Left in place rather
than deleted.

Also: lb_wb2's English file carried the Norwegian job title.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@drmowinckels drmowinckels changed the title Say what we do in words people already use Say what we do in words people use — and fix what was under the copy Oct 20, 2026

This branch has not been deployed

No deployments
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