{"id":"l-css-4-4","courseId":"css-mastery","lang":"en","kind":"lesson","summary":"Organise a growing stylesheet with cascade layers, token tiers, single-class components and a small set of utilities, so a team can change CSS without fear.","html":"<p>Waypoint’s front end started with one developer and one <code dir=\"ltr\">styles.css</code>. Two years later there are five developers, forty components, a sponsor microsite that reskins the schedule, and a stylesheet nobody dares to delete from. Every bug fix is a new override, every override is a little heavier than the last, and the <code dir=\"ltr\">!important</code> count only goes up.</p>\n<p>That’s not a discipline problem. It’s an architecture problem, and everything in this course so far gives you the pieces to fix it.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">The layer stack is the architecture</h2>\n<p>Start the entry stylesheet with one statement that names every kind of CSS you have, in priority order:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"css\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">main.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:#CF222E;--shiki-dark:#FF7B72\">@layer</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> reset</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">tokens</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">base</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">layouts</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">components</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">utilities</span><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\">@import</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> url</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"reset.css\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">layer</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">reset</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">@import</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> url</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"tokens.css\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">layer</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">tokens</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">@import</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> url</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"base.css\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">layer</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">base</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">@import</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> url</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"layouts/schedule.css\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">layer</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">layouts</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">@import</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> url</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"components/session-card.css\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">layer</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">components</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">@import</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> url</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"components/speaker.css\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">layer</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">components</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">@import</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> url</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"utilities.css\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">layer</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">utilities</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span></code></pre></div>\n<p>Each layer has one job:</p>\n<div class=\"table-wrap\" role=\"region\" aria-label=\"Scrollable table\" tabindex=\"0\"><table><thead>\n<tr>\n<th>Layer</th>\n<th>Contains</th>\n<th>Example</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>reset</td>\n<td>Normalising browser defaults</td>\n<td><code dir=\"ltr\">*, *::before { box-sizing: border-box; }</code></td>\n</tr>\n<tr>\n<td>tokens</td>\n<td>Custom properties only</td>\n<td><code dir=\"ltr\">--color-brand</code>, <code dir=\"ltr\">--space-m</code></td>\n</tr>\n<tr>\n<td>base</td>\n<td>Bare element styles, low specificity</td>\n<td><code dir=\"ltr\">:where(a) { color: var(--link); }</code></td>\n</tr>\n<tr>\n<td>layouts</td>\n<td>Page and region structure</td>\n<td><code dir=\"ltr\">.schedule { display: grid; gap: … }</code></td>\n</tr>\n<tr>\n<td>components</td>\n<td>One block per component</td>\n<td><code dir=\"ltr\">.session-card { … }</code></td>\n</tr>\n<tr>\n<td>utilities</td>\n<td>Single-purpose overrides</td>\n<td><code dir=\"ltr\">.u-hidden</code>, <code dir=\"ltr\">.u-visually-hidden</code></td>\n</tr>\n</tbody>\n</table></div><p>Because layers decide conflicts <em>between</em> kinds, selectors within each layer can stay flat: one class for a component, <code dir=\"ltr\">:where()</code> for base styles. Nobody needs an ID or a three-class chain to win, so nobody writes one.</p>\n<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 680 230\" role=\"img\" aria-labelledby=\"l-css-4-4-t1\"><defs><marker id=\"arrow-l-css-4-4-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-css-4-4-t1\">Three token tiers connected by arrows: palette values such as violet 600 feed semantic tokens such as color brand, which feed component tokens such as card accent. A sponsor wrapper overrides card accent. Beside them, the layer stack from reset to utilities.</title>\n  <rect class=\"d-box\" x=\"20\" y=\"20\" width=\"200\" height=\"50\" rx=\"10\"/>\n  <text class=\"d-code\" x=\"36\" y=\"50\">--violet-600</text>\n  <path class=\"d-arrow\" d=\"M120 70 L120 96\" marker-end=\"url(#arrow-l-css-4-4-1)\"/>\n  <rect class=\"d-box-primary\" x=\"20\" y=\"100\" width=\"200\" height=\"50\" rx=\"10\"/>\n  <text class=\"d-code\" x=\"36\" y=\"130\">--color-brand</text>\n  <path class=\"d-arrow\" d=\"M120 150 L120 176\" marker-end=\"url(#arrow-l-css-4-4-1)\"/>\n  <rect class=\"d-box-accent\" x=\"20\" y=\"180\" width=\"200\" height=\"40\" rx=\"10\"/>\n  <text class=\"d-code\" x=\"36\" y=\"205\">--card-accent</text>\n  <rect class=\"d-box-warn\" x=\"250\" y=\"180\" width=\"170\" height=\"40\" rx=\"10\"/>\n  <text class=\"d-code\" x=\"262\" y=\"205\">.theme-sponsor</text>\n  <path class=\"d-arrow\" d=\"M250 200 L224 200\" marker-end=\"url(#arrow-l-css-4-4-1)\"/>\n  <text class=\"d-label-strong\" x=\"470\" y=\"24\">Layer order</text>\n  <text class=\"d-code\" x=\"470\" y=\"54\">1 reset</text>\n  <text class=\"d-code\" x=\"470\" y=\"80\">2 tokens</text>\n  <text class=\"d-code\" x=\"470\" y=\"106\">3 base</text>\n  <text class=\"d-code\" x=\"470\" y=\"132\">4 layouts</text>\n  <text class=\"d-code\" x=\"470\" y=\"158\">5 components</text>\n  <text class=\"d-code\" x=\"470\" y=\"184\">6 utilities</text>\n  <text class=\"d-label-muted\" x=\"470\" y=\"212\">later wins</text>\n</svg><figcaption>Tokens flow from raw values to roles to components; layers decide which kind of rule wins</figcaption></figure>\n<h2 id=\"sec-2\" tabindex=\"-1\">Tokens in three tiers</h2>\n<p>A flat list of 200 custom properties is as hard to maintain as 200 hex codes. Tier them:</p>\n<ol>\n<li><strong>Palette</strong>: raw values, named by what they are. <code dir=\"ltr\">--violet-600: oklch(52% 0.22 290);</code></li>\n<li><strong>Semantic</strong>: roles, named by what they’re for. <code dir=\"ltr\">--color-brand: var(--violet-600); --surface: light-dark(…);</code></li>\n<li><strong>Component</strong>: a component’s public dials, defaulting to semantic tokens. <code dir=\"ltr\">--card-accent: var(--color-brand);</code></li>\n</ol>\n<p>Keep tier 1 small and boring: a dozen hues with a handful of steps each covers most products. The interesting decisions happen in tier 2, where a designer and a developer agree on what “brand”, “surface” or “danger” means in each theme.</p>\n<p>Components only read tiers 2 and 3. Then a rebrand changes tier 1, dark mode changes tier 2, and a sponsor page changes tier 3 on a wrapper:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"css\" data-index=\"1\"><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:#CF222E;--shiki-dark:#FF7B72\">@layer</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> tokens</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<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:#953800;--shiki-dark:#FFA657\">    --color-brand</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">oklch</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">52</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">%</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 0.22</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 290</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">    --card-accent</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\">--color-brand</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 style=\"color:#0550AE;--shiki-dark:#79C0FF\">  .theme-sponsor</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">    --card-accent</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">oklch</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">55</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">%</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 0.11</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 210</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 style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>Every card inside <code dir=\"ltr\">.theme-sponsor</code> turns teal: border, badge, hover state, all of it, because they all read <code dir=\"ltr\">--card-accent</code>.</p>\n<h2 id=\"sec-3\" tabindex=\"-1\">Component rules</h2>\n<p>A handful of conventions keeps forty components predictable:</p>\n<ul>\n<li><strong>One root class</strong>, with parts nested inside it: <code dir=\"ltr\">.session-card { h3 { … } .badge { … } }</code>. Nesting stays one or two levels deep.</li>\n<li><strong>State through attributes</strong>: <code dir=\"ltr\">[aria-pressed=&quot;true&quot;]</code>, <code dir=\"ltr\">[data-state=&quot;live&quot;]</code>, <code dir=\"ltr\">:has(:checked)</code>. The attribute is also the accessible state, so CSS and assistive technology can’t disagree.</li>\n<li><strong>No outer margins on the root.</strong> The component doesn’t know where it lives.</li>\n</ul>\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>Components that space themselves</span></p>\n<p><code dir=\"ltr\">.session-card { margin-bottom: 16px; }</code> looks harmless until the card goes into a grid with <code dir=\"ltr\">gap: 24px</code> (now there’s 40px), a sidebar that needs 8px, and a carousel that needs none. Each context then overrides the margin. Let layouts own spacing with <code dir=\"ltr\">gap</code>, and components own only their insides.</p>\n</aside>\n<h2 id=\"sec-4\" tabindex=\"-1\">Utilities: few and final</h2>\n<p>Utilities are single-purpose classes the HTML can apply directly: <code dir=\"ltr\">.u-hidden</code>, <code dir=\"ltr\">.u-visually-hidden</code>, <code dir=\"ltr\">.u-flow</code> (vertical rhythm). Put them in the last layer and they win over any component without <code dir=\"ltr\">!important</code>. Keep the set small and boring. If you find yourself writing <code dir=\"ltr\">.u-mt-17</code>, you’re building a second styling language inside the first; a design-token-driven utility framework is a valid choice, but make it a deliberate one, not drift.</p>\n<h2 id=\"sec-5\" tabindex=\"-1\">Scoping with @scope</h2>\n<p>When a component must not leak styles into nested content, such as a CMS-rendered abstract inside a card, <code dir=\"ltr\">@scope</code> limits rules to a subtree and can stop at an inner boundary:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"css\" data-index=\"2\"><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:#CF222E;--shiki-dark:#FF7B72\">@scope</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (.session-card) to (.cms-content) {</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  p</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">margin-block</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0.5</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">rem</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 paragraph rule applies inside cards but not inside <code dir=\"ltr\">.cms-content</code>. <code dir=\"ltr\">@scope</code> became Baseline in March 2026, so it’s a recent tool; layers and single-class components already solve most scoping problems, and <code dir=\"ltr\">@scope</code> handles the remaining “donut” cases.</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>Enforce it with a linter</span></p>\n<p>Conventions drift without automation. Stylelint can reject ID selectors, <code dir=\"ltr\">!important</code> outside the utilities file, and nesting deeper than two levels. Add those three rules and review conversations get much shorter.</p>\n</aside>\n<h2 id=\"sec-6\" tabindex=\"-1\">Your turn</h2>\n<p>The exercise is a small slice of the old stylesheet: ID selectors, <code dir=\"ltr\">!important</code>, a component with its own margin. Refactor it into layers with a token for the card accent, a sponsor theme, layout-owned spacing and a utility that wins cleanly. The final lesson looks at what all this CSS costs the browser, and how to keep animations and layouts fast.</p>\n","toc":[{"id":"sec-1","text":"The layer stack is the architecture","level":2},{"id":"sec-2","text":"Tokens in three tiers","level":2},{"id":"sec-3","text":"Component rules","level":2},{"id":"sec-4","text":"Utilities: few and final","level":2},{"id":"sec-5","text":"Scoping with @scope","level":2},{"id":"sec-6","text":"Your turn","level":2}],"takeaways":["A layer order such as <code dir=\"ltr\">reset, tokens, base, layouts, components, utilities</code> decides conflicts between kinds of CSS, so selectors can stay at one class.","Tokens work best in tiers: raw palette values, semantic roles like <code dir=\"ltr\">--color-brand</code>, and component-level properties like <code dir=\"ltr\">--card-accent</code> that a parent can override.","Components should not set their own outer margins; the layout around them owns the spacing, usually with <code dir=\"ltr\">gap</code>.","Keep utilities few and single-purpose, in the last layer, so they win without <code dir=\"ltr\">!important</code>."],"further":[{"title":"Cascade layers (MDN guide)","url":"https://developer.mozilla.org/en-US/docs/Learn_web_development/Core/Styling_basics/Cascade_layers"},{"title":"@scope (MDN)","url":"https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/At-rules/@scope"},{"title":"Organizing your CSS (MDN)","url":"https://developer.mozilla.org/en-US/docs/Learn_web_development/Core/Styling_basics/Organizing"}],"quiz":[{"q":"<p>A sponsor page needs every session card’s accent in teal instead of violet. In a token-based architecture, what’s the right change?</p>","options":[{"text":"Add <code dir=\"ltr\">.sponsor-page .session-card { border-color: teal !important; }</code>.","why":"That overrides one property with brute force; the badge, hover state and anything else using the accent stay violet."},{"text":"Copy the card’s CSS into a <code dir=\"ltr\">.session-card--sponsor</code> variant with teal values.","why":"Duplicated component CSS drifts the first time someone edits the original."},{"text":"Set <code dir=\"ltr\">--card-accent: teal</code> on the sponsor page’s wrapper.","why":"Correct. The card reads <code dir=\"ltr\">--card-accent</code> everywhere it uses the accent, and custom properties inherit, so one declaration re-themes every part of every card inside."}],"answer":2},{"q":"<p>Why shouldn’t <code dir=\"ltr\">.session-card</code> set <code dir=\"ltr\">margin-bottom: 16px</code> on itself?</p>","options":[{"text":"Because margins don’t work on grid items.","why":"Margins work on grid items; the problem is who decides the spacing."},{"text":"Because spacing depends on where the card is placed, so the layout around it should own it, for example with <code dir=\"ltr\">gap</code>.","why":"Correct. The same card sits in a grid, a sidebar and a carousel; each context needs different spacing, and a built-in margin fights all of them."},{"text":"Because margins are slower to render than gap.","why":"There’s no meaningful performance difference; this is about ownership and reuse."},{"text":"Because logical properties should be used instead.","why":"<code dir=\"ltr\">margin-block-end</code> would have the same problem; the issue is the component spacing itself, not the property name."}],"answer":1},{"q":"<p>Where should a <code dir=\"ltr\">.u-hidden { display: none; }</code> utility live so it always beats component display rules without <code dir=\"ltr\">!important</code>?</p>","options":[{"text":"In the last declared layer, such as <code dir=\"ltr\">utilities</code>.","why":"Correct. Later layers win for normal declarations, whatever the specificity of the component selector."},{"text":"In the <code dir=\"ltr\">reset</code> layer, so it applies early.","why":"Early layers lose to later ones; any component setting <code dir=\"ltr\">display</code> would override it."},{"text":"Unlayered, at the top of the stylesheet.","why":"Unlayered styles do beat every layer, but leaving rules unlayered on purpose makes the order invisible and invites more unlayered patches."}],"answer":0}],"exercise":{"kind":"web","mode":"dom","starter":{"html":"<main id=\"schedule\" class=\"schedule\">\n  <div class=\"theme-sponsor\">\n    <article class=\"session-card\" id=\"sponsored\">\n      <h3>Sponsor Talk: Shipping at Scale</h3>\n      <span class=\"badge u-hidden\">Draft</span>\n    </article>\n  </div>\n  <article class=\"session-card\" id=\"regular\">\n    <h3>CSS Architecture at Scale</h3>\n    <span class=\"badge\">Platform</span>\n  </article>\n</main>","css":"h3 { margin: 0 0 8px; }\n\n#schedule .session-card {\n  padding: 12px;\n  margin-bottom: 16px;\n  border-left: 6px solid #6d28d9;\n  border-radius: 10px;\n  background: #f8fafc;\n}\n#schedule .session-card .badge {\n  display: inline-block;\n  padding: 0.1em 0.6em;\n  border-radius: 999px;\n  background: #ede9fe;\n}\n.theme-sponsor .session-card {\n  border-left-color: #0e7490 !important;\n}\n.u-hidden { display: none !important; }\n"},"solution":{"html":"<main id=\"schedule\" class=\"schedule\">\n  <div class=\"theme-sponsor\">\n    <article class=\"session-card\" id=\"sponsored\">\n      <h3>Sponsor Talk: Shipping at Scale</h3>\n      <span class=\"badge u-hidden\">Draft</span>\n    </article>\n  </div>\n  <article class=\"session-card\" id=\"regular\">\n    <h3>CSS Architecture at Scale</h3>\n    <span class=\"badge\">Platform</span>\n  </article>\n</main>","css":"@layer reset, tokens, layouts, components, utilities;\n\n@layer reset {\n  h3 { margin: 0 0 8px; }\n}\n\n@layer tokens {\n  :root {\n    --color-brand: #6d28d9;\n    --card-accent: var(--color-brand);\n  }\n  .theme-sponsor {\n    --card-accent: #0e7490;\n  }\n}\n\n@layer layouts {\n  .schedule {\n    display: grid;\n    gap: 16px;\n  }\n}\n\n@layer components {\n  .session-card {\n    padding: 12px;\n    border-inline-start: 6px solid var(--card-accent);\n    border-radius: 10px;\n    background: #f8fafc;\n\n    .badge {\n      display: inline-block;\n      padding: 0.1em 0.6em;\n      border-radius: 999px;\n      background: #ede9fe;\n    }\n  }\n}\n\n@layer utilities {\n  .u-hidden { display: none; }\n}\n"},"checks":[{"id":"layer-order","test":"const all = []; const walk = (rules) => { for (const r of rules) { all.push(r); if (r.cssRules) walk(r.cssRules); } }; for (const s of document.styleSheets) walk(s.cssRules); const stmt = all.find((r) => r instanceof CSSLayerStatementRule); if (!stmt) return false; const names = [...stmt.nameList]; const want = ['reset', 'tokens', 'components', 'utilities']; const idx = want.map((n) => names.indexOf(n)); return idx.every((i) => i >= 0) && idx.every((v, i) => i === 0 || v > idx[i - 1]);"},{"id":"no-ids-or-important","test":"const all = []; const walk = (rules) => { for (const r of rules) { all.push(r); if (r.cssRules) walk(r.cssRules); } }; for (const s of document.styleSheets) walk(s.cssRules); const src = [...document.querySelectorAll('style')].map((s) => s.textContent).join(''); return !src.includes('!important') && !all.some((r) => r.selectorText && r.selectorText.includes('#'));"},{"id":"accent-token","test":"return style('#regular', 'border-left-color') === 'rgb(109, 40, 217)' && style('#regular', '--card-accent').trim() !== '';"},{"id":"sponsor-theme","test":"return style('#sponsored', 'border-left-color') === 'rgb(14, 116, 144)' && style('.theme-sponsor', '--card-accent').trim() !== '';"},{"id":"token-drives-card","test":"const w = $('.theme-sponsor'); w.style.setProperty('--card-accent', 'rgb(1, 2, 3)'); await tick(16); const c = style('#sponsored', 'border-left-color'); w.style.removeProperty('--card-accent'); await tick(16); return c === 'rgb(1, 2, 3)';"},{"id":"utility-wins","test":"return style('#sponsored .badge', 'display') === 'none' && style('#regular .badge', 'display') === 'inline-block';"},{"id":"layout-owns-spacing","test":"const a = $('#sponsored').getBoundingClientRect(); const b = $('#regular').getBoundingClientRect(); return parseFloat(style('#regular', 'margin-bottom')) === 0 && parseFloat(style('#sponsored', 'margin-bottom')) === 0 && Math.abs(b.top - a.bottom - 16) < 1;"}],"title":"Refactor the card into an architecture","prompt":"<p>Refactor this slice of the old stylesheet. Keep the HTML as it is.</p>\n<ol>\n<li>Declare layers including <strong><code dir=\"ltr\">reset</code>, <code dir=\"ltr\">tokens</code>, <code dir=\"ltr\">components</code>, <code dir=\"ltr\">utilities</code></strong> in that order (you may add <code dir=\"ltr\">layouts</code> or others in between), and put every rule in a layer.</li>\n<li>No ID selectors and no <code dir=\"ltr\">!important</code> anywhere.</li>\n<li>The card’s accent border reads a <strong><code dir=\"ltr\">--card-accent</code></strong> token that defaults to <code dir=\"ltr\">#6d28d9</code>; <code dir=\"ltr\">.theme-sponsor</code> sets it to <strong><code dir=\"ltr\">#0e7490</code></strong>.</li>\n<li><code dir=\"ltr\">.u-hidden</code> still hides the Draft badge.</li>\n<li>Cards have <strong>no margins of their own</strong>; the <code dir=\"ltr\">.schedule</code> layout puts <strong>16px</strong> between them.</li>\n</ol>\n","hints":["Start with the layer statement, then move each existing rule into the layer that matches its job.","Tokens: <code dir=\"ltr\">:root { --card-accent: #6d28d9; }</code> and <code dir=\"ltr\">.theme-sponsor { --card-accent: #0e7490; }</code>. The card uses <code dir=\"ltr\">border-inline-start: 6px solid var(--card-accent);</code>.","Spacing: delete the card’s margin and give <code dir=\"ltr\">.schedule</code> <code dir=\"ltr\">display: grid; gap: 16px;</code>. Put <code dir=\"ltr\">.u-hidden</code> in the utilities layer, without <code dir=\"ltr\">!important</code>."],"explanation":"<p>The layer statement moves conflict resolution out of selectors: <code dir=\"ltr\">.u-hidden</code> in the last layer beats the component’s <code dir=\"ltr\">display: inline-block</code> with plain (0,1,0) specificity, so neither IDs nor <code dir=\"ltr\">!important</code> are needed. The card reads <code dir=\"ltr\">--card-accent</code> instead of a hard-coded color, so <code dir=\"ltr\">.theme-sponsor</code> re-themes every card inside it with one custom property, and any future part of the card that uses the accent follows automatically. Moving spacing to <code dir=\"ltr\">.schedule { gap: 16px; }</code> lets the same card drop into a sidebar or carousel without fighting a built-in margin.</p>\n","checkLabels":{"layer-order":"Layers reset, tokens, components, utilities are declared in order","no-ids-or-important":"No ID selectors and no !important","accent-token":"Regular cards use the violet --card-accent token","sponsor-theme":"Sponsor cards are teal through the token","token-drives-card":"Changing --card-accent re-themes the card","utility-wins":"The u-hidden utility still hides the Draft badge","layout-owns-spacing":"Cards have no margins; the layout spaces them 16px apart"}},"words":711,"readMinutes":3}