YF Agency · internal reference · published to staging only
Every specimen below uses the real site classes, not copies. What renders here is the live system. If a specimen looks wrong, the system is wrong — fix the class, not this page.
The Home and Approach pages are the reference. A new page is assembled from what they established, in this order:
1 · Page shell. Nav component at the top. If the page opens on a hero over media, set the body and hero attributes from §6 — all of them, or the nav will be unreadable over it.
2 · Sections. Stack .yfapp-sec shells (§5), one ground combo each, alternating light and dark as the design asks. Every section gets data-yf="‹name›".
3 · Heads. .yfapp-head with a .yfeyebrow-row (§4) and a type utility from §5. Copy the eyebrow specimen rather than rebuilding it.
4 · Content. Reach for a layout primitive in §5 before inventing one — column row, split-with-rule, or a linked CMS list. If the content is a card, chip, form, counter, disclosure row or image frame, the component catalogue (§9–§18) already specifies it.
5 · Buttons. Copy a specimen from §3. On a new page that means .yfapp-btn.
6 · Tier block. Add the 992–1919 token block from §6 to the page's embed, or the page jumps straight from desktop to tablet on a laptop.
7 · Focus states. Anything clickable or focusable needs its own :focus-visible rule. There is no site-wide default to inherit — see §11.
8 · Check the traps. §8 before debugging anything visual. Most of what looks like a mystery is on that list.
Do — copy an existing specimen before building a new one. It is the fastest way to stay on-system.
Do — bind every size, colour, space and font to a variable. The token tables below are the source of truth; they step automatically per breakpoint, so one binding covers all three.
Do — reuse an existing class before inventing one. Search the class list for the pattern name first, and check §9–§18 for whether the component already exists under another prefix.
Do — let colour inherit. Set it once on the section's ground combo and never again below it.
Don't — type a raw px or hex value into the Designer. If a value isn't in a table below, it either belongs in a variable or it's an exception that needs writing down in §8.
Don't — add a per-section duplicate of an existing pattern (a seventh eyebrow class, an eighth button, a fifth counter). Extend the shared one with a combo class.
System — the rules that apply everywhere.
Component catalogue — what exists, which generation is current, and what is frozen.
01
One collection, Base, with three live modes bound to breakpoints. Bind once at main and the value steps on its own — you should almost never need a per-breakpoint override.
| Mode | Breakpoint | Applies to |
|---|---|---|
| Base mode | main | ≥ 992px — desktop |
| Tablet | medium | ≤ 991px |
| Mobile | small | ≤ 767px (and ≤479 by inheritance) |
| zz-probe-nobp | — | Dead. Left by a failed probe; delete by hand in the Designer. |
Identical in all three modes — colour does not step.
| Token | CSS variable | Value | |
|---|---|---|---|
| Brand/Dark | --brand--dark | #272727 | |
| Brand/Light | --brand--light | #fffffb | |
| Brand/Y | --brand--y | #ffee2d | |
| Brand/B | --brand--b | #0036c3 | |
| Backgrounds/BG | --backgrounds--bg | #f6f6f6 | |
| Backgrounds/Pure white | --backgrounds--pure-white | #ffffff | |
| Backgrounds/BG Alt | --backgrounds--bg-alt | #272727 | |
| Backgrounds/BG Secondary | --backgrounds--bg-secondary | #eeeeee | |
| Lines/Separator | --lines--separator | #d9d9d5 | |
| Lines/Separator Alt | --lines--separator-alt | #fffffb59 | |
| Lines/Border | --lines--border | #27272733 | |
| Text/Text Light | --text--text-light | #27272780 | |
| Text/Text Light Alt | --text--text-light-alt | #fffffba8 | |
| Text/Text Super Light | --text--text-super-light | #2727271a |
Per-solution identity colours. Used for the solution stack and CMS-driven accents — never for UI chrome.
| Token | CSS variable | Value | |
|---|---|---|---|
| Brand | --solutions--brand | #ffc62a | |
| Crossborder | --solutions--crossborder | #ef81e8 | |
| Content | --solutions--content | #ff5a4d | |
| Event | --solutions--event | #72d95a | |
| Digital | --solutions--digital | #667cff | |
| Campaign | --solutions--campaign | #a65bef | |
| Operation | --solutions--operation | #61dcc7 |
| Token | CSS variable | Base ≥992 | Tablet ≤991 | Mobile ≤767 |
|---|---|---|---|---|
| H1 | --typography--size--h1 | 4.875rem / 78px | 3.5rem / 56px | 2.75rem / 44px |
| H2 | --typography--size--h2 | 4rem / 64px | 2.875rem / 46px | 2.125rem / 34px |
| H3 | --typography--size--h3 | 3.375rem / 54px | 2.25rem / 36px | 1.75rem / 28px |
| H4 | --typography--size--h4 | 2rem / 32px | 1.25rem / 20px | 1.125rem / 18px |
| H5 | --typography--size--h5 | 1.5rem / 24px | 1.125rem / 18px | 1rem / 16px |
| Numbers | --typography--size--numbers | 4.625rem / 74px | 3.25rem / 52px | 2.5rem / 40px |
| Text lg | --typography--size--text-lg | 2.25rem / 36px | 1.5rem / 24px | 1.125rem / 18px |
| Text md | --typography--size--text-md | 1.125rem / 18px | 0.875rem / 14px | 0.875rem / 14px |
| Text sm | --typography--size--text-sm | 0.875rem / 14px | 0.75rem / 12px | 0.75rem / 12px |
| Meta | --typography--size--meta | 1rem / 16px | 0.75rem / 12px | 0.75rem / 12px |
| Menu Item | --typography--size--menu-item | 1.125rem / 18px | 1rem / 16px | 0.875rem / 14px |
| Token | CSS variable | Base ≥992 | Tablet ≤991 | Mobile ≤767 |
|---|---|---|---|---|
| Section gap lg | --padding--section--gap-lg | 8rem / 128px | 4.25rem / 68px | 2.8125rem / 45px |
| Section gap | --padding--section--gap | 4rem / 64px | 2.125rem / 34px | 1.875rem / 30px |
| Section gap sm | --padding--section--gap-sm | 2rem / 32px | 1.0625rem / 17px | 0.9375rem / 15px |
| Section gap xs | --padding--section--gap-xs | 1rem / 16px | 0.75rem / 12px | 0.75rem / 12px |
| Section top-bottom | --padding--section--tb | 8rem / 128px | 4.25rem / 68px | 2.9375rem / 47px |
| Section left-right | --padding--section--left-right | 8rem / 128px | 4.25rem / 68px | 1.25rem / 20px |
| Top menu height | --padding--top-menu--height | 8rem / 128px | 4.25rem / 68px | 2.9375rem / 47px |
| Top menu right-left | --padding--top-menu--right-left | 4rem / 64px | 2.125rem / 34px | 1.25rem / 20px |
| Top menu top-bottom | --padding--top-menu--top-bottom | 0.625rem / 10px | 0.625rem / 10px | 0.625rem / 10px |
| Button BG left-right | --padding--button--bg--left-right | 1.5rem / 24px | 1.0625rem / 17px | 1.125rem / 18px |
| Button BG top-bottom | --padding--button--bg--top-bottom | 0.625rem / 10px | 0.5rem / 8px | 0.5rem / 8px |
| Button Clear top-bottom | --padding--button--clear--top-bottom | 0.25rem / 4px | 0.5rem / 8px | 0.5rem / 8px |
| Button Clear left-right | --padding--button--clear--left-right | 0 | 0 | 0 |
| Token | CSS variable | Base ≥992 | Tablet ≤991 | Mobile ≤767 |
|---|---|---|---|---|
| Icon Size (m) | --item-sizes--icon--size | 2rem / 32px | 1.25rem / 20px | 1.25rem / 20px |
| Icon Size l | --item-sizes--icon--size-l | 2.5rem / 40px | 1.875rem / 30px | 1.25rem / 20px |
| Icon Size s | --item-sizes--icon--size-s | 1.25rem / 20px | 1rem / 16px | 0.875rem / 14px |
| Icon Size xs | --item-sizes--icon--size-xs | 1rem / 16px | 0.75rem / 12px | 0.625rem / 10px |
| Icon Distance | --item-sizes--icon--distance | 0.625rem / 10px | 0.375rem / 6px | 0.25rem / 4px |
| Logo | --item-sizes--logo | 4rem / 64px | 2.125rem / 34px | 1.875rem / 30px |
| Logo Footer | --item-sizes--logo--logo-footer | 12rem / 192px | 6.375rem / 102px | 5.625rem / 90px |
| Client logo project | --item-sizes--logo--client-logo-project | 2.5rem / 40px | 1.875rem / 30px | 1.5625rem / 25px |
| Project lg | --item-sizes--project--lg | 40rem / 640px | 21.25rem / 340px | 16.75rem / 268px |
| Project md | --item-sizes--project--md | 20rem / 320px | 10.625rem / 170px | 8.375rem / 134px |
02
Four families, each bound to a variable. The table is the source of truth — read the family names here, not off the rendered specimens.
| Role | CSS variable | Family | Weight | Used for |
|---|---|---|---|---|
| Header | --typography--family--header | Archivo | 700 / 500 | H1–H5, project tile titles |
| Text | --typography--family--text | Inter | 400 | Body, ledes, paragraphs |
| Meta | --typography--family--meta | Sometype Mono | 500 | Eyebrows, button labels, captions, chips |
| Numbers | --typography--family--numbers | Unbounded Variable | 400 | Impact stat figures only |
| Family | Source | Weights available |
|---|---|---|
| Archivo | Webflow Google Fonts | As selected in Site Settings → Fonts |
| Inter | Webflow Google Fonts | As selected in Site Settings → Fonts |
| Sometype Mono | Self-hosted custom font — SometypeMono-Variable.ttf | Variable, wght 400–700 |
| Unbounded Variable | Self-hosted custom font | Variable, wght 200–900 |
Headings — Archivo, weight 700 (500 on tile titles), line-height: 1, letter-spacing 0, no text stroke.
Meta — Sometype Mono, weight 500, line-height: 1, text-transform: uppercase, white-space: nowrap. The nowrap is load-bearing: without it a button label wraps to two lines and changes the button's height.
Body — Inter, weight 400. line-height: 1.2 for captions and stat labels, 1.55 for running text.
Sizes come from the live variables — resize the canvas and they step.
03
Figma (node 2:911) defines six styles. Every button on the site is one of them.
| Variant | Rest | Hover promotes to | Classes in use |
|---|---|---|---|
| Clear | No fill, no rule | 1px bottom rule appears | .yfwrk-allbtn-inner · .yfins-more · .solstack-clearbtn |
| Full | Brand/Dark fill, light text | Highlight | .letstalkemailbutton |
| Highlight | Brand/Y fill, dark text | Full | .yfabout-btn.yfabout-btn-fill · .solstack-pill · .yfcon-submit |
| Outline (dark) | Transparent, 1px Brand/Dark ring | Fills dark | .letstalkemailbutton.clearbutton |
| Outline (light) | Transparent, 1px Brand/Light ring | Fills light | .yfabout-btn.yfabout-btn-ghost |
| Light | Brand/Light fill, dark text | Highlight | — defined in Figma, not currently used |
| New pages | Brand/Y fill, dark text | Brand/Light fill, −1px lift | .yfapp-btn.yfapp-btn-fill |
Never hardcode a button width or height. Height is a consequence of the arrow box plus vertical padding plus the ring, so it steps on its own. Values below are measured, not derived:
| Type | Formula | Base | Tablet | Mobile |
|---|---|---|---|---|
| Filled / Outline | Icon Size + 2 × Button BG tb + 2 × 1px ring | 54px | 38px | 38px |
| Clear | Icon Size + 2 × Button Clear tb + 1px bottom rule | 41px | 37px | 37px |
Radius 1.875rem / 30px on filled and outline variants. Gap between label and arrow is --item-sizes--icon--distance. Every button carries flex: 0 0 auto and every label white-space: nowrap — without both, buttons shrink or wrap inside flex parents.
Copy the element rather than this markup — it is here so you can recognise a malformed button when you meet one.
<a class="yfabout-btn yfabout-btn-fill">
<div class="yfabout-btn-label">our work</div>
<div class="yfabout-arrow-wrap">
<div class="yfabout-arrow">
<!-- canonical arrow, 13×10 viewBox, fill=currentColor -->
<svg width="100%" height="100%" viewBox="0 0 13 10" fill="none">…</svg>
</div>
</div>
</a>
Same 13×10 geometry either way. They differ in how it is drawn, and that difference decides which one you can use:
| Technique | Where | Colour follows | Use when |
|---|---|---|---|
| Inline SVG in an embed | .yfwrk-allbtn-art · .yfabout-arrow | fill="currentColor" | Default. The arrow must inherit a colour that changes on hover or on a dark ground. |
| CSS mask-image on a div | .yfapp-btn-arrow · .yfapp-sol-toggle-arrow | background-color: currentColor | When the icon is built by script, or you want no embed inside the element. |
04
Every section opens with one. Three parts, always in this order: a square icon box, the mark inside it, and a Meta label. The label colour follows the section ground — Brand/Dark on light sections, Brand/Light on dark ones.
Both specimens below are real elements. Copy the one whose ground matches the section you are building.
the work
the impact
The shared family is .yfeyebrow-*, with a ground-state combo. One family, two grounds, no per-section copy:
<div class="yfeyebrow-row on-light"> <!-- or on-dark -->
<div class="yfeyebrow-icon">
<div class="yfeyebrow-art"></div> <!-- mark, CSS mask -->
</div>
<p class="yfeyebrow-label">the work</p>
</div>
| Part | Property | Bound to |
|---|---|---|
| .yfeyebrow-row | min-height | --padding--section--gap |
| .yfeyebrow-row | column-gap | --item-sizes--icon--distance |
| .yfeyebrow-icon | width, height (square) | --item-sizes--icon--size |
| .yfeyebrow-art | width, height | 56.25% of the icon box |
| .yfeyebrow-label | family, size | --typography--family--meta · --typography--size--meta |
| .on-light / .on-dark | colour | --brand--dark | --brand--light |
05
Every section is the same three-part shell. Build a new page by stacking these and filling the content slot — do not invent a layout.
<section class="yfapp-sec yfapp-sec-light" data-yf="‹name›">
<div class="yfapp-head">
<div class="yfeyebrow-row on-light">…</div>
<h2 class="yfapp-h2">Section title</h2>
</div>
<div class="yfapp-content">…</div>
</section>
| Class | Does | Bound to |
|---|---|---|
| .yfapp-sec | relative, flex column, full width, centred | padding: --padding--section--tb / --left-right · row-gap: --padding--section--gap |
| .yfapp-sec-light | ground + text colour | --backgrounds--bg on --brand--dark |
| .yfapp-sec-dark | the inverse | --brand--dark on --brand--light |
| .yfapp-head | eyebrow, then title | row-gap: --padding--section--gap-sm |
| .yfapp-content | the content slot | flex column, align start, no gap of its own |
Use these instead of styling a heading ad hoc. All are margin: 0, color: inherit, and bound to the scale in §1.
| Class | Family | Size token | Weight | Line-height |
|---|---|---|---|---|
| .yfapp-h1 | Header | --typography--size--h1 | 700 | 1.09 |
| .yfapp-h2 | Header | --typography--size--h2 | 700 | 1.09 |
| .yfapp-h3 | Header | --typography--size--h3 | 700 | 1.09 |
| .yfapp-h4 | Header | --typography--size--h4 | 500 | 1.09 |
| .yfapp-h5 | Header | --typography--size--h5 | 700 | 1.09 |
| .yfapp-lede | Text | --typography--size--text-lg | 400 | 1.25 |
| .yfapp-body | Text | --typography--size--text-md | 400 | 1.5 |
| .yfapp-num | Meta, uppercase | --typography--size--meta | 500 | 1 |
.yfapp-num takes a ground combo of its own — .yfapp-num-onlight (--text--text-light) or .yfapp-num-ondark (--text--text-light-alt) — because a counter is deliberately quieter than body text rather than inheriting it.
Added 18 Sep 2026, after an audit found six different ways of presenting a numbered run across the site.
A run of numbered items — process steps, an argument in stages, a list of situations — is always the same three parts, in this order. There is one pattern. Do not invent a second.
<div class="‹block›-col">
<p class="‹block›-col-label">01</p> <!-- .yfapp-num · bare two-digit number -->
<h3 class="‹block›-col-title">Research & Positioning</h3>
<p class="‹block›-col-body">…</p>
</div>
| Part | Rule |
|---|---|
| Number | Bare, zero-padded to two digits — 01. Not 1, and never 01 / LABEL. No slash, no label beside it. |
| Heading | A real heading element, Title Case, no terminal full stop. This is what puts the block into the document outline. |
| Body | One paragraph. Anything above the body that is not the heading is a variant — see below. |
Equal columns under a rule. Wraps to two-up at ≤991 and one-up at ≤767.
<div class="yfapp-cols"> <!-- 1px top rule, gap-sm above the row -->
<div class="yfapp-colrow"> <!-- column-gap: --padding--section--gap -->
<div class="yfapp-col">… <!-- flex:1 0 0, min-width:0, row-gap: gap-sm -->
</div>
</div>
Two or three stacks divided by a hairline: .yfapp-whom-row > .yfapp-whom-col with .yfapp-whom-rule between them. The rule is width:1px; align-self:stretch on desktop and flips to width:100%; height:1px at ≤991, so the same markup serves both axes.
Webflow inserts .w-dyn-list / .w-dyn-items / .w-dyn-item between your styled container and your styled row. They are not yours to class, so the flex context must be handed down through them in CSS or the rows lose their layout:
[data-yf="‹name›"] .‹list› .w-dyn-list,
[data-yf="‹name›"] .‹list› .w-dyn-items{width:100%}
[data-yf="‹name›"] .‹list› .w-dyn-items{display:flex;flex-direction:column}
Tactics link to Solutions through ONE field, not two. The Solution's curated Tactics list is the only place the relationship is edited; saving it auto-fills each tactic's Solution Slug and Curated Order. Every surface reads those two fields — the /solutions chips and the solution template's tactics grid both filter on Solution Slug and sort by Curated Order, which is why their counts agree. The legacy Parent Solution reference is still on the collection for older rows; never filter a list on it, or a page will advertise capabilities another page does not show.
06
The nav is a shared component and it reads the page, not the other way round. A page tells it what it is sitting on through attributes; get these wrong and the nav is unreadable over your hero.
It takes NO space in the flow. Your first section already starts at the very top of the viewport, underneath it. So the nav's height is added to the hero's top padding, never subtracted:
[data-yf-page="‹name›-hero"]{
padding-top: calc(var(--padding--section--tb) + 4.75rem);
}
| Attribute | On | Effect |
|---|---|---|
| data-yf="hero" | hero section | Marks it for the shared hero CSS |
| data-yf-nav-theme="dark" | any section | Forces the nav's palette where automatic luminance reading cannot see — a dark photo or video behind a transparent block |
| data-yf-glass="1" | hero section | Puts the nav into its glass state over this section |
| data-yf-glass-top | <body> | Nav opens in glass on the FIRST painted frame. Without it the bar paints light, then flips — a visible flash on any page that opens on a glass hero |
| data-yf-hero-glass="own" | <body> | The hero owns the glass and the nav stands down over it. See the trap below |
| Surface | Tint | Filter | No-backdrop-filter fallback |
|---|---|---|---|
| Nav over raw video | rgba(39,39,39,.65) | blur(20px) saturate(130%) | rgba(39,39,39,.82) |
| Hero owning the glass | rgba(39,39,39,.8775) | blur(28.28px) saturate(169%) | rgba(39,39,39,.96) |
.yfhero-vidfit{position:absolute;top:50%;left:50%;
width:100vw;height:56.25vw;min-width:177.78vh;min-height:100vh;
transform:translate(-50%,-50%);pointer-events:none;border:0}
Cover-fit for a 16:9 embed: 56.25vw is the height of a 16:9 box at full width, 177.78vh the width of one at full height, and whichever is larger wins. Repeat the rule under @supports (height:100svh) with the svh units so mobile browser chrome does not letterbox it. Stacking inside the hero is video 0, scrim 1, copy 2 — and the copy needs position: relative to get a z-index at all.
Webflow has no ≤1439 breakpoint and the 1920 one is not enabled, so a laptop gets desktop values with nowhere to put its own. Solve it once per page in CSS by re-declaring the TOKENS on the section class rather than restating every rule — everything already reads from the tokens, so one block moves the whole page down a tier:
@media (min-width:992px) and (max-width:1919px){
.yfapp-sec{
--typography--size--h1:4rem;
--padding--section--tb:6rem;
--item-sizes--icon--size:1.5rem;
/* …the rest of the 1440 column */
}
}
07
Two families, and they must not be mixed. UI feedback is quick and eased; content transitions are slow and share one curve so overlapping changes read as a single motion.
| Use | Duration | Curve | Properties |
|---|---|---|---|
| Button hover | 220ms | ease | background-color, border-color, color |
| Arrow nudge | 260ms | cubic-bezier(.2,.7,.2,1) | transform: translateX(3px) |
| Row / dot state | 140ms | ease | background-color, transform: scale(1.25) |
| Tile / card content | 0.45s | cubic-bezier(0.4,0,0.2,1) | color, border-color, filter |
| Image zoom on card | 0.6s | ease | transform: scale(1.12) |
| Nav ground change | 420ms | ease | background-color, border-bottom-color |
| Nav glass blur hold | 460ms | — | Delay before the blur is dropped on exit |
| Disclosure arrow | 200ms | ease | transform: rotate(180deg) — see §15 |
| Marquee loop | 30s | linear infinite | keyframe who-scroll — see §17 |
The 0.45s value is not arbitrary — it matches the project overlay slide, so a tile's title, client logo and solution chips all land at the same instant as the overlay.
08
Documented deviations, and the failure modes that have already cost real time. Read this before debugging anything visual.
| Where | Deviation | Why |
|---|---|---|
| .yfabout-btn | No hover lift, unlike every other button | A 1px translate on a bordered 30px-radius pill lands the edge on a half-pixel; the ring antialiases across two rows and reads as the button resizing |
| .yfabout-btn-fill | Carries a 1px transparent border | Matches the Outline variant's box so the two buttons are the same height |
| Project card chips | Hidden below 768px (Home) / 480px (Work) | Card is narrower than one chip at those widths |
| Work card chips | Capped at 3 by CSS, not by collection limit | The filter script reads every nested item's text; a collection limit would silently drop solutions from the filter |
| .image-71 | Carried a 1px Brand/Light border on all four sides — removed | Every About team portrait framed itself in near-white, so adjacent cards showed a 2px light line between them and it read as a gap in the grid. The cards are flush by design; there is no gutter to close. If a light line reappears between those cards, look for a border on the image before touching layout. |
| Impact 1M counter | Font-size measured and scaled during count | "999,999" needs 366px at desktop against a 211px card — six digits cannot fit the Numbers scale |
| Approach hero | Owns its glass; nav stands down over it | Two glass layers never equal one — see §6 |
| Chip labels | Not uppercase, weight 400 — unlike all other Meta type | Solution names are proper nouns — see §10 |
| .tactic-card | The only box-shadow on the site | Legacy. Separation elsewhere is a 1px rule — see §9 |
| .sg-fontload | An invisible element on this guide page only | Forces Webflow to bundle all four families on a page built from embeds — see §2 |
Lowercase selectors. Webflow publishes class names lowercased. Custom code querying .ProjectOverlay matches nothing; it must be .projectoverlay. The same gap bites when copying markup out of this guide — the Designer name may be capitalised.
Never hand-write a box-shadow containing var(…, fallback). The comma inside var() does not survive Webflow's shadow storage. It fails silently and renders as a solid blob with stretched children. Use real border properties bound to a variable instead.
SVGs are display:inline by default and sit ~4px below their box on the text baseline. The site head carries an icon reset that zeroes line-height on every icon box. Add new icon boxes to that list.
align-items:center cancels negative margins on a full-width flex child. Full-bleed needs align-self:stretch; width:auto.
A CSS border adds height to an auto-height box; box-shadow: inset 0 0 0 1px does not. Pick one per variant family and keep it consistent, or buttons drift apart by 2px.
Webflow forbids a Collection List inside a Link. Nested lists must be siblings of the link, with pointer-events:none so they don't steal clicks.
A nested multi-reference Collection List renders at most five items, silently. See §5 for the join-by-field workaround.
A class applied to no element in the Designer can be stripped from the published CSS. Style script-built elements in an embed, never as a Webflow class.
An inline font-family inside an embed does not make Webflow bundle that font. Only a Webflow style applied to a real element on the page does — §2 documents the fix.
A custom font is matched to its variable by literal family string. --typography--family--meta resolves to Sometype Mono; renaming the uploaded font, or the variable's value, breaks the match silently and everything falls back to a serif.
An empty combo class usually means the styling is on a global of the same name. .sol-card.sol-card--vert and .yfapp-num.yfapp-num-ondark are both empty; the flex-direction and the colour live on the standalone classes. Check before assuming something is broken.
Accordion state is not in CSS. Panels set only overflow:hidden; height is written at runtime. A panel that will not open is a script problem — §15.
There is no site-wide focus style. Exactly one :focus-visible rule exists on the entire site, on a legacy accordion header. Every new interactive element needs its own — §11.
mix-blend-mode: multiply blends an element with its whole subtree. Anything light placed inside the project overlay disappears against the photo — which is why the title, logo and chips are siblings of it, never children.
The Designer canvas runs no scripts, and Preview renders from the Designer's in-memory copy. Changes made through the API do not always reach an already-open Designer session. If something looks unchanged, reload the Designer tab before assuming the change failed.
· Dangling variables. Nine variable IDs are referenced by styles but no longer exist in the Base collection, so those declarations are invalid and paint nothing. Affected: .work-card-image, .tac-card, .partner-card-top, .partner-image-wrapper, .card-tactics-item, .footer-block-content, .counter-label, .fs-numbercount_number, .rangeslider_fill, .rangeslider_handle, .text-style-link, .tac-faq, .faq-a-padding, .filter_form, .filter_block_header, .filter_tags_item, .filter_search, and the hover states of .filter_dropdown_toggle / .filter_clear-all / .filter_dropdown_link. Fix by rebinding to a live token or deleting the declaration.
· No focus states. The contact form, the current disclosure toggle and the filter chip remove control are all keyboard-reachable with no visible focus indicator. Highest-impact accessibility item — §11.
· Form validation is unstyled — Webflow's default red and green blocks render against the dark contact section. §11.
· Active filters are invisible below 992px — .filter_tags is hidden at medium, so applied filters cannot be seen or removed individually on tablet or phone. §12.
· .yfhero-vidfit has no centring transform — the hero video is offset unless page CSS supplies one. §16.
· .yfprj-film has no height or ratio and collapses without an intrinsically-sized child. §16.
· Four broken utilities: .hide-mobile-portrait (empty), .show-mobile-portrait (hides forever), .max-width-full-mobile-portrait and .text-style-2lines (empty), plus .text-size-medium.text-weight-bold which sets weight 400. §18.
· Two 301 redirects are still owed for the old Geothermal project slugs.
· Two button generations. .yfapp-btn is the one for new pages; .letstalkemailbutton, .yfabout-btn, .yfwrk-allbtn and .yfcon-submit stay on their pages until touched.
· Two eyebrow generations. .yfeyebrow-* is the consolidation; eight per-section families migrate when their pages are rebuilt.
· Four counter implementations, two of them byte-identical (impactnumbersholder_* and fs-numbercount-4_*). §14.
· Three accordion implementations plus " 2" and " 3" clones of the Finsweet one. §15.
· Six chip/tag variants, including four paste-duplicates of .sol-tag. §10.
· An untrimmed Client-First utility import — roughly 96 classes, about 20 in use. §18.
· Dead classes from the Approach accordion removal, awaiting a sweep: .yfapp-sol-panel · -panelrow · -desc · -tactics · -tactics-tag · -chips · -chip · -chip-label · -dot · -more · -toggle-plus
· Dead classes awaiting a sweep: .projectsolutioncollection · .collection-item-28 · .heading-55 · .finsweetholder · .fs-numbercount-4_* · .page-wrapper · .yf-page-padding · .who-marquee-outer-copy · .yfval-rail-items · .ZFooter2Col · .GridNumber · .faq-body
· Roughly 122 Webflow default-named leftovers (Div Block 142, Paragraph 31, Image 77) and ~380 singleton class names.
· The 1920 breakpoint is still not enabled; §6 documents the CSS tier that stands in for it. Base still holds Desktop values.
· Stray variable mode zz-probe-nobp needs deleting by hand.
· .yfwrk-allbtn-icon / -art are the shared button arrow primitives despite the section prefix — rename to .yfbtn-* in one move, updating the icon reset list and hover selectors together.
· Sometype Mono is uploaded and self-hosted as a variable font (wght 400–700). Meta type no longer falls back to a serif. See §2.
09
Eight card families exist. Three are current, five are legacy and should not be extended. Every current card is a flex column whose spacing comes from the section gap tokens — a card never invents a padding value.
| Family | Status | Use for |
|---|---|---|
| .yfsolp-card | Canonical | Solution cards. The reference implementation — fully token-bound, needs no breakpoint overrides. |
| .yfins-card | Current | Insights / article grid cells. |
| .yfsol-tactic-card | Current | Tactic cards inside a solution page. |
| .yf-sol-card-top / -mid / -bottom | Half-migrated | Slot classes dropped into a legacy .sol-card. New yf- prefix but hardcoded 3.5rem type. Do not copy. |
| .work-card* | Legacy | Work grid tiles. Frozen. |
| .sol-card* | Legacy | Old solutions grid. Frozen. |
| .tac-card / .tactic-card | Legacy | Partnership/careers drafts. Frozen. |
| .partner-card / .member-card / .proj-card / .project-card | Legacy | One-off surfaces. Frozen. |
.yfsolp-cards flex column, row-gap --padding--section--gap-lg (8rem)
.yfsolp-card flex column, row-gap --padding--section--gap (4rem), min-height 41.875rem
.yfsolp-top .yfsolp-tl + .yfsolp-tr
.yfsolp-bot
.yfsolp-stage absolute inset 0, z-index 1, pointer-events none, display none
No padding, no border, no radius, no background on the card itself. A card is a spacing container; the ground comes from the section it sits in (§5).
.yfsolp-stage ships display:none and is switched on by script. It is inert by design — do not "fix" it in the Designer.
| Class | display | gap | Other |
|---|---|---|---|
| .yfsolp-card | flex column | --padding--section--gap | min-height 41.875rem |
| .yfsolp-cards | flex column | --padding--section--gap-lg | — |
| .yfsolp-desc | — | — | max-width 31.25rem, Inter, --typography--size--text-md, lh 1.5 |
| .yfins-card | flex column | --padding--section--gap-sm | min-width 0 |
| .yfins-body | flex column | --padding--section--gap-xs | — |
| .yfins-media | — | — | aspect-ratio 469/307, overflow hidden |
| .yfsol-tactic-card | flex column | --padding--section--gap-xs | padding same token, bg --backgrounds--pure-white |
· .yfins-list 3 → 2 (medium) → 1 column (small); .yfins-cell gains margin:auto at small to centre the single column; .yfins-card caps at 30rem.
· .work-card-wrapper aspect-ratio 2/3 → 1/1 at small.
· .proj-card is the only card that restacks row → column (medium).
· .yfsolp-* has zero breakpoint overrides. That is the target: if a new card needs a medium rule, ask first whether a token would do it instead.
10
A chip is a dot plus a label in a row. It is not a pill: the current chip has no background, no border and no radius. Six variants exist; two are the same component under different prefixes, and the rest are legacy pills.
.yfsolp-tactics / .yfapp-sol-chips flex, wrap, gap --item-sizes--icon--distance (0.625rem)
.yfsolp-chip / .yfapp-sol-chip flex, padding 0.25rem 0, gap 0.625rem, no bg/border/radius
.yfsolp-chip-dotwrap 1rem square, centring box
.yfsolp-chip-dot 0.625rem circle, 1px border, flex 0 0 auto
.yfsolp-chip-label Sometype Mono, --typography--size--meta, lh 1.2, 400, nowrap
Chip labels are not uppercase. Unlike every other Meta element on the site, .yfsolp-chip-label and .yfapp-sol-chip-label set no text-transform and weight 400, not 500. Solution names are proper nouns and are set as written. This is an intentional exception to the §2 Meta rule.
.yfapp-sol-chip is a byte-identical copy of .yfsolp-chip for the app shell. Two prefixes, one component — consolidate when either page is next touched.
| Class | Shape | Why it is legacy |
|---|---|---|
| .solstack-pill | radius 1.875rem, bg --brand--dark, uppercase label | Token-bound but a different shape language from the current chip |
| .yfprj-tag | inline-flex, radius 30px, 1px --lines--border | Hardcoded 30px radius and padding |
| .sol-tag · .sol-tag-1 · .sol-tag-1-2 · .sol-tag-1-2-3 | radius 20px, 10–11px type | Four Webflow paste-duplicates of one class. Delete three. |
| .work-tag · .insights-tag | radius 20px, rgba borders | No variable bindings at all; .insights-tag adds a lone backdrop-filter: blur(4px) |
| .TacticPill | height 2.6875rem, radius 62.4375rem, 1px black | PascalCase — the oldest naming layer |
| .filter_tags_item | radius 0.25rem, background from a deleted variable | Finsweet snake_case; hidden entirely at medium (§12) |
11
One current form system, .yfcon-*, on the Contact page. Four legacy sets survive on drafts and Finsweet demo surfaces. Fields are flat, square and transparent on the dark ground — there is no filled input anywhere on this site.
.yfcon-section flex column, padding --padding--section--tb / --left-right, bg --brand--dark
.yfcon-row grid 1fr 1fr (medium: 1fr)
.yfcon-col-right
.yfcon-formwrap width 100%, margin-bottom 0 [Webflow Form Block]
.yfcon-form flex column, row-gap --padding--section--gap-sm (2rem)
.yfcon-field × n
.yfcon-field.yfcon-field-area [textarea]
.yfcon-submit
.yfcon-submit-icon > .yfcon-submit-art
.yfcon-submit-label
| Property | Value |
|---|---|
| width / height | 100% / auto |
| padding | 0.75rem all sides |
| border | 1px solid --lines--separator on all four sides |
| border-radius | 0 — fields are square, buttons are pills |
| background | transparent |
| colour | --brand--light |
| type | Sometype Mono, --typography--size--meta, lh 1.25, weight 400 |
| textarea combo | .yfcon-field-area — height and min-height 9.9375rem, resize: vertical |
The submit is a full member of the §3 button family: padding --padding--button--bg--top-bottom / --padding--button--bg--left-right, gap --item-sizes--icon--distance, radius 1.875rem, ground --brand--y, label --brand--dark, Sometype Mono --typography--size--meta weight 500 uppercase, icon box 1.25rem × --item-sizes--icon--size, art 0.8125 × 0.625rem. It differs from .yfapp-btn only in prefix — consolidate when Contact is next rebuilt.
| Set | Status | Where |
|---|---|---|
| .yfcon-* | Current | Contact |
| .checkbox_* .radio_* | Off-system but live | Filter controls on Work / Solutions listings — functionally current, naming is not |
| .form-* | Legacy | Webflow starter residue — Dump, Password, Finsweet styleguide |
| .tac-form .tac-input .tac-submit | Legacy | Partnership / Careers drafts. Carries dangling variables. |
| .fs-checkbox-5_* .fs-selectcustom_* | Legacy demo | Hardcoded #4c4c4c / #a1a1a1 / #6e30f7 — matches no token |
There is no .yfcon-fieldlabel. .yfcon-label exists but is a value label, not bound to a field — inputs currently carry no visible label element, only placeholders.
12
One coherent Finsweet-based filtering system, used on the Work listing. It is entirely snake_case and off the token naming, but it works and it is the only filter UI on the site — document it, do not rewrite it piecemeal.
.filter_layout flex column, gap 2rem
.filter_content flex column, padding 2rem 2rem 4rem, bg --backgrounds--pure-white
.filter_grid grid auto 1fr (medium: flex · small: 1fr)
.filter_form_block border-right 1px --lines--separator (medium: display none)
.filter_form grid 1fr, row-gap 1rem, radius 0.375rem
.filter_block
.filter_block_header padding 1rem, borders top+bottom 1px separator
.filter_list-wrapper > .filter_list > .checkbox_field
.filter_list [results column]
.filter_tags flex wrap (medium: display none)
.filter_results flex, min-height 3rem, justify flex-end (medium: space-between)
.filter_empty flex column centred, min-height 12rem, padding 3rem
Dropdown. .filter_dropdown_toggle min-width 12rem, padding 0.5rem 4rem 0.5rem 1.2rem, radius 0.25rem, ground --backgrounds--pure-white. Icon 1rem with 1.2rem right margin. Links: padding 0.5rem 2rem 0.5rem 1.5rem, 1px bottom border in white.
Search. .filter_search min-height 3rem, padding-left 3.25rem to clear the magnifier background-image, transparent ground, 1px borders.
Results counter. .filter_results_span is a hardcoded #111 chip with #a1a1a1 text — off-token, min-width 1.5rem, radius 0.25rem.
Facet count. .filter_facet-count margin-left auto, Sometype Mono 0.875rem.
Empty and loading. .filter_empty colour #b7b7b7, 4rem icon. An exhausted facet is .is-list-emptyfacet → opacity: 0.5 and nothing else. A loading item is .is-list-loading → opacity 0; translateY(2rem).
13
The footer is the second global surface after the nav (§6). One current implementation, .yffoot-*; one dead PascalCase generation still sitting in the class list.
.yffoot-footer flex column, border-top 1px --lines--separator, bg --brand--dark
padding --padding--section--tb / --left-right, gap --padding--section--gap
.yffoot-main flex row, col-gap --padding--section--gap-lg (medium: column)
.yffoot-logo-col > .yffoot-logo > .yffoot-logo-art
.yffoot-cols-wrap margin-left auto, align-items flex-end (medium: full width, left)
.yffoot-cols grid repeat(4, minmax(0, max-content)) (medium: 1fr 1fr)
.yffoot-col flex column, row-gap --padding--section--gap-sm
.yffoot-col-title
.yffoot-links > .yffoot-link (+ combo .yffoot-sol-link)
.yffoot-note
.yffoot-codeholder display:none
| Class | Family | Size | Colour | Weight |
|---|---|---|---|---|
| .yffoot-col-title | Meta | --typography--size--menu-item | --brand--y | 500, uppercase |
| .yffoot-link | Meta | --typography--size--meta | --brand--light | 400 |
| .yffoot-note | Meta | --typography--size--meta | --text--text-light-alt | 400, lh 1.4 |
medium — .yffoot-main becomes a column; .yffoot-cols-wrap drops its auto margin and goes full width, left-aligned; .yffoot-cols 4 → 2 columns; logo 12rem → 8rem; section padding 8rem/8rem → 5rem/4rem.
small — padding 4rem/2rem, gaps tighten to 2rem. The grid stays at two columns; it never goes to one.
14
Four parallel implementations of the same idea. .yfimp-* is the one to use; the other three are a Finsweet component, a renamed copy of that same component, and a set of one-off numbers.
.yfimp-section flex column, padding --padding--section--tb / --left-right, bg --brand--dark
.yfimp-eyebrowbar height --padding--section--gap, overflow hidden
.yfimp-eyebrowrow > .yfimp-eyebrowicon > .yfimp-eyebrowart + .yfimp-eyebrow
.yfimp-row flex, space-between, border-top 1px --lines--separator,
padding-block --padding--section--gap
.yfimp-passthrough display:contents [CMS list wrapper made invisible to layout]
.yfimp-num flex column, row-gap --padding--section--gap-sm, flex 0 1 15rem
.yfimp-numrow flex, align-items: baseline
.yfimp-numvalue Unbounded, --typography--size--numbers, --brand--y, lh 1, nowrap
.yfimp-numlabel Sometype Mono, --typography--size--meta, --brand--light, lh 1.2
align-items: baseline on .yfimp-numrow is load-bearing. The figure is Unbounded at 4.625rem and the suffix is Sometype Mono at 1rem; anything other than baseline alignment makes the suffix float.
.yfimp-passthrough uses display: contents so a Webflow Collection List wrapper disappears from the flex layout and the items become direct children of .yfimp-row. Reuse this whenever a CMS list needs to participate in its parent's flex or grid — it is cleaner than restyling the wrapper.
medium — .yfimp-row switches to flex-start + wrap, col-gap --padding--section--gap, row-gap 3.4375rem; .yfimp-num fixes at 14.375rem.
small — back to space-between, col-gap 0, .yfimp-num width 45.5% — a deliberate two-up grid.
15
Three accordion implementations plus one current toggle primitive. Read the state note first — it explains why none of these can be debugged from CSS alone.
| Set | Status | Notes |
|---|---|---|
| .yfapp-sol-toggle / -arrow / -plus | Current | --item-sizes--icon--size-s box, glyph drawn with a CSS mask-image data-URI over currentColor — so it inherits the section's ground automatically. Use this pattern for any new icon that must work on both grounds. |
| .faq2_* | Transitional | Token-bound colours and one responsive rule, but Relume naming. Question row: padding 1.5rem 2rem, 1px --lines--separator top/right/left, ground --backgrounds--pure-white, 700 uppercase. |
| .fs_accordion-1_* / -2_* | Legacy | Duplicated as " 2" and " 3" clones, hardcoded hsla() borders, fixed 7.3125rem header height. fs_accordion-2 exists only as an arrow wrapper — there is no component behind it. |
| .tac-faq* / .faq-a* | Legacy | Draft pages only. Paints from two deleted variables and a @swatch_ literal. .faq-body is empty. |
Only one breakpoint override exists across all four sets: .faq2_arrow shrinks from 2rem to 1.5rem at small. Everything else is breakpoint-agnostic, which is correct — disclosure rows should be fluid, not stepped.
16
One pattern, used everywhere current: a frame that owns the ratio and clips, holding a fill that covers it. Never put the ratio on the image itself.
.frame position:relative; overflow:hidden; width:100%; aspect-ratio: W / H; .fill display:block; width:100%; height:100%; object-fit:cover;
Thirty-one classes set an aspect-ratio. Before inventing a new one, use the ratio that already means what you want.
| Ratio | Means | Classes |
|---|---|---|
| 1880 / 1080 | Full-bleed page banner | .yfprj-banner-img · .yfsol-hero-img · .yfsol-hero-media |
| 16 / 9 | Gallery image | .yfprj-gallery-img |
| 469 / 307 | Insights card media | .yfins-media |
| 749 / 430 | Solution card media | .yfsolp-media-img |
| 569 / 321 | Solution stack media | .solstack-media-frame |
| 4 / 3 | Insights thumbnail | .insights-img-wrap |
| 4 / 5 | Portrait photo | .yfabout-photo |
| 2 / 3 | Tall tile | .work-card-wrapper (small: 1/1) · .member-card |
| 3 / 2 | Landscape tile | .counter · .project-card.related |
| 1 / 1 | Logo, icon, square crop | .work-card-image · .work-card-logo · .who-logo-box · .partner-icon · icon wrappers · several numbered Image classes |
object-fit convention. cover for photography and any image that fills a frame (20 classes). contain for logos, marks, icons and map artwork — anything whose edges must not be cropped (11 classes). fill appears twice and both are legacy; do not add a third.
17
Three unrelated mechanisms share this vocabulary. Knowing which one you are looking at saves the most time here.
.who-marquee-outer clips (min-height 6.25rem, overflow-x/y: hidden); .who-marquee-track is display:flex; width:max-content; animation: who-scroll 30s linear infinite; will-change:transform. No script. Copy this when you need a logo strip. Note the base class also sets overflow:visible before the axis-specific rules — dead shorthand, harmless, worth removing.
.yfval-rail-cell and .yfval-rail-items are byte-identical duplicates; keep one. .yfval-gridwrap is width: 49.875rem; max-width: 50% — the only place on the site where a rem width and a percentage cap are used together, and the reason the block looks fixed until the viewport drops below ~100rem.
The native slider set (.slider min/max-height 720px, .slide-nav, .SliderNavWrap, .SliderNumberWrap, .hero-slide) plus the whole our-partners-logo-slider-* family (~20 classes, px units, Lato, !important backgrounds) is legacy. Only two classes in it carry any breakpoint rule at all.
.rangeslider_* is a bespoke control: track 96% × 0.25rem, radius 999rem, ground #4c4c4c; handle 1rem square, radius 999rem, cursor: grab; native input display:none. Both the fill and the handle paint from a deleted variable, so the control currently renders with no colour on either — listed in §8.
18
A Client-First utility layer arrived with an old import and never left. It is not the system — §1 variables are — but roughly a hundred of these classes exist, a few dozen are genuinely in use, and the scale has been locally modified, so it has to be written down.
| Step | Base | Tablet | Mobile |
|---|---|---|---|
| 0 | 0 | — | — |
| tiny | 0.125rem | — | — |
| xxsmall | 0.25rem | — | — |
| xsmall | 0.5rem | — | — |
| small | 1rem | — | — |
| medium | 2rem | 1.5rem | 1.25rem |
| large | 3rem | 2.5rem | 1.5rem |
| xlarge | 4rem | 3rem | 2rem |
| xxlarge | 5rem | 4rem | 3rem |
| huge | 6rem | 5rem | 3.5rem |
| xhuge | 8rem | 6rem | 4rem |
| xxhuge | 12rem | 8rem | 4.5rem |
max-width-* — xxsmall 12 · xsmall 16 · small 20 · medium 32 · large 48 · xlarge 64 · xxlarge 80 (rem, each with width:100%). No huge/xhuge/xxhuge exist.
text-size-* — tiny 0.75 · small 0.875 · regular 1 · medium 1.25 · large 1.5rem (1.25rem at small). No xlarge.
text-weight-* — light 300 · normal 400 · medium 500 · semibold 600 · bold 700 · xbold 800.
heading-* — xsmall 0.875/1.5 · small 1.25 (1rem) · medium 2 (1.5) · large 3 (2) · xlarge 4rem (2.5rem at small), all weight 700.
container-* — small 48 · medium 64 · large 80rem · wide 90vw · nav 100%. page-padding 2.5rem → 1.25rem at small.
Visibility — hide-tablet, hide-mobile-landscape, show-tablet, show-mobile-landscape behave as named.