{"id":"l-ds-5-2","courseId":"design-systems","lang":"en","kind":"lesson","summary":"Add a second brand to a design system with a small brand tier between primitives and semantic tokens, verify contrast per brand, and decide what brands may and may not change.","html":"<p>Eight months after Northwind acquired Tidewater, its marketing site moved onto Northwind UI. Tidewater’s brand team arrived with a 60-page brand book, a serif typeface, a teal palette and rounded pill buttons. They also arrived with a list of 23 “brand requirements” for components. Our job was to say yes to the brand and no to most of the list, and to make both feel reasonable.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">What a brand may change</h2>\n<p>The first conversation set a boundary. A brand changes <strong>personality</strong>: color palette, typefaces, corner radius, illustration style and voice. A brand doesn’t change <strong>behavior or structure</strong>: keyboard handling, focus management, spacing scale, layout grids, component APIs, accessibility. Tidewater’s buttons can be teal pills with a serif label. They can’t close dialogs on outside click when Northwind’s don’t, because that’s a product decision, not a brand one.</p>\n<p>Of the 23 requirements, 15 turned out to be visual and fit into tokens. Six were behavior requests, which went through API review as ordinary proposals for both brands (two were accepted, for both). Two were dropped.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">The brand tier</h2>\n<p>Section 2 introduced a brand ramp so dark mode could pick steps. For a full second brand, that ramp grows into its own tier between primitives and semantic tokens:</p>\n<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 660 230\" role=\"img\" aria-labelledby=\"l-ds-5-2-t1\"><defs><marker id=\"arrow-l-ds-5-2-1\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"7\" markerHeight=\"7\" orient=\"auto-start-reverse\"><path d=\"M0 0 L10 5 L0 10 z\" class=\"d-arrowhead\"/></marker></defs>\n  <title id=\"l-ds-5-2-t1\">Primitives include blue and teal palettes and two font families. The Northwind brand tier points brand tokens at blue and Inter; the Tidewater brand tier points them at teal and Georgia. Semantic tokens point at brand tokens, and components point at semantic tokens.</title>\n  <rect class=\"d-box\" x=\"20\" y=\"20\" width=\"140\" height=\"190\" rx=\"10\"/>\n  <text class=\"d-label-strong\" x=\"90\" y=\"46\" text-anchor=\"middle\">Primitives</text>\n  <text class=\"d-code\" x=\"90\" y=\"80\" text-anchor=\"middle\">blue-*</text>\n  <text class=\"d-code\" x=\"90\" y=\"106\" text-anchor=\"middle\">teal-*</text>\n  <text class=\"d-code\" x=\"90\" y=\"132\" text-anchor=\"middle\">inter</text>\n  <text class=\"d-code\" x=\"90\" y=\"158\" text-anchor=\"middle\">georgia</text>\n  <rect class=\"d-box-primary\" x=\"200\" y=\"20\" width=\"160\" height=\"84\" rx=\"10\"/>\n  <text class=\"d-label-strong\" x=\"280\" y=\"48\" text-anchor=\"middle\">Brand: Northwind</text>\n  <text class=\"d-code\" x=\"280\" y=\"78\" text-anchor=\"middle\">brand-600 → blue</text>\n  <rect class=\"d-box-success\" x=\"200\" y=\"126\" width=\"160\" height=\"84\" rx=\"10\"/>\n  <text class=\"d-label-strong\" x=\"280\" y=\"154\" text-anchor=\"middle\">Brand: Tidewater</text>\n  <text class=\"d-code\" x=\"280\" y=\"184\" text-anchor=\"middle\">brand-600 → teal</text>\n  <rect class=\"d-box-accent\" x=\"400\" y=\"80\" width=\"120\" height=\"70\" rx=\"10\"/>\n  <text class=\"d-label\" x=\"460\" y=\"120\" text-anchor=\"middle\">Semantic</text>\n  <rect class=\"d-box\" x=\"550\" y=\"80\" width=\"100\" height=\"70\" rx=\"10\"/>\n  <text class=\"d-label\" x=\"600\" y=\"120\" text-anchor=\"middle\">Components</text>\n  <path class=\"d-arrow\" d=\"M200 62 L164 90\" marker-end=\"url(#arrow-l-ds-5-2-1)\"/>\n  <path class=\"d-arrow\" d=\"M200 168 L164 140\" marker-end=\"url(#arrow-l-ds-5-2-1)\"/>\n  <path class=\"d-arrow\" d=\"M400 105 L364 70\" marker-end=\"url(#arrow-l-ds-5-2-1)\"/>\n  <path class=\"d-arrow\" d=\"M400 125 L364 160\" marker-end=\"url(#arrow-l-ds-5-2-1)\"/>\n  <path class=\"d-arrow\" d=\"M550 115 L524 115\" marker-end=\"url(#arrow-l-ds-5-2-1)\"/>\n</svg><figcaption>The brand tier sits between primitives and semantic tokens</figcaption></figure>\n<p>The brand tier is deliberately small. Northwind’s has 24 tokens: a color ramp from 50 to 900, a heading and a body font family, two radii and a focus-ring color. Semantic tokens point at brand tokens, and components never know a brand exists.</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"css\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">CSS</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">:root</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">[</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">data-brand</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">] {</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">  --nw-color-action-bg</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">var</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">--nw-brand-600</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">  --nw-color-action-text</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">var</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">--nw-white</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">  --nw-font-heading</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">var</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">--nw-brand-font-heading</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">  --nw-radius-control</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">var</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">--nw-brand-radius-control</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">[</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">data-brand</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"tidewater\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">] {</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">  --nw-brand-600</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">var</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">--nw-teal-600</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">  --nw-brand-700</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">var</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">--nw-teal-700</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">  --nw-brand-font-heading</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">Georgia</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'Times New Roman'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">serif</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">  --nw-brand-radius-control</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">999</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">px</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\">  /* Tidewater's 600 fails contrast with white text: use the darker step. */</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">  --nw-color-action-bg</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">var</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">--nw-brand-700</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>The selector <code dir=\"ltr\">:root, [data-brand]</code> matters. Remember from the themes lesson that a custom property containing <code dir=\"ltr\">var()</code> is resolved where it is declared. Declared only on <code dir=\"ltr\">:root</code>, <code dir=\"ltr\">--nw-color-action-bg</code> would be computed once with Northwind’s ramp, and a Tidewater section on the same page would inherit Northwind blue. Re-declaring the semantic mappings on every <code dir=\"ltr\">[data-brand]</code> element makes them resolve inside each brand’s scope. That’s what lets Northwind’s co-branded comparison page show both brands side by side.</p>\n<h2 id=\"sec-3\" tabindex=\"-1\">Contrast is per brand</h2>\n<p>The last line of the Tidewater block is the interesting one. Northwind’s action color is step 600 of its ramp. Tidewater’s 600 is lighter, so white button text on it fails:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"js\" data-run=\"js\" data-index=\"1\"><div class=\"code-head\"><span class=\"code-title\">JavaScript</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn code-run\" data-action=\"run\" data-lang=\"js\" data-index=\"1\" aria-label=\"Run this example\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z\"/></svg><span>Run</span></button><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> luminance</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">hex</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  const</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> [</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">r</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">g</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">b</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">] </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> [</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">3</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">5</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">].</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">map</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">((</span><span style=\"color:#953800;--shiki-dark:#FFA657\">i</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> parseInt</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(hex.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">slice</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(i, i </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">+</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 2</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">), </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">16</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">/</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 255</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    .</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">map</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">((</span><span style=\"color:#953800;--shiki-dark:#FFA657\">v</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (v </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">&#x3C;=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 0.04045</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> ?</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> v </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">/</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 12.92</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> :</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> ((v </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">+</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 0.055</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">/</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 1.055</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">**</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 2.4</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">));</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  return</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 0.2126</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> *</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> r </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">+</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 0.7152</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> *</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> g </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">+</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 0.0722</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> *</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> b;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> contrast</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">a</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">b</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  const</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> [</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">hi</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">lo</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">] </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> [</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">luminance</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(a), </span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">luminance</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(b)].</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">sort</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">((</span><span style=\"color:#953800;--shiki-dark:#FFA657\">x</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">y</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> y </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">-</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> x);</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  return</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (hi </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">+</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 0.05</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">/</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (lo </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">+</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 0.05</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> pairs</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'northwind blue-600'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'#1f5ae0'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'tidewater teal-600'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'#0d9488'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'tidewater teal-700'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'#0f766e'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> };</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">for</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> [</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">name</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">bg</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">] </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">of</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> Object.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">entries</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(pairs)) {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> ratio</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> contrast</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(bg, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'#ffffff'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  console.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">log</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">`${</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">name</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">} + white text: ${</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">ratio</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">toFixed</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">2</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">)</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">}:1 ${</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">ratio</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> >=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 4.5</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> ?</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'pass'</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> :</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'FAIL'}`</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre><div class=\"run-output\" data-run-output=\"1\" hidden></div></div>\n<p>So Tidewater maps the same semantic role to a different step. This is why the brand tier doesn’t just swap palettes blindly: each brand’s semantic mapping is designed and checked. The contrast check in Northwind’s pipeline runs every documented pair in every brand and theme, and it caught this before the brand team saw a single button.</p>\n<aside class=\"callout callout-mistake\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3\"/><path d=\"M12 9v4\"/><path d=\"M12 17h.01\"/></svg><span><span class=\"callout-kind\">Common mistake</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>Brand logic inside components</span></p>\n<p>The first Tidewater pull request added <code dir=\"ltr\">if (brand === 'tidewater')</code> to the Button. Within a month, five components had brand conditionals, and nobody could add a third brand without touching all of them. If a difference can’t be expressed as a token, question whether it’s really a brand difference.</p>\n</aside>\n<h2 id=\"sec-4\" tabindex=\"-1\">Delivering brands</h2>\n<p>Each surface loads what it needs. The marketing sites set <code dir=\"ltr\">data-brand</code> on <code dir=\"ltr\">&lt;html&gt;</code> and load only their brand’s CSS file, generated by the pipeline from <code dir=\"ltr\">primitives</code> + <code dir=\"ltr\">brand-tidewater</code> + <code dir=\"ltr\">semantic</code>. The dispatch app, which shows both brands in its admin views, loads both blocks. In Figma, the same structure lives in extended collections, so Tidewater’s designers see the shared semantic variables with their brand’s values.</p>\n<p>Every brand also doubles the test matrix. With two brands, two color schemes and two densities, each core component renders in eight combinations in the visual suite from Section 4. That sounds expensive until you remember the alternative: a Tidewater designer discovering in production that the dark-mode focus ring is invisible on teal. Machines are cheap reviewers for combinations; humans only look at the diffs.</p>\n<aside class=\"callout callout-tip\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5\"/><path d=\"M9 18h6\"/><path d=\"M10 22h4\"/></svg><span><span class=\"callout-kind\">Tip</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>Budget the brand surface</span></p>\n<p>Northwind reviews any request to grow the brand tier beyond its 24 tokens. Each new brand token multiplies the combinations you test. A small, fixed brand surface is what makes adding a third brand a two-week job instead of a quarter.</p>\n</aside>\n<p>In the exercise, you add Tidewater to a page where both brands appear side by side. Next, you measure whether all of this is actually being used.</p>\n","toc":[{"id":"sec-1","text":"What a brand may change","level":2},{"id":"sec-2","text":"The brand tier","level":2},{"id":"sec-3","text":"Contrast is per brand","level":2},{"id":"sec-4","text":"Delivering brands","level":2}],"takeaways":["A brand changes personality (color ramp, typefaces, shape) through a small set of brand tokens; behavior, spacing and accessibility stay shared.","Put a brand tier between primitives and semantic tokens, so each brand points the same semantic names at its own palette.","Contrast must be checked per brand, and a brand may need a different step of its ramp for the same semantic role.","When brands can appear side by side, re-declare semantic mappings under the brand selector so they resolve inside each brand’s scope."],"further":[{"title":"Understanding SC 1.4.3 Contrast (Minimum) (W3C)","url":"https://www.w3.org/WAI/WCAG22/Understanding/contrast-minimum.html"},{"title":"Extend a variable collection (Figma)","url":"https://help.figma.com/hc/en-us/articles/36346281624471-Extend-a-variable-collection"},{"title":"Using CSS custom properties (MDN)","url":"https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Cascading_variables/Using_custom_properties"}],"quiz":[{"q":"<p>Tidewater’s brand team asks for its Dialog to close when people click outside, while Northwind’s doesn’t. How should the system respond?</p>","options":[{"text":"Add a <code dir=\"ltr\">brand</code> prop to Dialog that switches the behavior.","why":"Brand-specific behavior inside components means every component must know about every brand, which doesn’t scale past two."},{"text":"Fork Dialog for Tidewater.","why":"Two Dialogs drift apart in fixes and accessibility, which is the cost the system exists to avoid."},{"text":"Treat it as a product behavior question for both brands, decided in API review; brands change look and voice, not behavior.","why":"Correct. If outside-click closing is right, it’s right for both brands; if not, it’s wrong for both."},{"text":"Let Tidewater override the Dialog’s JavaScript with a theme token.","why":"Tokens describe visual values; behavior hidden in tokens is untestable and surprising."}],"answer":2},{"q":"<p>Tidewater’s teal-600 on white text has a contrast ratio of 3.74:1. What should the Tidewater mapping for <code dir=\"ltr\">--nw-color-action-bg</code> be?</p>","options":[{"text":"Keep teal-600; brand colors are exempt from contrast rules.","why":"WCAG makes no brand exception; button text on that background must reach 4.5:1 at normal size."},{"text":"Point it at teal-700 (5.47:1 with white text) in the Tidewater brand block.","why":"Correct. Same semantic role, a darker step of the brand’s own ramp, and the button text passes."},{"text":"Make the button text bold so 3.74:1 is enough.","why":"The 3:1 threshold applies to large text (about 18.66px bold or 24px regular); a normal bold button label still needs 4.5:1."},{"text":"Use Northwind blue for Tidewater buttons.","why":"That passes contrast but drops Tidewater’s identity from its most visible element."}],"answer":1},{"q":"<p>On the co-branded comparison page, a Tidewater section’s buttons are still Northwind blue, although <code dir=\"ltr\">[data-brand=&quot;tidewater&quot;]</code> sets <code dir=\"ltr\">--nw-brand-600</code> correctly. What’s missing?</p>","options":[{"text":"The semantic token <code dir=\"ltr\">--nw-color-action-bg</code> is only declared on <code dir=\"ltr\">:root</code>, so it was resolved with Northwind’s ramp at the root.","why":"Correct. Re-declaring the semantic mappings under <code dir=\"ltr\">[data-brand]</code> makes them resolve inside each brand’s section."},{"text":"<code dir=\"ltr\">data-brand</code> attributes only work on <code dir=\"ltr\">&lt;html&gt;</code>.","why":"Attribute selectors match any element; the problem is where the semantic token is computed."},{"text":"The button needs a <code dir=\"ltr\">.tidewater</code> class.","why":"Brand-aware classes on components are exactly what the token tiers avoid."},{"text":"Custom properties can’t be overridden twice on one page.","why":"Custom properties can be redeclared at any level; each element sees its nearest declaration."}],"answer":0}],"exercise":{"kind":"web","mode":"dom","starter":{"html":"<div class=\"compare\">\n  <section data-brand=\"northwind\" id=\"nw\">\n    <h2 class=\"nw-heading\">Northwind Dispatch</h2>\n    <button class=\"nw-button\" type=\"button\">Start trial</button>\n  </section>\n  <section data-brand=\"tidewater\" id=\"tw\">\n    <h2 class=\"nw-heading\">Tidewater Freight</h2>\n    <button class=\"nw-button\" type=\"button\">Get a quote</button>\n  </section>\n</div>","css":":root {\n  /* Primitives */\n  --nw-white: #ffffff;\n  --nw-blue-600: #1f5ae0;\n  --nw-blue-700: #1848b8;\n  --nw-teal-600: #0d9488;\n  --nw-teal-700: #0f766e;\n  --nw-font-inter: Inter, system-ui, sans-serif;\n  --nw-font-georgia: Georgia, 'Times New Roman', serif;\n\n  /* Brand tier: Northwind is the default */\n  --nw-brand-600: var(--nw-blue-600);\n  --nw-brand-700: var(--nw-blue-700);\n  --nw-brand-font-heading: var(--nw-font-inter);\n  --nw-brand-radius-control: 6px;\n}\n\n/* Semantic tier */\n:root {\n  --nw-color-action-bg: var(--nw-brand-600);\n  --nw-color-action-text: var(--nw-white);\n  --nw-font-heading: var(--nw-brand-font-heading);\n  --nw-radius-control: var(--nw-brand-radius-control);\n}\n\n/* Add the Tidewater brand here */\n\n/* Components: brand-agnostic, don't change */\n.nw-heading { font-family: var(--nw-font-heading); margin: 0 0 12px; }\n.nw-button {\n  --nw-button-bg: var(--nw-color-action-bg);\n  --nw-button-text: var(--nw-color-action-text);\n  background: var(--nw-button-bg);\n  color: var(--nw-button-text);\n  border: 0;\n  border-radius: var(--nw-radius-control);\n  padding: 8px 16px;\n  font: inherit;\n}\n.compare { display: grid; gap: 24px; }\n","js":""},"solution":{"html":"<div class=\"compare\">\n  <section data-brand=\"northwind\" id=\"nw\">\n    <h2 class=\"nw-heading\">Northwind Dispatch</h2>\n    <button class=\"nw-button\" type=\"button\">Start trial</button>\n  </section>\n  <section data-brand=\"tidewater\" id=\"tw\">\n    <h2 class=\"nw-heading\">Tidewater Freight</h2>\n    <button class=\"nw-button\" type=\"button\">Get a quote</button>\n  </section>\n</div>","css":":root {\n  /* Primitives */\n  --nw-white: #ffffff;\n  --nw-blue-600: #1f5ae0;\n  --nw-blue-700: #1848b8;\n  --nw-teal-600: #0d9488;\n  --nw-teal-700: #0f766e;\n  --nw-font-inter: Inter, system-ui, sans-serif;\n  --nw-font-georgia: Georgia, 'Times New Roman', serif;\n\n  /* Brand tier: Northwind is the default */\n  --nw-brand-600: var(--nw-blue-600);\n  --nw-brand-700: var(--nw-blue-700);\n  --nw-brand-font-heading: var(--nw-font-inter);\n  --nw-brand-radius-control: 6px;\n}\n\n/* Semantic tier, re-declared in every brand scope */\n:root,\n[data-brand] {\n  --nw-color-action-bg: var(--nw-brand-600);\n  --nw-color-action-text: var(--nw-white);\n  --nw-font-heading: var(--nw-brand-font-heading);\n  --nw-radius-control: var(--nw-brand-radius-control);\n}\n\n[data-brand=\"tidewater\"] {\n  --nw-brand-600: var(--nw-teal-600);\n  --nw-brand-700: var(--nw-teal-700);\n  --nw-brand-font-heading: var(--nw-font-georgia);\n  --nw-brand-radius-control: 999px;\n  /* teal-600 fails 4.5:1 with white text */\n  --nw-color-action-bg: var(--nw-brand-700);\n}\n\n/* Components: brand-agnostic, don't change */\n.nw-heading { font-family: var(--nw-font-heading); margin: 0 0 12px; }\n.nw-button {\n  --nw-button-bg: var(--nw-color-action-bg);\n  --nw-button-text: var(--nw-color-action-text);\n  background: var(--nw-button-bg);\n  color: var(--nw-button-text);\n  border: 0;\n  border-radius: var(--nw-radius-control);\n  padding: 8px 16px;\n  font: inherit;\n}\n.compare { display: grid; gap: 24px; }\n","js":""},"checks":[{"id":"northwind-intact","test":"const b = $('#nw .nw-button'); return style(b, 'background-color') === 'rgb(31, 90, 224)' && style(b, 'border-top-left-radius') === '6px' && style('#nw .nw-heading', 'font-family').startsWith('Inter');"},{"id":"tidewater-font","test":"return style('#tw .nw-heading', 'font-family').startsWith('Georgia');"},{"id":"tidewater-shape","test":"return style('#tw .nw-button', 'border-top-left-radius') === '999px';"},{"id":"tidewater-contrast","test":"const b = $('#tw .nw-button'); return style(b, 'background-color') === 'rgb(15, 118, 110)' && style(b, 'color') === 'rgb(255, 255, 255)';"},{"id":"no-brand-in-components","test":"const rules = [...document.styleSheets].flatMap((s) => [...s.cssRules]).filter((r) => r.selectorText); return rules.every((r) => !(/tidewater|data-brand/.test(r.selectorText) && /nw-(button|heading)/.test(r.selectorText)));"}],"title":"Add Tidewater to the comparison page","prompt":"<p>The co-branded comparison page shows both brands side by side, each in a section with its own <code dir=\"ltr\">data-brand</code>. Add the Tidewater brand <strong>without touching the component rules</strong>:</p>\n<ul>\n<li>headings use the Georgia font stack (<code dir=\"ltr\">--nw-font-georgia</code>),</li>\n<li>buttons are pills (<code dir=\"ltr\">999px</code> radius),</li>\n<li>the button background is <code dir=\"ltr\">--nw-teal-700</code>, because teal-600 fails contrast with white text,</li>\n<li>the Northwind section must stay exactly as it is.</li>\n</ul>\n<p>Right now the semantic tier is only declared on <code dir=\"ltr\">:root</code>. Think about where it gets resolved.</p>\n","hints":["Write a <code dir=\"ltr\">[data-brand=&quot;tidewater&quot;] { … }</code> block that points the brand tokens (<code dir=\"ltr\">--nw-brand-font-heading</code>, <code dir=\"ltr\">--nw-brand-radius-control</code> and the ramp) at Tidewater values.","Brand tokens alone won’t reach the section: the semantic tokens were computed on <code dir=\"ltr\">:root</code> with Northwind’s values. Change the semantic rule’s selector to <code dir=\"ltr\">:root, [data-brand]</code>.","Inside the Tidewater block, also map <code dir=\"ltr\">--nw-color-action-bg</code> to <code dir=\"ltr\">var(--nw-brand-700)</code>."],"explanation":"<p>The brand block only swaps brand-tier values, plus one semantic mapping that Tidewater genuinely needs for contrast. Re-declaring the semantic tier under <code dir=\"ltr\">[data-brand]</code> is what makes each section compute <code dir=\"ltr\">--nw-color-action-bg</code>, <code dir=\"ltr\">--nw-font-heading</code> and <code dir=\"ltr\">--nw-radius-control</code> from its own brand tokens, instead of inheriting values already resolved at the root. The components never mention a brand, so a third brand is one more block.</p>\n","checkLabels":{"northwind-intact":"The Northwind section keeps its blue button, 6px radius and Inter headings","tidewater-font":"Tidewater headings use Georgia","tidewater-shape":"Tidewater buttons are pills (999px radius)","tidewater-contrast":"Tidewater buttons use teal-700 with white text","no-brand-in-components":"No rule targets components by brand"}},"words":686,"readMinutes":3}