YF Agency · internal reference · published to staging only

Style Guide

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.

Specimens are real elements — copy them. The buttons (§3) and eyebrows (§4) are ordinary Designer elements, so you can select one in the Navigator, copy it, and paste it onto any page. It arrives carrying its real classes and styles itself on landing. The documentation around them — tables, notes, code samples, token swatches — stays in embeds, because nobody copies reference material.

Building a new 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.

How to use this

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.

Breakpoints. Only four are enabled: main (≥992), medium (≤991), small (≤767), tiny (≤479). large, xl and xxl are NOT enabled and cannot be enabled through the API. tiny has no variable mode of its own — it inherits small. §6 covers the CSS tier that stands in for the missing 1440 column.
Webflow publishes class names lowercased. The Designer shows ProjectOverlay; the emitted selector and DOM class are projectoverlay. DOM selectors are case-sensitive — any custom code that queries a class MUST use the lowercase form or it silently matches nothing. The same gap bites when copying markup: the class list in a code sample is the published form, so the Designer name may be capitalised (LetsTalkEmailButton, not letstalkemailbutton).

Contents

System — the rules that apply everywhere.

Component catalogue — what exists, which generation is current, and what is frozen.

What this guide does not cover, and why. The site carries roughly 2,200 styles across 30 pages, and about a third of those are Webflow default names (Div Block 142, Paragraph 31), one-off singletons, or dead residue from imports. Documenting every one would be documenting the mess, not the system. What is specified here is every pattern that recurs — plus, for each, which classes are legacy so nobody extends the wrong generation. If you meet a class that appears in no section below, that is the answer: it is not part of the system, and it should not be copied.

01

Variables

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.

ModeBreakpointApplies to
Base modemain≥ 992px — desktop
Tabletmedium≤ 991px
Mobilesmall≤ 767px (and ≤479 by inheritance)
zz-probe-nobp—Dead. Left by a failed probe; delete by hand in the Designer.

Colour

Identical in all three modes — colour does not step.

TokenCSS variableValue
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

Solution accents

Per-solution identity colours. Used for the solution stack and CMS-driven accents — never for UI chrome.

TokenCSS variableValue
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

Type scale

TokenCSS variableBase ≥992Tablet ≤991Mobile ≤767
H1--typography--size--h14.875rem / 78px3.5rem / 56px2.75rem / 44px
H2--typography--size--h24rem / 64px2.875rem / 46px2.125rem / 34px
H3--typography--size--h33.375rem / 54px2.25rem / 36px1.75rem / 28px
H4--typography--size--h42rem / 32px1.25rem / 20px1.125rem / 18px
H5--typography--size--h51.5rem / 24px1.125rem / 18px1rem / 16px
Numbers--typography--size--numbers4.625rem / 74px3.25rem / 52px2.5rem / 40px
Text lg--typography--size--text-lg2.25rem / 36px1.5rem / 24px1.125rem / 18px
Text md--typography--size--text-md1.125rem / 18px0.875rem / 14px0.875rem / 14px
Text sm--typography--size--text-sm0.875rem / 14px0.75rem / 12px0.75rem / 12px
Meta--typography--size--meta1rem / 16px0.75rem / 12px0.75rem / 12px
Menu Item--typography--size--menu-item1.125rem / 18px1rem / 16px0.875rem / 14px

Spacing

TokenCSS variableBase ≥992Tablet ≤991Mobile ≤767
Section gap lg--padding--section--gap-lg8rem / 128px4.25rem / 68px2.8125rem / 45px
Section gap--padding--section--gap4rem / 64px2.125rem / 34px1.875rem / 30px
Section gap sm--padding--section--gap-sm2rem / 32px1.0625rem / 17px0.9375rem / 15px
Section gap xs--padding--section--gap-xs1rem / 16px0.75rem / 12px0.75rem / 12px
Section top-bottom--padding--section--tb8rem / 128px4.25rem / 68px2.9375rem / 47px
Section left-right--padding--section--left-right8rem / 128px4.25rem / 68px1.25rem / 20px
Top menu height--padding--top-menu--height8rem / 128px4.25rem / 68px2.9375rem / 47px
Top menu right-left--padding--top-menu--right-left4rem / 64px2.125rem / 34px1.25rem / 20px
Top menu top-bottom--padding--top-menu--top-bottom0.625rem / 10px0.625rem / 10px0.625rem / 10px
Button BG left-right--padding--button--bg--left-right1.5rem / 24px1.0625rem / 17px1.125rem / 18px
Button BG top-bottom--padding--button--bg--top-bottom0.625rem / 10px0.5rem / 8px0.5rem / 8px
Button Clear top-bottom--padding--button--clear--top-bottom0.25rem / 4px0.5rem / 8px0.5rem / 8px
Button Clear left-right--padding--button--clear--left-right000

Item sizes

TokenCSS variableBase ≥992Tablet ≤991Mobile ≤767
Icon Size (m)--item-sizes--icon--size2rem / 32px1.25rem / 20px1.25rem / 20px
Icon Size l--item-sizes--icon--size-l2.5rem / 40px1.875rem / 30px1.25rem / 20px
Icon Size s--item-sizes--icon--size-s1.25rem / 20px1rem / 16px0.875rem / 14px
Icon Size xs--item-sizes--icon--size-xs1rem / 16px0.75rem / 12px0.625rem / 10px
Icon Distance--item-sizes--icon--distance0.625rem / 10px0.375rem / 6px0.25rem / 4px
Logo--item-sizes--logo4rem / 64px2.125rem / 34px1.875rem / 30px
Logo Footer--item-sizes--logo--logo-footer12rem / 192px6.375rem / 102px5.625rem / 90px
Client logo project--item-sizes--logo--client-logo-project2.5rem / 40px1.875rem / 30px1.5625rem / 25px
Project lg--item-sizes--project--lg40rem / 640px21.25rem / 340px16.75rem / 268px
Project md--item-sizes--project--md20rem / 320px10.625rem / 170px8.375rem / 134px

02

Typography

Four families, each bound to a variable. The table is the source of truth — read the family names here, not off the rendered specimens.

RoleCSS variableFamilyWeightUsed for
Header--typography--family--headerArchivo700 / 500H1–H5, project tile titles
Text--typography--family--textInter400Body, ledes, paragraphs
Meta--typography--family--metaSometype Mono500Eyebrows, button labels, captions, chips
Numbers--typography--family--numbersUnbounded Variable400Impact stat figures only

Where the four families come from

FamilySourceWeights available
ArchivoWebflow Google FontsAs selected in Site Settings → Fonts
InterWebflow Google FontsAs selected in Site Settings → Fonts
Sometype MonoSelf-hosted custom font — SometypeMono-Variable.ttfVariable, wght 400–700
Unbounded VariableSelf-hosted custom fontVariable, wght 200–900
Sometype Mono is uploaded and live. It is self-hosted as a variable font (upstream google/fonts · ofl/sometypemono, OFL-1.1), registered under the family name Sometype Mono — which is the exact string --typography--family--meta resolves to. Do not rename either side: the variable matches the uploaded family by literal string, so renaming one silently drops every Meta element back to a serif. Because it carries a wght axis from 400 to 700, weight 500 renders as a true 500 rather than a synthesised one; no second upload is needed to add a weight in that range.
Why the specimens on this page render correctly — and the invisible block that makes them. Webflow only bundles a font if a Webflow style on that page references it, and this guide is built almost entirely from embeds, whose inline font-family declarations Webflow never sees. The first child of the page body is therefore a 1×1, zero-opacity .sg-fontload block holding four text elements classed .sg-fontload-header / -text / -meta / -numbers, each bound to its family variable. It exists solely to make Webflow bundle all four faces here. Do not delete it — without it every specimen below silently falls back to a system face and this page stops being a usable reference. Apply the same trick to any future page whose type is driven from embeds or from script-built markup.

Rules

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.

Scale specimens

Sizes come from the live variables — resize the canvas and they step.

H1 · We make your brand work
H2 · Solutions. Implemented
H3 · Section heading
H4 · Card title
H5 · Sub heading
300+
Text lg · lede paragraph
Text md · running body copy
Text sm · caption
Meta · eyebrow and button label

03

Buttons

Figma (node 2:911) defines six styles. Every button on the site is one of them.

The specimens below are real Designer elements, not markup in an embed. Select one in the Navigator, copy it, and paste it onto any page — it arrives carrying its real classes, so it styles itself on landing. Change the label and the link and it is done. Only the surrounding documentation on this page is an embed; anything that renders as a component is grabbable.
Which family to use on a NEW page: .yfapp-btn. There are two generations here. The older families were named per section (.letstalkemailbutton, .yfabout-btn, .yfwrk-allbtn) and they stay where they are — the pages using them work. The Approach page introduced one page-neutral family, .yfapp-btn + .yfapp-btn-fill, and that is what a new page should use. Do not add an eighth.
VariantRestHover promotes toClasses in use
ClearNo fill, no rule1px bottom rule appears.yfwrk-allbtn-inner · .yfins-more · .solstack-clearbtn
FullBrand/Dark fill, light textHighlight.letstalkemailbutton
HighlightBrand/Y fill, dark textFull.yfabout-btn.yfabout-btn-fill · .solstack-pill · .yfcon-submit
Outline (dark)Transparent, 1px Brand/Dark ringFills dark.letstalkemailbutton.clearbutton
Outline (light)Transparent, 1px Brand/Light ringFills light.yfabout-btn.yfabout-btn-ghost
LightBrand/Light fill, dark textHighlight— defined in Figma, not currently used
New pagesBrand/Y fill, dark textBrand/Light fill, −1px lift.yfapp-btn.yfapp-btn-fill

Geometry — all variants

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:

TypeFormulaBaseTabletMobile
Filled / OutlineIcon Size + 2 × Button BG tb + 2 × 1px ring54px38px38px
ClearIcon Size + 2 × Button Clear tb + 1px bottom rule41px37px37px
Where the ring comes into the height. Outline variants carry a real 1px border, so they are 2px taller than padding alone would make them. The filled variants therefore carry a transparent 1px border of their own — purely so a Highlight and an Outline sitting side by side have identical boxes. Clear buttons get 1px from their transparent bottom rule for the same reason.

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.

Live specimens — on light

Live specimens — on dark

Structure

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>

The arrow — two techniques, one shape

Same 13×10 geometry either way. They differ in how it is drawn, and that difference decides which one you can use:

TechniqueWhereColour followsUse when
Inline SVG in an embed.yfwrk-allbtn-art · .yfabout-arrowfill="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-arrowbackground-color: currentColorWhen the icon is built by script, or you want no embed inside the element.
Both inherit currentColor, which is the part that matters. What must never come back is a second shape — there were once two viewBoxes and two typed → glyphs on the site. One geometry, drawn one of two ways.
The inline-SVG arrow stays an embed inside the element. Webflow has no native inline-SVG element, so it is an HTML Embed nested in the icon box — exactly as the live nav and About buttons build it. It travels with the button when you copy it, so this costs you nothing.
The wrapper sets the button's height. 1.25rem wide by --item-sizes--icon--size tall. That height must stay bound to the token or the button stops stepping between breakpoints.

04

Eyebrows

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.

On a light section

実績

On a dark section

インパクト

Use this one on a new page

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>
PartPropertyBound to
.yfeyebrow-rowmin-height--padding--section--gap
.yfeyebrow-rowcolumn-gap--item-sizes--icon--distance
.yfeyebrow-iconwidth, height (square)--item-sizes--icon--size
.yfeyebrow-artwidth, height56.25% of the icon box
.yfeyebrow-labelfamily, size--typography--family--meta · --typography--size--meta
.on-light / .on-darkcolour--brand--dark | --brand--light
min-height, not height. A fixed height set from the gap token is smaller than a button at tablet and below, so anything placed in the row overflows it. The row uses min-height for exactly this reason.
The label inherits its colour. .yfeyebrow-label and .yfeyebrow-icon are both color: inherit, so the ground combo on the row colours the whole thing in one place — and a section that flips dark needs no eyebrow change at all.
Resolved debt — the five families are now six, and the sixth is the fix. yfwrk- · yfimp- · yfcli- · yfins-eyebrow- · yfcon-eyebrow- are the old per-section copies and still live on their pages. .yfeyebrow-* is the consolidation this guide used to ask for. Build new pages on it and migrate the old ones when you touch them; do not add a seventh.

05

Section shell & layout primitives

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>
ClassDoesBound to
.yfapp-secrelative, flex column, full width, centredpadding: --padding--section--tb / --left-right · row-gap: --padding--section--gap
.yfapp-sec-lightground + text colour--backgrounds--bg on --brand--dark
.yfapp-sec-darkthe inverse--brand--dark on --brand--light
.yfapp-headeyebrow, then titlerow-gap: --padding--section--gap-sm
.yfapp-contentthe content slotflex column, align start, no gap of its own
Colour flows down, it is never repeated. The ground combo sets color once on the section; every type utility, eyebrow part and icon below it is color: inherit. Flipping a section from light to dark is a one-class change and nothing inside it needs touching. Never set a text colour on a child that could inherit it.

Type utilities

Use these instead of styling a heading ad hoc. All are margin: 0, color: inherit, and bound to the scale in §1.

ClassFamilySize tokenWeightLine-height
.yfapp-h1Header--typography--size--h17001.09
.yfapp-h2Header--typography--size--h27001.09
.yfapp-h3Header--typography--size--h37001.09
.yfapp-h4Header--typography--size--h45001.09
.yfapp-h5Header--typography--size--h57001.09
.yfapp-ledeText--typography--size--text-lg4001.25
.yfapp-bodyText--typography--size--text-md4001.5
.yfapp-numMeta, uppercase--typography--size--meta5001

.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.

Numbered blocks

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 &amp; Positioning</h3> <p class="‹block›-col-body">…</p> </div>
PartRule
NumberBare, zero-padded to two digits — 01. Not 1, and never 01 / LABEL. No slash, no label beside it.
HeadingA real heading element, Title Case, no terminal full stop. This is what puts the block into the document outline.
BodyOne paragraph. Anything above the body that is not the heading is a variant — see below.
The number must not carry the label. The solution pages shipped 01 / CHALLENGE as a single label element, directly above a section using 01 + heading + body. Two anatomies, one page, adjacent. It also meant the Challenge / Transformation / Result arc contributed nothing to the heading outline, because the only text above each paragraph was itself a paragraph. Rebuilt on .yfsol-col-label + .yfsol-col-title + .yfsol-col-body, which is what the approach block below it already used.
Indices pad. Quantities do not. A list index is 01–07. A measured figure is written as it reads: 15, 80%, 1M+, 12 yrs. The /work filter counts pad because they read as a set; the Impact stats do not because they are measurements.
Known exception, unresolved — the /solutions row numbers. They render 1–7 because they are bound to the Solutions order field, and a Number field cannot render a leading zero. The two ways out are a separate PlainText index field on the collection, or a CSS ::before. The second is a runtime-only fix the next editor cannot see in the Designer, so it is not the house answer. Left as is pending a decision.
The one permitted variant. A block whose items are named things rather than steps — the trademarked frameworks on /approach — may carry a single tagline line between the heading and the body. Build it as a combo class on the standard block, named so the name says what it does. Never as a second pattern.
Section heads follow the same logic. One module, one eyebrow, one heading — wherever it appears. The more work module ran THE WORK + “Keep exploring” on solution pages and KEEP EXPLORING + “Other projects” on /approach: the same two strings, swapped between slots. A heading takes a full stop only when it is a complete sentence making a claim.

Column row

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>
min-width: 0 on every flex column. Without it a long word sets the column's floor and the row overflows instead of wrapping. This is on .yfapp-col already; keep it on any column you add.

Split with a rule

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.

Collection List plumbing

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}
A nested multi-reference Collection List renders at most FIVE items. Webflow caps it silently — no warning, no error, the sixth item simply is not there. If a row needs more than five children from a referenced collection, do not nest a list: put a hidden top-level list of that collection on the page and join it in script by a shared field. Both the Home solution stack and the Approach page hit this.

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.

A class applied to no element in the Designer can be dropped from the published stylesheet. If script builds an element at runtime, style it in an embed, not as a Webflow class — otherwise it works in Preview and vanishes on the live site.
Every section carries data-yf="‹name›" on the section element. Custom code scopes to that attribute rather than to class names, so a class rename can't silently break a script.

06

Page shell — hero, video and the nav

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.

The nav is position: fixed

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); }
Do not pull the hero up with a negative margin. It looks equivalent and is not: it measures the top padding from the top of the viewport instead of from the nav's bottom edge. That reads fine while tb is larger than the nav (128px, 96px) and slides the eyebrow underneath it at the smaller tiers (68px, 47px). The Approach hero shipped that bug and it only surfaced when the copy stopped being bottom-aligned.

Attribute contract

AttributeOnEffect
data-yf="hero"hero sectionMarks it for the shared hero CSS
data-yf-nav-theme="dark"any sectionForces the nav's palette where automatic luminance reading cannot see — a dark photo or video behind a transparent block
data-yf-glass="1"hero sectionPuts 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
Theme and glass are two separate attributes on purpose. An early build used a single data-yf-nav-theme="glass"; the theme engine evaluates that as "not dark", classified the hero as light, and the nav labels measured 1.06:1 into the next section. Tag a glass hero as BOTH dark and glass.

Glass

SurfaceTintFilterNo-backdrop-filter fallback
Nav over raw videorgba(39,39,39,.65)blur(20px) saturate(130%)rgba(39,39,39,.82)
Hero owning the glassrgba(39,39,39,.8775)blur(28.28px) saturate(169%)rgba(39,39,39,.96)
Two glass layers never equal one — the double-glass trap. The nav is a separate fixed layer, so its backdrop is the video PLUS any scrim you put over that video. Give a hero the nav's own values and the nav band takes them a second time: measured over a white frame, the nav landed at rgb(64,64,64) against the hero's rgb(114,114,114). Darkening the hero cannot close the gap, because the nav always composites on top of whatever you set.

Fix by having one glass layer. Either the nav owns it and the hero carries no scrim, or the hero owns it and the nav stands down (data-yf-hero-glass="own"). The second row above is the first row applied twice, collapsed into a single layer: alpha 1-(1-.65)², blur √(20²+20²) — gaussians compose in quadrature — saturate 1.3².
Tint carries the contrast, so do not soften it. Nothing can sample a cross-origin video frame, so the tint has to survive the brightest frame the clip can show. At .65 over pure white, #fffffb holds 4.76:1; below roughly .6 it fails AA.
Drop the blur when the surface is off screen. A live backdrop-filter makes the browser re-snapshot its backdrop every scroll frame. On a 76px bar that is affordable; on a full hero it is not. Toggle it with an IntersectionObserver, and make the class an OFF switch — if the script never runs, the blur simply stays on. Failure mode should be a cost, never a wrong look.

Background video

.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.

The 992–1919 tier

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 */ } }
Keep min-width:992px on that query. Without it the block outranks the Webflow tablet styles and the whole tier ladder inverts. Scope it to the section class too, so the shared Nav, Contact and Footer keep their own tiers.

07

Motion

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.

UseDurationCurveProperties
Button hover220mseasebackground-color, border-color, color
Arrow nudge260mscubic-bezier(.2,.7,.2,1)transform: translateX(3px)
Row / dot state140mseasebackground-color, transform: scale(1.25)
Tile / card content0.45scubic-bezier(0.4,0,0.2,1)color, border-color, filter
Image zoom on card0.6seasetransform: scale(1.12)
Nav ground change420mseasebackground-color, border-bottom-color
Nav glass blur hold460ms—Delay before the blur is dropped on exit
Disclosure arrow200mseasetransform: rotate(180deg) — see §15
Marquee loop30slinear infinitekeyframe 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.

Nav type does not cross-fade with its ground. Fading both at once passes them through each other: measured, every synchronised fade dropped the nav labels to about 1.0–1.1:1 against their own background for roughly 100ms. The script instead rides the painted background each frame and flips the palette the instant its luminance crosses the threshold. Worst case is 1.93:1 for a single frame. Apply the same reasoning to any element that changes ground and text together.
Every motion rule needs a prefers-reduced-motion: reduce branch that sets transition: none and clears any transform. This is already true across the button system; keep it true. The 30s marquee in §17 is an animation, not a transition — it needs its own branch setting animation: none.
Hover lives in site-wide CSS, not Webflow hover states. A button must move its label AND its arrow together, which needs a descendant selector Webflow's hover panel can't express. Consequence: hover never previews on the Designer canvas. That is expected — test hover on a published URL, never in Preview.
Gate hover on @media (hover:hover). Without it a tap on a touch device leaves the hover state stuck on until something else is tapped.

08

Exceptions & traps

Documented deviations, and the failure modes that have already cost real time. Read this before debugging anything visual.

Live exceptions

WhereDeviationWhy
.yfabout-btnNo hover lift, unlike every other buttonA 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-fillCarries a 1px transparent borderMatches the Outline variant's box so the two buttons are the same height
Project card chipsHidden below 768px (Home) / 480px (Work)Card is narrower than one chip at those widths
Work card chipsCapped at 3 by CSS, not by collection limitThe filter script reads every nested item's text; a collection limit would silently drop solutions from the filter
.image-71Carried a 1px Brand/Light border on all four sides — removedEvery 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 counterFont-size measured and scaled during count"999,999" needs 366px at desktop against a 211px card — six digits cannot fit the Numbers scale
Approach heroOwns its glass; nav stands down over itTwo glass layers never equal one — see §6
Chip labelsNot uppercase, weight 400 — unlike all other Meta typeSolution names are proper nouns — see §10
.tactic-cardThe only box-shadow on the siteLegacy. Separation elsewhere is a 1px rule — see §9
.sg-fontloadAn invisible element on this guide page onlyForces Webflow to bundle all four families on a page built from embeds — see §2

Traps

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.

Open debt — broken

· 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.

Open debt — duplication

· 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.

Cleared

· 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

Cards & tiles

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.

Which family to use

FamilyStatusUse for
.yfsolp-cardCanonicalSolution cards. The reference implementation — fully token-bound, needs no breakpoint overrides.
.yfins-cardCurrentInsights / article grid cells.
.yfsol-tactic-cardCurrentTactic cards inside a solution page.
.yf-sol-card-top / -mid / -bottomHalf-migratedSlot classes dropped into a legacy .sol-card. New yf- prefix but hardcoded 3.5rem type. Do not copy.
.work-card*LegacyWork grid tiles. Frozen.
.sol-card*LegacyOld solutions grid. Frozen.
.tac-card / .tactic-cardLegacyPartnership/careers drafts. Frozen.
.partner-card / .member-card / .proj-card / .project-cardLegacyOne-off surfaces. Frozen.

Canonical anatomy

.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.

Real values, current families

ClassdisplaygapOther
.yfsolp-cardflex column--padding--section--gapmin-height 41.875rem
.yfsolp-cardsflex column--padding--section--gap-lg—
.yfsolp-desc——max-width 31.25rem, Inter, --typography--size--text-md, lh 1.5
.yfins-cardflex column--padding--section--gap-smmin-width 0
.yfins-bodyflex column--padding--section--gap-xs—
.yfins-media——aspect-ratio 469/307, overflow hidden
.yfsol-tactic-cardflex column--padding--section--gap-xspadding same token, bg --backgrounds--pure-white

Breakpoints that actually exist

· .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.

.tactic-card is the only card on the site with a box-shadow (0 2px 5px rgba(0,0,0,.2)). Nothing else does. Do not add a second one — separation on this site is a 1px --lines--separator rule, not a shadow. See the shadow trap in §8 before ever hand-writing one.
Four legacy cards paint from variables that no longer exist. .work-card-image, .tac-card, .partner-card-top and .card-tactics-item each set a background bound to a deleted variable ID, so the declaration is invalid and the ground silently falls through to whatever is behind it. Listed in §8 open debt.
Dead combos. .sol-card.sol-card--vert and .sol-card.sol-card--horiz are empty combo classes duplicating the global .sol-card--vert / --horiz that actually carry the flex-direction. .member-card ships a baked-in @img_example_bg placeholder. .proj-card carries grid properties under display:flex that do nothing.

10

Chips & tags

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.

Canonical anatomy

.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
The dot has no colour in CSS, and that is deliberate. .yfsolp-chip-dot sets a 1px border and a 50% radius but neither border-color nor background-color. The per-solution colour is applied at runtime from the solution token set — --solutions--brand #ffc62a, --solutions--crossborder #ef81e8, --solutions--content #ff5a4d, --solutions--event #72d95a, --solutions--digital #667cff, --solutions--campaign #a65bef, --solutions--operation #61dcc7. If a dot renders invisible, the script did not run — do not hardcode a colour onto the class, it would apply to all seven solutions at once.

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.

Legacy pills — do not extend

ClassShapeWhy it is legacy
.solstack-pillradius 1.875rem, bg --brand--dark, uppercase labelToken-bound but a different shape language from the current chip
.yfprj-taginline-flex, radius 30px, 1px --lines--borderHardcoded 30px radius and padding
.sol-tag · .sol-tag-1 · .sol-tag-1-2 · .sol-tag-1-2-3radius 20px, 10–11px typeFour Webflow paste-duplicates of one class. Delete three.
.work-tag · .insights-tagradius 20px, rgba bordersNo variable bindings at all; .insights-tag adds a lone backdrop-filter: blur(4px)
.TacticPillheight 2.6875rem, radius 62.4375rem, 1px blackPascalCase — the oldest naming layer
.filter_tags_itemradius 0.25rem, background from a deleted variableFinsweet snake_case; hidden entirely at medium (§12)

11

Forms & inputs

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.

Anatomy

.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

Field

PropertyValue
width / height100% / auto
padding0.75rem all sides
border1px solid --lines--separator on all four sides
border-radius0 — fields are square, buttons are pills
backgroundtransparent
colour--brand--light
typeSometype Mono, --typography--size--meta, lh 1.25, weight 400
textarea combo.yfcon-field-area — height and min-height 9.9375rem, resize: vertical
The contact form has no focus state, and that is a live accessibility defect. .yfcon-field and .yfcon-submit define no :focus, :focus-visible, :hover or ::placeholder rule. A keyboard user tabbing the form gets whatever the UA outline survives Webflow's normalize — on a styled input, usually nothing. Fix by adding :focus-visible with a 2px --brand--y outline at 2px offset, matching the one focus style that already exists on the site (.fs_accordion-1_header:focus-visible). Until then, treat every new interactive element as needing its own focus rule — there is no site-wide default to inherit.
Validation states are unstyled. .form-message-error sets only margin and padding; .form-message-success sets only padding. Neither sets colour, background or border, so both fall back to Webflow's default pale red and green blocks — against the --brand--dark #272727 contact section. There is no yfcon- equivalent at all. Any work on the contact form should add one.

Submit button

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.

Which system is which

SetStatusWhere
.yfcon-*CurrentContact
.checkbox_* .radio_*Off-system but liveFilter controls on Work / Solutions listings — functionally current, naming is not
.form-*LegacyWebflow starter residue — Dump, Password, Finsweet styleguide
.tac-form .tac-input .tac-submitLegacyPartnership / Careers drafts. Carries dangling variables.
.fs-checkbox-5_* .fs-selectcustom_*Legacy demoHardcoded #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

CMS filter UI

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.

Skeleton

.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

Controls

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).

Active filters become invisible below 992px. .filter_tags is display:none at medium, and .filter_form_block is too — so on tablet and phone a user can apply filters through the mobile overlay but has no on-screen indication of what is currently applied, and no way to remove one individually. The desktop chips are the only affordance. This is a real usability bug, not a styling choice.
Mobile filtering is a separate overlay. At medium, .filter-menu becomes a fixed 100vw × 100vh scrolling panel on --backgrounds--pure-white, .filter-mobile flips from none to block, and .filter_form_block-mobile goes 18rem → 100vw with 6rem top padding. Both .filter-menu and .filter-menu-button have no properties at all at desktop — they exist only inside the medium breakpoint. Editing them on the desktop canvas does nothing visible; switch to tablet first.
Six filter properties paint from deleted variables. .filter_form background, .filter_block_header background, .filter_tags_item background, .filter_search bottom border, .filter_dropdown_toggle:hover and .filter_clear-all:hover backgrounds all reference variable IDs that no longer exist in the Base collection. The declarations are invalid and render as nothing — which is why the filter panel looks flatter than it was designed to. Listed in §8 open debt.
.filter_tags_remove has no visible affordance. It is a 1.4rem absolutely-positioned box with a radius and a pointer cursor — no colour, no glyph style, no focus state, and it is not a button element. Any rework should make it a real <button> with an accessible name.

14

Stat counters

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.

Canonical anatomy

.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.

Breakpoints

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.

impactnumbersholder_* is a byte-identical copy of fs-numbercount-4_*. Two full component class sets doing the same job, differing only in that the Finsweet original binds --padding--section--gap-sm for its gaps and adds a top border, while the copy hardcodes 1rem. Pick one and delete the other; do not add a fifth counter.
The one-off numbers — .counter · .counter-label · .counter-wrapper · .counters · .acct-number · .adv-number · .GridNumber · .HighlightedNumber · .fs_numbercount-1_* — are legacy. .GridNumber is empty; .counter-label and .fs-numbercount_number paint from deleted variables; .fs_numbercount-1_* still references @swatch_ literals from a previous colour system.
Empty combos. .yfapp-num.yfapp-num-onlight and .yfapp-num.yfapp-num-ondark carry no properties — the colour lives on the global .yfapp-num-onlight / -ondark classes instead. Same shape as the dead .sol-card--vert combos in §9. When a combo looks empty, check for a global of the same name before assuming the styling is missing.
The 1M counter exception — font-size measured and rescaled during the count because six digits cannot fit the Numbers scale — is documented in §8.

15

Accordions & disclosure

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.

Open and closed are not expressed in CSS. Every panel on this site — .fs_accordion-1_content, .faq2_answer, .tac-faq-item — sets nothing but overflow: hidden. There is no max-height, no height, no display rule for either state. Panel height is written at runtime by Webflow IX2 or Finsweet's script. The only styled state anywhere is the arrow: .is-active-accordion → transform: rotate(180deg), eased by transition: transform 200ms ease on the base wrapper. Consequence: a panel that will not open is a script problem, never a CSS problem — do not go looking for a missing height.

The four sets

SetStatusNotes
.yfapp-sol-toggle / -arrow / -plusCurrent--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_*TransitionalToken-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_*LegacyDuplicated 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*LegacyDraft pages only. Paints from two deleted variables and a @swatch_ literal. .faq-body is empty.
Keyboard state is missing on the current toggle. The only :focus-visible rule in this whole subsystem is on the legacy .fs_accordion-1_header (2px outline, 4px offset). .yfapp-sol-toggle, .faq2_question and .tac-faq-q have none — and all three are click targets. Any disclosure control added from here on needs a focus rule written with it; see the same finding for form fields in §11.

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

Media & imagery

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;

The ratio register

Thirty-one classes set an aspect-ratio. Before inventing a new one, use the ratio that already means what you want.

RatioMeansClasses
1880 / 1080Full-bleed page banner.yfprj-banner-img · .yfsol-hero-img · .yfsol-hero-media
16 / 9Gallery image.yfprj-gallery-img
469 / 307Insights card media.yfins-media
749 / 430Solution card media.yfsolp-media-img
569 / 321Solution stack media.solstack-media-frame
4 / 3Insights thumbnail.insights-img-wrap
4 / 5Portrait photo.yfabout-photo
2 / 3Tall tile.work-card-wrapper (small: 1/1) · .member-card
3 / 2Landscape tile.counter · .project-card.related
1 / 1Logo, 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.

.yfhero-vidfit is missing its centring transform. It sets left:50%; top:50% with cover-sizing (width:100vw; height:56.25vw; min-height:100vh; min-width:177.78vh) but no transform: translate(-50%,-50%) — so the video sits down and to the right unless the page's own CSS supplies it. Check the hero embed before adding one, then fix it in one place.
.yfprj-film collapses on its own. It sets position:relative; overflow:hidden; width:100% and no ratio or height, so it has zero height without an intrinsically-sized child. Give it a ratio from the register above when it is next touched.
Legacy media patterns — do not copy. .card-image-front / -back are background-image divs still pointing at @img_example_bg placeholders (-back also carries mix-blend-mode: luminosity — see the blend-mode trap in §8). .level-image-wrapper uses the pre-aspect-ratio padding hack (height:0; padding-top:33%; padding-bottom:33%). The numbered Image NN classes carry bare ratios with no frame.

17

Rails, marquees & sliders

Three unrelated mechanisms share this vocabulary. Knowing which one you are looking at saves the most time here.

1 · CSS marquee — the only self-contained one

.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.

2 · "Rails" that are not rails

.yfval-rail and .yfapp-fw-rail contain no horizontal-scroll CSS whatsoever. .yfval-rail is a plain vertical flex column with a --padding--section--gap row-gap; .yfapp-fw-rail is display:none outright. Whatever horizontal motion these produce is entirely script-driven. Do not try to fix a rail's behaviour in the Designer — and do not assume the name describes the CSS.

.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.

3 · Webflow slider and the range control

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.

Empty combos here: .yfapp-sec.yfapp-sec-dark.yfapp-fw and .yfapp-h3.yfapp-fw-title. .who-marquee-outer-copy is a stray duplicate.

18

Spacing & utility scale

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.

This scale is not stock Client-First. Every step from medium upward is one notch larger than the shipped defaults, and a tiny step was added locally. Anyone who knows Client-First from another project will guess wrong. Read the table.

padding-* and margin-* (identical scales)

StepBaseTabletMobile
00——
tiny0.125rem——
xxsmall0.25rem——
xsmall0.5rem——
small1rem——
medium2rem1.5rem1.25rem
large3rem2.5rem1.5rem
xlarge4rem3rem2rem
xxlarge5rem4rem3rem
huge6rem5rem3.5rem
xhuge8rem6rem4rem
xxhuge12rem8rem4.5rem

The other scales

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.

Four utilities are broken and must not be used. .hide-mobile-portrait has no properties at all. .show-mobile-portrait is display:none at base with no reveal at any breakpoint — it hides forever. .max-width-full-mobile-portrait and .text-style-2lines are empty. .text-size-medium.text-weight-bold overrides weight back to 400 and swaps the family to Archivo — it does the opposite of its name.
What is actually used. Only about twenty of these carry a real attachment: padding-vertical (16 uses), margin-bottom (22), margin-xhuge (12), padding-xhuge (6), padding-xxhuge (5), and single-digit uses of medium/large/xlarge/xxlarge/huge. Everything else — the whole max-width-* set, overflow-*, background-color-*, text-color-*, most of text-size-* — is unattached residue, safe to sweep alongside the fs-styleguide_* set it arrived with.
Rule of thumb. On a new page, bind to a §1 variable. Reach for a utility only to reproduce spacing on an existing page that already uses them — and never mix the two on the same element, or the value stops being traceable to either system. .page-wrapper is empty, and .yf-page-padding is a straight duplicate of .page-padding; neither is worth keeping.