<?xml version="1.0" encoding="UTF-8" standalone="no"?><feed xmlns="http://www.w3.org/2005/Atom" xmlns:idx="urn:atom-extension:indexing" xmlns:media="http://search.yahoo.com/mrss/" idx:index="no"><subtitle>tipy na zajímavé články</subtitle>
    <!--
Content-type: Preventing XSRF in IE.

-->
    <generator uri="https://cloud.feedly.com">feedly cloud</generator>
    <id>tag:feedly.com,2013:cloud/feed/https://feedly.com/f/rJIIpYUxxvSQIe3s6CKpJaO8</id>
    <link href="https://feedly.com/f/rJIIpYUxxvSQIe3s6CKpJaO8" rel="self" type="application/rss+xml"/>
    <link href="https://feedly.com/f/rJIIpYUxxvSQIe3s6CKpJaO8?continuation=19cbc9c276d:32fe9b:117a53e3" rel="next" type="application/rss+xml"/>
    <title>rarouš.w3b</title>
    <updated>2026-05-15T05:08:58Z</updated>
    <entry>
        <id>tag:feedly.com,2013:cloud/entry/cOXRjoksGIQzU1pdqbdybn+OTWYeI0riptNneGckdsI=_19e2a09a0f2:5db05a:10dfc124</id>
        <title type="html">Native Apps Should Be Avoided Whenever Possible</title>
        <published>2026-05-15T05:08:54Z</published>
        <updated>2026-05-15T05:08:58Z</updated>
        <link href="https://nooneshappy.com/article/native-apps-should-be-avoided-whenever-possible/" rel="alternate" type="text/html"/>
        <summary type="html">Why native apps have become privacy liabilities, and why the browser is almost always the better choice.</summary>
        <content type="html">&lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"&gt;
&lt;html&gt;&lt;body&gt;&lt;div&gt;&lt;div&gt; &lt;article&gt;&lt;header&gt;&lt;time datetime="2026-04-12"&gt;April 12, 2026&lt;/time&gt;&lt;h1&gt;Native Apps Should Be Avoided Whenever Possible&lt;/h1&gt; &lt;/header&gt;&lt;blockquote&gt;
&lt;p&gt;TL;DR: &lt;strong&gt;What you should do:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;&lt;li&gt;Openly refuse apps, and vocally advocate for the web instead.&lt;/li&gt;
&lt;li&gt;Try not to install any apps if you don’t need to.&lt;/li&gt;
&lt;li&gt;If a service has a functioning website, use it instead.&lt;/li&gt;
&lt;li&gt;Revoke all permissions by default, including background location, microphone, and camera permissions for anything that doesn’t require them to function.&lt;/li&gt;
&lt;li&gt;Audit your installed apps. Uninstall all apps you don’t actively need.&lt;/li&gt;
&lt;li&gt;Treat every “download our app” prompt with skepticism.&lt;/li&gt;
&lt;/ul&gt;&lt;/blockquote&gt;
&lt;p&gt;Most native apps collect far more data than their website equivalents ever could. They request permissions to hardware, sensors, and background processes that browsers deliberately restrict. The third-party software embedded in these apps frequently transmits your location, device identifiers, and behavioral data to third parties before you even see a consent prompt. This data is in tandem bought, sold, and aggregated by brokers. It has been used to out individuals, track immigrants, and enable prosecution over reproductive healthcare.&lt;/p&gt;
&lt;h2 id="the-white-house-app"&gt;The White House App&lt;/h2&gt;
&lt;p&gt;On March 27, 2026, the Trump administration released an official White House app for iOS and Android. Within hours, two independent security researchers decompiled it and published their findings [&lt;a href="https://nooneshappy.com/article/native-apps-should-be-avoided-whenever-possible/#ref-1"&gt;1&lt;/a&gt;]. The app is a textbook example of everything wrong with the native app model.&lt;/p&gt;
&lt;p&gt;Apple requires apps to submit a privacy manifest disclosing what data they collect. The White House app declared an empty array. Zero data collection. Meanwhile, the actual binary contained ten analytics frameworks, including the full OneSignal SDK with a sub-framework specifically for location tracking [&lt;a href="https://nooneshappy.com/article/native-apps-should-be-avoided-whenever-possible/#ref-2"&gt;2&lt;/a&gt;]. The GPS pipeline polled precise coordinates every 4.5 minutes in the foreground and every 9.5 minutes in the background, syncing everything to OneSignal’s commercial servers. A boolean flag in OneSignal’s server responses could remotely enable or disable GPS tracking without an app update and without Apple review.&lt;/p&gt;
&lt;p&gt;An Exodus Privacy audit identified three embedded trackers, one of which was Huawei Mobile Services Core [&lt;a href="https://nooneshappy.com/article/native-apps-should-be-avoided-whenever-possible/#ref-3"&gt;3&lt;/a&gt;]. The app’s privacy policy, last updated January 20, 2025, makes no mention of GPS tracking, OneSignal, or background data collection.&lt;/p&gt;
&lt;p&gt;Nearly everything in the app is available on whitehouse.gov. The app’s unique additions are push notifications, a pre-filled text message to the President, and an ICE tip button (also available on ice.gov). What it actually added at scale was a surveillance pipeline: 77% of the app’s network requests go to third parties, not whitehouse.gov.&lt;/p&gt;
&lt;h2 id="the-software-embedded-in-apps"&gt;The Software Embedded in Apps&lt;/h2&gt;
&lt;p&gt;Most people think of apps as products built by a single company. In practice, the average app is a thin wrapper around dozens of third-party software packages, each with its own data collection pipeline and commercial incentives. When you grant an app permission to access your location, every package embedded in that app inherits that permission. A single package can appear in hundreds of apps, feeding location data on millions of people to a single aggregator.&lt;/p&gt;
&lt;p&gt;In January 2025, a hacker breached Gravy Analytics and leaked roughly 30 million location records collected from 3,455 apps — dating, fitness, gaming, and health apps among them [&lt;a href="https://nooneshappy.com/article/native-apps-should-be-avoided-whenever-possible/#ref-4"&gt;4&lt;/a&gt;]. The FTC subsequently banned Gravy Analytics from selling Americans’ location data [&lt;a href="https://nooneshappy.com/article/native-apps-should-be-avoided-whenever-possible/#ref-5"&gt;5&lt;/a&gt;], but by then the data was already circulating on cybercrime forums.&lt;/p&gt;
&lt;p&gt;In a separate case, Google paid $391.5 million to settle claims from 40 states for continuing to collect location data even when users explicitly disabled location tracking [&lt;a href="https://nooneshappy.com/article/native-apps-should-be-avoided-whenever-possible/#ref-6"&gt;6&lt;/a&gt;].&lt;/p&gt;
&lt;h2 id="why-everyone-should-care"&gt;Why Everyone Should Care&lt;/h2&gt;
&lt;p&gt;This data is bought, sold, and aggregated by brokers. It has been used to out individuals, track immigrants, and enable prosecution over reproductive healthcare. In multiple cases, journalists and private groups have purchased app-derived location data to identify specific people based on their movements.&lt;/p&gt;
&lt;p&gt;There are virtually no restrictions in the United States on buying, selling, or weaponizing this data. There is no comprehensive federal privacy law. And there isn’t likely to be one soon. The best we can do is minimize the data we share in the first place.&lt;/p&gt;
&lt;h2 id="what-apps-can-do-that-websites-cant"&gt;What Apps Can Do That Websites Can’t&lt;/h2&gt;
&lt;p&gt;The core argument for using the website instead of the app comes down to what each platform is technically capable of doing without your knowledge.&lt;/p&gt;

































































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Capability&lt;/th&gt;&lt;th&gt;Native App&lt;/th&gt;&lt;th&gt;Website / PWA&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Background location tracking&lt;/td&gt;&lt;td&gt;Yes, can poll GPS continuously&lt;/td&gt;&lt;td&gt;No&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Run at device startup&lt;/td&gt;&lt;td&gt;Yes&lt;/td&gt;&lt;td&gt;No&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Access biometric hardware&lt;/td&gt;&lt;td&gt;Yes&lt;/td&gt;&lt;td&gt;Limited (WebAuthn, user initiated)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Modify or delete device storage&lt;/td&gt;&lt;td&gt;Yes&lt;/td&gt;&lt;td&gt;No (sandboxed to browser)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Embed invisible third-party software&lt;/td&gt;&lt;td&gt;Yes, all inherit granted permissions&lt;/td&gt;&lt;td&gt;No, scripts visible in page source&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Transmit data before consent prompt&lt;/td&gt;&lt;td&gt;Yes (common with third-party software)&lt;/td&gt;&lt;td&gt;Restricted by browser policies&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Push notifications while closed&lt;/td&gt;&lt;td&gt;Yes&lt;/td&gt;&lt;td&gt;Yes (PWA, user opt in required)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Access contacts, call logs, SMS&lt;/td&gt;&lt;td&gt;Yes (if permitted)&lt;/td&gt;&lt;td&gt;No&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Prevent phone from sleeping&lt;/td&gt;&lt;td&gt;Yes&lt;/td&gt;&lt;td&gt;No&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Camera and microphone&lt;/td&gt;&lt;td&gt;Yes (persistent if granted)&lt;/td&gt;&lt;td&gt;Yes (per session, prompted each time)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Offline functionality&lt;/td&gt;&lt;td&gt;Yes&lt;/td&gt;&lt;td&gt;Yes (via service workers)&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;p&gt;The browser is the security boundary. Websites operate within it. Native apps bypass it.&lt;/p&gt;
&lt;h2 id="the-access-provided-by-default-is-enough-to-do-real-harm"&gt;The Access Provided by Default is Enough to Do Real Harm&lt;/h2&gt;
&lt;p&gt;The moment you install an app, before you allow a single permission prompt, it can:&lt;/p&gt;
&lt;ul&gt;&lt;li&gt;Reach any server on the internet&lt;/li&gt;
&lt;li&gt;Read your IP address, device model, OS version, timezone, country, carrier, and network type&lt;/li&gt;
&lt;li&gt;Generate and persist a unique identifier tied to your device&lt;/li&gt;
&lt;li&gt;Run code at device startup (Android) and wake up in the background&lt;/li&gt;
&lt;li&gt;Fingerprint your device by combining the above into a signature that follows you across sessions&lt;/li&gt;
&lt;li&gt;Grant all of this same access to every third-party software package embedded in the app&lt;/li&gt;
&lt;li&gt;Compare this data to other datasets to infer your identity, demographics, interests, and habits&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;The runtime permission prompts you actually see (location, camera, contacts) are helpful while annoying, but the majority of the default access permissions do not require your consent.&lt;/p&gt;
&lt;p&gt;A website, by contrast, starts with almost none of this: no persistent identifier, no background execution, no third-party software inheritance, no startup hooks.&lt;/p&gt;
&lt;h2 id="some-things-need-to-be-apps-but-most-dont"&gt;Some Things Need to Be Apps, But Most Don’t&lt;/h2&gt;
&lt;p&gt;Some things need to be apps. AR and VR, real-time games, anything talking to NFC or Bluetooth hardware, serious audio and video work, accessibility tools. These are legitimate cases where the browser sandbox is the limitation. In these circumtances, I personally use a full computer as opposed to my phone.&lt;/p&gt;
&lt;p&gt;Almost nothing else qualifies. Your banking, your travel, your grocery store, the restaurant down the street — none of it needs an app. And rewards be damned. No rewards are worth the data you are willingly giving them.&lt;/p&gt;
&lt;p&gt;Same goes for hardware. If a thermostat or a fitness tracker can’t be set up without a proprietary app, that’s a flaw in the product, not a feature. I immediately avoid such products. You’re buying an ongoing relationship with someone else’s servers and guaranteeing that you’ll forget that corporations are watching everything they can.&lt;/p&gt;
&lt;h2 id="conclusion"&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;I avoid most apps. It turns out this is easier than most people assume, because the app is almost never the only option. It is just the option the company wants you to take and not enough people question.&lt;/p&gt;
&lt;p&gt;We are at a very specific time in humanity right now. Where aggregating data is a currency, and it is actively being utilized at a scale never before seen. I recommend you at least take stock of what you’re freely giving away.&lt;/p&gt;
&lt;hr&gt;&lt;h2 id="references"&gt;References&lt;/h2&gt;
&lt;p&gt;&lt;span id="ref-1"&gt;1.&lt;/span&gt; &lt;a href="https://thereallo.dev/blog/decompiling-the-white-house-app"&gt;I Decompiled the White House’s New App&lt;/a&gt; (Thereallo, March 28, 2026) and &lt;a href="https://www.atomic.computer/blog/white-house-app-security-analysis/"&gt;Security Analysis of the Official White House iOS App&lt;/a&gt; (atomic.computer, March 27, 2026). Two independent researchers decompiled the app on Android and iOS within hours of release.&lt;/p&gt;
&lt;p&gt;&lt;span id="ref-2"&gt;2.&lt;/span&gt; &lt;a href="https://www.atomic.computer/blog/white-house-app-security-analysis/"&gt;Security Analysis of the Official White House iOS App&lt;/a&gt; (atomic.computer). Documents the empty privacy manifest, OneSignal SDK with ten sub-frameworks, and the remote GPS toggle via server response.&lt;/p&gt;
&lt;p&gt;&lt;span id="ref-3"&gt;3.&lt;/span&gt; &lt;a href="https://reports.exodus-privacy.eu.org/en/reports/gov.whitehouse.app/latest/"&gt;Exodus Privacy Report: gov.whitehouse.app&lt;/a&gt;. Automated audit identifying three embedded trackers including Huawei Mobile Services Core. Additional context in &lt;a href="https://www.sambent.com/the-white-house-app-has-huawei-spyware-and-an-ice-tip-line/"&gt;Fedware: 13 Government Apps That Spy Harder Than the Apps They Ban&lt;/a&gt; (Sam Bent).&lt;/p&gt;
&lt;p&gt;&lt;span id="ref-4"&gt;4.&lt;/span&gt; &lt;a href="https://techcrunch.com/2025/01/13/gravy-analytics-data-broker-breach-trove-of-location-data-threatens-privacy-millions/"&gt;A breach of Gravy Analytics’ huge trove of location data threatens the privacy of millions&lt;/a&gt; (TechCrunch, January 13, 2025).&lt;/p&gt;
&lt;p&gt;&lt;span id="ref-5"&gt;5.&lt;/span&gt; &lt;a href="https://www.ftc.gov/news-events/news/press-releases/2025/01/ftc-finalizes-order-prohibiting-gravy-analytics-venntel-selling-sensitive-location-data"&gt;FTC Finalizes Order Prohibiting Gravy Analytics, Venntel from Selling Sensitive Location Data&lt;/a&gt; (FTC, January 14, 2025).&lt;/p&gt;
&lt;p&gt;&lt;span id="ref-6"&gt;6.&lt;/span&gt; &lt;a href="https://techcrunch.com/2022/11/14/google-pay-391-5-million-location-tracking-settlement/"&gt;Google to pay $391.5 million in location tracking settlement with 40 states&lt;/a&gt; (TechCrunch, November 14, 2022).&lt;/p&gt;  &lt;/article&gt;&lt;/div&gt;&lt;/div&gt;&lt;/body&gt;&lt;/html&gt;</content>
        <author>
            <name>No One's Happy</name>
        </author>
        <source>
            <id>tag:feedly.com,2013:cloud/feed/https://nooneshappy.com/rss.xml</id>
            <title type="html">No One's Happy</title>
            <link href="https://nooneshappy.com" rel="alternate" type="text/html"/>
            <updated>2026-05-15T05:08:58Z</updated>
        </source>
    </entry>
    <entry>
        <id>tag:feedly.com,2013:cloud/entry/cOXRjoksGIQzU1pdqbdybn+OTWYeI0riptNneGckdsI=_19e00ecc82e:330034:c12867d</id>
        <title type="html">AI dělá chyby a autisté je dokážou najít. Z jejich jinakosti dělá Češka ve svém startupu přednost</title>
        <published>2026-05-07T05:32:58Z</published>
        <updated>2026-05-07T05:33:01Z</updated>
        <link href="https://cc.cz/ai-dela-chyby-a-autiste-je-dokazou-najit-z-jejich-jinakosti-dela-ceska-ve-svem-startupu-prednost/" rel="alternate" type="text/html"/>
        <summary type="html">„I nejpokročilejší vývojářské týmy musí manuálně odstraňovat duplicity a řešit případy, kde automatizace selhává,“ říká Irena Zatloukalová.</summary>
        <content type="html">&lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"&gt;
&lt;html&gt;&lt;body&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;

					
&lt;p&gt;

	
	

	
	

	
&lt;/p&gt;
&lt;p&gt;Představte si konkrétní situaci: datoví vědci ve startupu trénují svůj analytický model, ale v určitých případech narážejí na vysokou chybovost v podobě falešně pozitivních výsledků. Místo toho, aby se pálil drahý čas seniorních inženýrů na manuálním čištění datasetů, předá se úkol ven. Výsledek? Najatý tým projde pět iterací dat a pomůže tak o polovinu zlepšit úspěšnost modelu. Právě na tom staví mladý pražský startup Diversight. Jeho tajnou zbraní nejsou pokročilejší algoritmy, nýbrž dedikovaní analytici na autistickém spektru. Ukazuje tak, že vlastnosti, kvůli kterým se jiné firmy těmto lidem někdy vyhýbají, se mohou proměnit na silné stránky.&lt;/p&gt;&lt;p id="czech-bbc21e7e4b919b9a9b21266f2b6fc810"&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;„Nejsme charita ani neziskovka, jsme klasická technologická firma,“ &lt;/em&gt;říká hned na úvod spoluzakladatelka a ředitelka Diversightu Irena Zatloukalová. V českém technologickém prostředí se pohybuje dvacet let, a to zejména v oblasti komunikace, kdy sedm let byla mluvčí tuzemské internetové jedničky Seznam.cz. Mimo jiné působila i jako spolumajitelka PR agentury Fenek a později na volné noze radila s komunikací technologickým firmám jako Heureka, Apify, Liftago či Ackee. Loni v červenci ale všechny klienty předala dál a naplno se vrhla do nového byznysu.&lt;/p&gt;
&lt;p&gt;V Diversightu se zaměřuje na takzvaný datový dluh. V éře, kdy se prakticky každá firma snaží do svých procesů implementovat umělou inteligenci, stojí budoucí úspěch primárně na kvalitě vstupních dat. &lt;em&gt;„I ty nejpokročilejší vývojářské týmy totiž musí manuálně odstraňovat duplicity a řešit hraniční případy, v nichž zavedená automatizace zkrátka selhává a modely začínají generovat nesmysly. Diversight funguje jako filtr, který přebírá na přesnost náročnou vrstvu ekosystému a zajišťuje potřebnou lidskou pojistku proti halucinacím,“&lt;/em&gt; vysvětluje pro CzechCrunch Zatloukalová.&lt;/p&gt;


&lt;div&gt;
	&lt;div&gt;&lt;a href="https://cc.cz/ai-dela-chyby-a-autiste-je-dokazou-najit-z-jejich-jinakosti-dela-ceska-ve-svem-startupu-prednost/galerie/541483/"&gt;&lt;span&gt;&lt;img src="https://cc.cz/wp-content/uploads/2026/04/zatloukalova-simon-815x509.jpg" alt="zatloukalova-simon"&gt;&lt;/span&gt;&lt;/a&gt;&lt;p&gt;&lt;span&gt;Foto: Diversight&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;p&gt;Irena Zatloukalová s kolegou Šimonem&lt;/p&gt;&lt;/div&gt;

&lt;p&gt;Právě v oblasti hledání nepatrných vzorců totiž samotná umělá inteligence běžně naráží na své limity, dodává. Pokud modely generují opakující se chyby, vývojářům nepostačí jen jednoduché přepsání textového promptu. Týmy potřebují člověka, který dokáže precizně pochopit byznysovou logiku a vyčíst přímo ze syrové databáze konkrétní pravidla a odchylky. &lt;em&gt;„V tom máme ohromnou výhodu, protože máme lidi, kterým jednou řeknete pravidla a oni pak po nich neúnavně půjdou, od prvního řádku až po řádek číslo 1 500,“&lt;/em&gt; popisuje specifikum práce Zatloukalová.&lt;/p&gt;
&lt;p&gt;Doplňuje přitom, že jejím lidem nedělá problém udržet stejnou hladinu pozornosti na jednom tématu klidně i osm dní v kuse: &lt;em&gt;„Prostě potřebují problém vyřešit, neznámou najít a věc dotáhnout do konce. To pomyšlení &lt;/em&gt;&lt;em&gt;na rozlousknutí problému pomáhá jejich fokusu.“&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Na samotném začátku příběhu Diversightu přitom nestála Zatloukalová. &lt;em&gt;„Ráda bych to říkala, ale bohužel to nebyl můj nápad,“ &lt;/em&gt;usmívá se a jako originálního tvůrce označuje Tomáše Borovičku z technologické společnosti Datamole. Ten na zahraničních trzích narazil na izraelskou firmu Point AI, která budovala data-anotační byznys (zaměřený na označování surových dat) na přesné práci lidí na spektru. Od té doby přemýšlel nad tím, jak vyzkoušet podobný koncept i v Česku.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;„Byla to jedna večeře, jedno potkání, tři hodiny povídání. Šli jsme hodně do hloubky a do našich hodnot a na konci mi řekl: Tak jo, pojďme to zkusit,“&lt;/em&gt; vzpomíná Zatloukalová na zlom, který přišel v prosinci 2024. A dodává, že na začátku je propojila Lenka Kučerová ze spolku prg.ai.&lt;/p&gt;



&lt;p&gt;Původní plán Diversightu počítal s tím, že se do Prahy přenese jako franšíza přímo izraelský model. Rychlý průzkum trhu ale ukázal, že běžný anotační byznys je v tuzemsku v podstatě jen levná komodita, kde firmy často najímají studenty a na finální kvalitě jim tolik nesejde. Zatímco v Izraeli se data pro zdravotnictví či zbrojní průmysl anotují s kritickou přesností, v lokálních podmínkách by takový model ekonomicky nepřežil. Diversight proto musel okamžitě změnit plány. Zaměřil se na komplexnější analytické úkoly a takzvanou validaci s člověkem v procesu (human-in-the-loop), kde se naplno využije kognitivní preciznost ve složitých datech.&lt;/p&gt;
&lt;p&gt;Projekt od začátku strategicky i finančně podpořil zmíněný Datamole, který mu poskytl peníze na bezpečný roční provoz a působí v roli partnera. Mimo jiné nabízí i přímý přístup ke svým seniorním AI architektům. Hlavním byznysovým úkolem pro nadcházející měsíce je tak dostat Diversight do zisku díky příjmům ze zakázek.&lt;/p&gt;


&lt;div&gt;
	&lt;div&gt;&lt;a href="https://cc.cz/ai-dela-chyby-a-autiste-je-dokazou-najit-z-jejich-jinakosti-dela-ceska-ve-svem-startupu-prednost/galerie/541481/"&gt;&lt;span&gt;&lt;img src="https://cc.cz/wp-content/uploads/2026/04/borovicka-zatloukalova-815x509.jpg" alt="borovicka-zatloukalova"&gt;&lt;/span&gt;&lt;/a&gt;&lt;p&gt;&lt;span&gt;Foto: Diversight&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;p&gt;Tomáš Borovička a Irena Zatloukalová&lt;/p&gt;&lt;/div&gt;

&lt;p&gt;Mezi aktuální klienty vedle &lt;a href="https://cc.cz/sbiraji-data-o-kozach-e-shopech-i-teslach-cesky-startup-dela-byznys-na-tom-co-na-sebe-firmy-prozradi/"&gt;analytické společnosti BizMachine,&lt;/a&gt; pro kterou startup dělá v úvodu zmíněné čištění dat, patří například také InfraHex, jedna z firem holdingu Respilon. Ta vyvinula technologii, která upravuje tepelnou stopu živých i neživých objektů a tím omezuje jejich detekci pomocí termokamer. V současnosti se využívá i na Ukrajině, kde pomáhá vojákům v terénu. I drobné odchylky zde hrají zásadní roli, protože se systém opírá o přesné analýzy obrazů v různých infra spektrech. A tak se na této detailní práci vyhodnocování a výběru dat podílejí lidé na autistickém spektru.&lt;/p&gt;
&lt;p&gt;Českým mediálním i firemním prostorem dnes podle Zatloukalové bohužel rezonují převážně extrémní obrazy autismu, kdy na jedné straně stojí příběhy nespolupracujících jedinců s těžkým postižením, na straně druhé zprofanované postavy geniálních vědců, jako je například Sheldon Cooper z &lt;em&gt;Teorie velkého třesku&lt;/em&gt;. Diversight ale systematicky hledá velkou množinu lidí s nadprůměrnou analytickou inteligencí ležící mezi těmito dvěma extrémy. Většina z nich má však potíže projít přes výběrová řízení či běžná HR oddělení, protože standardní firemní postupy zkrátka neumí s neurodiverzitou pracovat.&lt;/p&gt;
&lt;div&gt;&lt;blockquote&gt;Pokud zahodíte své ego a uznáte, že někdo našel lepší cestu, firmě to hodně pomůže.&lt;/blockquote&gt;&lt;/div&gt;
&lt;p&gt;&lt;em&gt;„Už jen to, když do pracovního inzerátu vypíšete deset věcí jako ‚nice to have‘. Člověk na spektru si to projde, u třetí položky si řekne, že má jen dva a půl roku zkušeností místo tří, takže to není práce pro něj, a jde dál,“ &lt;/em&gt;vysvětluje Zatloukalová jeden z mnoha detailů, proč kandidáti sami rovnou z recruitmentu vypadávají. Značná část lidí na spektru tak i kvůli tomu končí na úřadu práce či na podřadných pozicích a ve firmách běžně narazí kvůli logickému poukazování na neefektivitu a navrhování lepších postupů.&lt;/p&gt;
&lt;p&gt;Zatloukalová proto změnila rovnou i náborový proces a přizpůsobila ho lidem na spektru. Přesné zadání úkolů a otázky zasílá s předstihem, kandidáty navíc netestuje pod umělým tlakem, ale zaměřuje se striktně na jejich schopnost strukturovaně přemýšlet. Aktuálně má firma pět lidí – vedle samotné Zatloukalové je to technologický manažer Adam Vesecký a interní tým tří specializovaných analytiků. Záměrem je tým rozšiřovat, nicméně se stropem na hranici deseti zaměstnanců, aby firma nepřišla o klidné prostředí komorního kolektivu.&lt;/p&gt;



&lt;p&gt;Zatloukalová navíc varuje před zjednodušováním nebo předsudky, že práce s neurodivergentními lidmi přináší jen těžkosti a komunikační zádrhele. Na první pohled nepříjemné situace, kdy podřízený manažerovi zcela upřímně oznámí, že zadal úkol špatně, totiž vedou k lepším procesům do budoucna, a tím i výsledkům. &lt;em&gt;„Pokud zahodíte své ego a uznáte, že někdo našel lepší cestu, firmě to hodně pomůže,“ &lt;/em&gt;vysvětluje Zatloukalová.&lt;/p&gt;
&lt;p&gt;Lidé na spektru podle ní přinášejí do byznysu inovace a odlišné úhly pohledu nejen v datové analytice, ale napříč všemi obory od zákaznické podpory po design. &lt;em&gt;„Mají přirozenou schopnost jít striktně po podstatě problému a všímat si i drobných odchylek, které neurotypická většina snadno přehlédne,“ &lt;/em&gt;doplňuje. Výměnou za tento výkon často vyžadují jen velmi specifické, avšak finančně nenáročné úpravy pracovního prostředí – typicky přesun stolu dál od rušné kuchyňky, utlumení ostrého kancelářského osvětlení nebo zkrátka jen možnost po náročné schůzce úplně vypnout a odejít domů.&lt;/p&gt;
&lt;div&gt;&lt;blockquote&gt;Cítím nespravedlnost. Jsem naštvaná na to, jakým způsobem se společnost staví k lidem, kteří jsou jiní.&lt;/blockquote&gt;&lt;/div&gt;
&lt;p&gt;Častou chybou některých manažerů je tyto potřeby zpochybňovat jen proto, že zaměstnanec nemá na stole oficiální lékařskou diagnózu. &lt;em&gt;„Moje velká prosba k jakémukoliv manažerovi je: i bez diagnózy člověka poslouchejte. Když říká, že něco potřebuje, respektujte to, nezpochybňujte a hledejte cesty, jak zařídit, aby mohl svoji práci vykonávat co nejlépe,“&lt;/em&gt; apeluje. Jak sama upozorňuje, nevyžaduje to stavbu drahých oddělených kanceláří, často stačí investice do kvalitních sluchátek s aktivním potlačením hluku.&lt;/p&gt;
&lt;p&gt;Přístup orientovaný na lidi se specifickými potřebami má podle Zatloukalové přesah do celkového chodu jakékoliv firmy. Veškerá drobná provozní pravidla a limity, které startup explicitně zavádí kvůli neurodivergentním kolegům, totiž v důsledku ulehčují práci všem. &lt;em&gt;„Každý zaměstnanec občas potřebuje ničím nerušený čas na práci, jen se to v běžných korporacích tolik neakcentuje. Funguje zde stejný princip jako v moderním urbanismu. Pokud navrhnete veřejný prostor tak, aby vyhovoval lidem na vozíku, dětem i rodičům s kočárky, vytvoříte ve finále vysoce funkční prostředí, ve kterém se bude dobře žít úplně všem,“ &lt;/em&gt;srovnává.&lt;/p&gt;
&lt;p&gt;Na druhou stranu Zatloukalová doplňuje, že budování neurodiverzního týmu s sebou pro vedení nese nutnost preciznosti v mezilidské komunikaci. &lt;em&gt;„Tou se živím dvacet let a největší paradox je, že se mi opravdu stává, že si s kolegy nerozumím v zadání,“&lt;/em&gt; přiznává s tím, že sama musela odložit své ego stranou a opět se učit zadávat úkoly maximálně jasně, doslovně a bez zbytečného sarkasmu.&lt;/p&gt;
&lt;p id="czech-7d2b87ee68a47d335feeceb6c53fb9c5"&gt;&lt;/p&gt;&lt;p id="czech-e70ae63f514d21e002eeddcd6c33af7a"&gt;&lt;/p&gt;&lt;p&gt;To vše se promítá i do samotného workflow při řešení úkolů pro klienty. &lt;em&gt;„Když řeknu ‚udělej A‘, kolega si začne domýšlet B, C a D. A místo toho, aby se zeptal, radši to rovnou udělá až po to D. Já mu pak musím říkat, že zašel moc daleko. Pro příště ale vím, že potřebuji dát víc kontextu k tomu, proč má v bodě A skončit,“&lt;/em&gt; ilustruje občasné komunikační střety zakladatelka. Doslovnost a analytický rozklad problémů se však startupu okamžitě vrací ve chvíli, kdy tým narazí na nekvalitní a chaotická data v zadání.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;„Cítím nespravedlnost. Jsem naštvaná na to, jakým způsobem se společnost staví k lidem, kteří jsou jiní. Ta jinakost nemusí být na první pohled patrná, ale znamená pro ně spoustu překážek, které jim vyrábíme my všichni. Angličtina pro to má slovní spojení ‚disabled by society‘. Jakmile mi ukážete něco takového, jsem totální buldok a jdu po narovnávání té nespravedlnosti,“ &lt;/em&gt;vysvětluje Zatloukalová, pro kterou se tak v Diversightu kromě společenského přesahu spojuje vášeň pro data a práce s lidmi. &lt;em&gt;„Baví mě dělat věci, které nikdo předtím moc nedělal nebo neprozkoumal. Diversight má v sobě všechno,“ &lt;/em&gt;uzavírá.&lt;/p&gt;
				&lt;/div&gt;

				
&lt;/div&gt;&lt;/div&gt;&lt;/body&gt;&lt;/html&gt;</content>
        <author>
            <name>Peter Brejčák</name>
        </author>
        <media:content medium="image" url="https://cc.cz/wp-content/uploads/2026/05/irena-zatloukalova.jpg"/>
        <source>
            <id>tag:feedly.com,2013:cloud/feed/https://feeds.buzzsprout.com/1104554.rss</id>
            <title type="html">CzechCrunch</title>
            <link href="https://cc.cz" rel="alternate" type="text/html"/>
            <updated>2026-05-07T05:33:01Z</updated>
        </source>
    </entry>
    <entry>
        <id>tag:feedly.com,2013:cloud/entry/cOXRjoksGIQzU1pdqbdybn+OTWYeI0riptNneGckdsI=_19dfd361137:57fe1:e753ea5d</id>
        <title type="html">Using safe-area-inset to build mobile-safe layouts</title>
        <published>2026-05-06T12:14:32Z</published>
        <updated>2026-05-06T12:14:35Z</updated>
        <link href="https://polypane.app/blog/using-safe-area-inset-to-build-mobile-safe-layouts/" rel="alternate" type="text/html"/>
        <summary type="html">Modern phones are not simple rectangles. They have rounded corners, camera cutouts, dynamic islands, and home indicators that double as gesture areas. Browsers…</summary>
        <content type="html">&lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"&gt;
&lt;html&gt;&lt;body&gt;&lt;div&gt;&lt;div&gt;&lt;div id="article"&gt;&lt;p&gt;Modern phones are not simple rectangles. They have rounded corners, camera cutouts, dynamic islands, and home indicators that double as gesture areas. Browsers know the dimensions of all of these and expose the parts that could obscure content as &lt;strong&gt;safe area insets&lt;/strong&gt;.&lt;/p&gt;&lt;p&gt;The "safe area" is the portion of the screen that is guaranteed to be free from being obscured by system UI. The safe-area-inset is the measurement of how much space the system UI is taking up on each edge of the screen. By using these values in your CSS, you can make sure that important content and controls are not obscured by the system UI.&lt;/p&gt;&lt;p&gt;If you don't want your floating chat button to end up sitting behind the home indicator, where it's unreachable, you need to account for the safe area inset.&lt;/p&gt;&lt;h2 id="environment-variables-for-safe-area-insets"&gt;Environment variables for safe area insets&lt;/h2&gt;&lt;p&gt;With the safe-area-inset environment variables, you can make your layout adapt to the current device's safe area and avoid those bugs. The &lt;code&gt;env()&lt;/code&gt; function is how you read those values in CSS:&lt;/p&gt;&lt;div&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;&lt;span&gt;body&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;padding-top&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;env&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;safe-area-inset-top&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;padding-right&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;env&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;safe-area-inset-right&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;padding-bottom&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;env&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;safe-area-inset-bottom&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;padding-left&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;env&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;safe-area-inset-left&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="browser-support"&gt;Browser support&lt;/h3&gt;&lt;p&gt;Safe-area-insets are &lt;strong&gt;baseline widely available&lt;/strong&gt;, which means you can use them in production today and be confident that they will work for almost all your users on mobile devices.&lt;/p&gt;&lt;p&gt;Since it's baseline widely available, you don't &lt;em&gt;really&lt;/em&gt; need to think about fallbacks but if you want to be extra safe, you can provide a fallback padding if the browser doesn't support &lt;code&gt;env()&lt;/code&gt;:&lt;/p&gt;&lt;div&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;&lt;span&gt;body&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;padding-top&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; 1rem&lt;span&gt;;&lt;/span&gt; 
  &lt;span&gt;padding-top&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;calc&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;env&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;safe-area-inset-top&lt;span&gt;)&lt;/span&gt; + 1rem&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And for browsers that support &lt;code&gt;env()&lt;/code&gt; but not &lt;code&gt;safe-area-inset-*&lt;/code&gt; variables, you can provide a fallback value directly in the &lt;code&gt;env()&lt;/code&gt; function. If &lt;code&gt;safe-area-inset-top&lt;/code&gt; is not supported, it will fall back to &lt;code&gt;1rem&lt;/code&gt;:&lt;/p&gt;&lt;div&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;&lt;span&gt;body&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;padding-top&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;env&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;safe-area-inset-top&lt;span&gt;,&lt;/span&gt; 1rem&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;It's worth noting that this situation is purely theoretical, as all browsers that support &lt;code&gt;env()&lt;/code&gt; also support &lt;code&gt;safe-area-inset-*&lt;/code&gt; variables. That might not be the case with other environment variables, so it's good to know that this fallback mechanism exists.&lt;/p&gt;&lt;h2 id="using-safe-area-inset-values"&gt;Using safe-area-inset values&lt;/h2&gt;&lt;p&gt;Before we get into problem areas, let's get a general understanding of how these variables work and how you can use them to affect the layout.&lt;/p&gt;&lt;p&gt;In this demo, change each inset and watch how a realistic mobile UI that takes safe area insets into account shifts to remain usable:&lt;/p&gt;&lt;p&gt;The specific px values here are not particular to any specific device, the point is to show how the device sets these variables and how your layout can respond to them as they change.&lt;/p&gt;&lt;p&gt;On real devices, you can think of these values as constants. They're provided by the browser and while they might change when switching from portrait to landscape or between OS updates that change the device UI as well as simply differ per device, they don't change dynamically as the user scrolls or interacts with the page. The browser provides them as a constant value that you can use to ensure your content is not obscured by the system UI.&lt;/p&gt;&lt;h2 id="which-web-pages-actually-need-this"&gt;Which web pages actually need this?&lt;/h2&gt;&lt;p&gt;If you want your pages to look the best they can, all of them.&lt;/p&gt;&lt;p&gt;Browsers by default will prevent your site from being obscured by the notch or home indicator, so your content will be safe without any special handling. That does come with a downside, which is that the browser will give you a smaller viewport to reserve space:&lt;/p&gt;&lt;p&gt;You'll notice the edges here keep the site from being obscured, but they also don't look that great. Ideally we want the content to stretch edge-to-edge, but we want to make sure they're not obscured by system UI. To get that, you need to opt in to the full viewport and handle safe areas yourself.&lt;/p&gt;&lt;p&gt;To do so is a two-step process. First, you need to add &lt;code&gt;viewport-fit=cover&lt;/code&gt; to your meta viewport tag:&lt;/p&gt;&lt;div&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;meta&lt;/span&gt; &lt;span&gt;name&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;"&lt;/span&gt;viewport&lt;span&gt;"&lt;/span&gt;&lt;/span&gt; &lt;span&gt;content&lt;/span&gt;&lt;span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span&gt;"&lt;/span&gt;width=device-width, viewport-fit=cover&lt;span&gt;"&lt;/span&gt;&lt;/span&gt; &lt;span&gt;/&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This will make the browser stretch your page edge-to-edge:&lt;/p&gt;&lt;p&gt;As you can see, the content sits behind the notch/dynamic island. &lt;code&gt;viewport-fit=cover&lt;/code&gt; tells the browser that you want to be responsible for making sure your content is not obscured by the system UI.&lt;/p&gt;&lt;p&gt;And now we can move those elements away from behind the system UI using &lt;code&gt;env(safe-area-inset-*)&lt;/code&gt;.&lt;/p&gt;&lt;div&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;&lt;span&gt;.content&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;padding-right&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;env&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;safe-area-inset-right&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;padding-left&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;env&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;safe-area-inset-left&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If you opt into the full viewport, here are the kinds of things you now have to think about to make sure your content is not obscured by the system UI:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;Fixed headers and navigation bars should have enough space above or below for the notch or home indicator&lt;/li&gt;&lt;li&gt;Floating chat or help buttons should remain inside the safe area&lt;/li&gt;&lt;li&gt;Full-screen dialogs and drawers should take up the right height without being obscured by the home indicator&lt;/li&gt;&lt;li&gt;Map or video controls near screen corners&lt;/li&gt;&lt;/ul&gt;&lt;h3 id="safe-area-inset-doesnt-provide-margins"&gt;Safe-area-inset doesn't provide margins&lt;/h3&gt;&lt;p&gt;Safe-area-insets are defined to be exactly the space that the system UI is taking up. That means they don't provide any margin between the system UI's edge and your content.&lt;/p&gt;&lt;p&gt;If you set padding to just the safe-area-inset value, your content will sit right up against the edge of the safe area, which is right up against the system UI.&lt;/p&gt;&lt;p&gt;To add some breathing room, you can add your own padding on top of the safe area insets by combining things with &lt;code&gt;calc()&lt;/code&gt;. This way you can ensure that your content is not only safe from being obscured but also has some space to breathe:&lt;/p&gt;&lt;div&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;&lt;span&gt;body&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;padding-top&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;calc&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;env&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;safe-area-inset-top&lt;span&gt;)&lt;/span&gt; + 1rem&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;padding-right&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;calc&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;env&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;safe-area-inset-right&lt;span&gt;)&lt;/span&gt; + 1rem&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;padding-bottom&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;calc&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;env&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;safe-area-inset-bottom&lt;span&gt;)&lt;/span&gt; + 1rem&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;padding-left&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;calc&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;env&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;safe-area-inset-left&lt;span&gt;)&lt;/span&gt; + 1rem&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="safe-area-inset-only-has-non-zero-values-on-mobile-devices"&gt;Safe-area-inset only has non-zero values on mobile devices&lt;/h2&gt;&lt;p&gt;&lt;code&gt;env()&lt;/code&gt; is supported across browsers and platforms, but the &lt;code&gt;safe-area-inset-*&lt;/code&gt; variables really only have non-zero values on mobile devices.&lt;/p&gt;&lt;p&gt;Desktop browsers always return 0 because there is no UI on top of pages in desktop browsers. It's only on mobile devices that these values are non-zero and that you have to account for them.&lt;/p&gt;&lt;p&gt;That's exactly why these bugs are so easy to miss. If you test in the Chrome responsive view, the safe area insets will still be 0 and you won't see any issues during development.&lt;/p&gt;&lt;p&gt;Testing on real devices is often delegated to the end of the project. By then, making layout changes to accommodate safe areas can be expensive, and the bugs can slip through to production. Even worse, they often only affect users on certain devices, so they can go unnoticed for a long time.&lt;/p&gt;&lt;h2 id="polypanes-device-emulation-supports-safe-area-insets"&gt;Polypane's device emulation supports safe area insets&lt;/h2&gt;&lt;p&gt;Polypane is the first and only desktop browser to emulate safe area insets. Every device in Polypane has correct safe area inset values for both portrait and landscape orientations.&lt;/p&gt;&lt;p&gt;Here's Polypane showing the safe area insets in blue, and the small viewport height difference in pink:&lt;/p&gt;&lt;img src="https://polypane.app/static/insetviz-be77ee8c0ab29ccda899743cd9a9b339.png" alt="a device showing safe area and small viewport overlays in Polypane"&gt;&lt;p&gt;&lt;em&gt;We're also the only desktop browser to emulate &lt;code&gt;svh&lt;/code&gt;, but that's a topic for another article.&lt;/em&gt;&lt;/p&gt;&lt;p&gt;With Polypane's safe-area-inset &lt;strong&gt;overlay visualization&lt;/strong&gt;, you can see exactly where the unsafe areas are on each device.&lt;/p&gt;&lt;h2 id="solving-a-specific-issue-floating-buttons"&gt;Solving a specific issue: Floating buttons&lt;/h2&gt;&lt;p&gt;Let's look at a specific example of how to use &lt;code&gt;safe-area-inset&lt;/code&gt; values to solve a common issue: floating buttons that end up behind the home indicator and become unreachable.&lt;/p&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;&lt;span&gt;.chat-button&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;position&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; fixed&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;right&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; 10px&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;bottom&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; 10px&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;Toggle the positioning between 'Fixed position' and 'Using env()' to switch between hard-coded offsets and offsets that use &lt;code&gt;safe-area-inset-bottom&lt;/code&gt; and &lt;code&gt;safe-area-inset-right&lt;/code&gt;.&lt;/p&gt;&lt;h2 id="safe-area-max-inset"&gt;&lt;code&gt;safe-area-max-inset&lt;/code&gt;&lt;/h2&gt;&lt;p&gt;Along with &lt;code&gt;safe-area-inset-*&lt;/code&gt;, the specification also describes &lt;code&gt;safe-area-max-inset-*&lt;/code&gt; variables. Those are not widely supported yet, but they are worth mentioning because they have slightly different behavior:&lt;/p&gt;&lt;ul&gt;&lt;li&gt;&lt;strong&gt;&lt;code&gt;safe-area-inset-*&lt;/code&gt;&lt;/strong&gt; gives you the &lt;em&gt;current&lt;/em&gt; inset value right now. On scroll, the browser chrome can collapse, and the inset value can shrink all the way to &lt;code&gt;0&lt;/code&gt;. Your element moves with that change.&lt;/li&gt;&lt;li&gt;&lt;strong&gt;&lt;code&gt;safe-area-max-inset-*&lt;/code&gt;&lt;/strong&gt; gives you the &lt;em&gt;maximum&lt;/em&gt; inset the browser can report for that edge. It stays stable even when the browser chrome collapses. Use it when you want a reserved zone that does not jump around.&lt;/li&gt;&lt;/ul&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;p&gt;Drag or scroll the phone to simulate the browser address bar appearing and disappearing.&lt;/p&gt;&lt;p&gt;The &lt;strong&gt;green button&lt;/strong&gt; uses &lt;code&gt;safe-area-inset-bottom&lt;/code&gt; and follows the current inset. The &lt;strong&gt;blue button&lt;/strong&gt; uses &lt;code&gt;safe-area-max-inset-bottom&lt;/code&gt; and stays put.&lt;/p&gt;&lt;p&gt;&lt;label&gt;Overlay&lt;/label&gt;&lt;label&gt;Show safe inset areas&lt;/label&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;p&gt;inset vs max-inset&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;p&gt;When to use which really depends on your situation. For some UI components it makes sense to move with the live viewport state, like a floating chat button that should always be just above the home indicator.&lt;/p&gt;&lt;p&gt;For other things, like a persistent cookie banner or a full-screen dialog, it can be better to reserve a stable zone that doesn't shift when the browser chrome collapses so that users don't inadvertently tap the wrong button.&lt;/p&gt;&lt;div&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;
&lt;span&gt;.floating-cta&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;bottom&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;calc&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;env&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;safe-area-inset-bottom&lt;span&gt;)&lt;/span&gt; + 1rem&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;


&lt;span&gt;.persistent-zone&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;bottom&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;calc&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;env&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;safe-area-max-inset-bottom&lt;span&gt;)&lt;/span&gt; + 1rem&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="browser-support-for-safe-area-max-inset-"&gt;Browser support for &lt;code&gt;safe-area-max-inset-*&lt;/code&gt;&lt;/h3&gt;&lt;p&gt;As of now, only Chromium implements &lt;code&gt;safe-area-max-inset-*&lt;/code&gt; so there is no support in (mobile) Safari or Firefox. That means you can't depend on it yet and should provide a fallback stack for other browsers:&lt;/p&gt;&lt;div&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;&lt;span&gt;.bottom-spacer&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;padding-bottom&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; 1rem&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;padding-bottom&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;calc&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;env&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;safe-area-inset-bottom&lt;span&gt;)&lt;/span&gt; + 1rem&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;padding-bottom&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;calc&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;env&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;safe-area-max-inset-bottom&lt;span&gt;,&lt;/span&gt; &lt;span&gt;env&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;safe-area-inset-bottom&lt;span&gt;)&lt;/span&gt;&lt;span&gt;)&lt;/span&gt; + 1rem&lt;span&gt;)&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Notice how we're using the &lt;code&gt;env()&lt;/code&gt; fallback mechanism to fall back to &lt;code&gt;safe-area-inset-bottom&lt;/code&gt; if &lt;code&gt;safe-area-max-inset-bottom&lt;/code&gt; is not supported.&lt;/p&gt;&lt;h2 id="testing-safe-areas-in-polypane"&gt;Testing safe areas in Polypane&lt;/h2&gt;&lt;p&gt;As covered earlier, safe area bugs are easy to miss during development. Chrome's responsive view always reports inset values of 0, and real-device testing tends to get pushed to the end of the project, when fixing layout issues is expensive and bugs have already reached production.&lt;/p&gt;&lt;p&gt;Polypane changes that. You can test safe area insets (and small viewport behavior) directly on your desktop, across multiple devices and orientations simultaneously, as part of your normal development workflow.&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Catch real-device layout failures during development, not after shipping.&lt;/strong&gt;&lt;/p&gt;&lt;img src="https://polypane.app/static/overlays-8190bd849571d99d1511f3cb9969653c.png" alt="mobile panes in Polypane showing visualized safe area and small viewport difference"&gt;&lt;h2 id="what-to-take-away"&gt;What to take away&lt;/h2&gt;&lt;p&gt;Safe areas are not edge cases for unusual devices. They are the reality of modern mobile devices with camera punch holes, notches, dynamic islands, and home indicator bars. Your users are on those devices, and if you want to give them the best experience, you need to make sure your content is not obscured by the system UI.&lt;/p&gt;&lt;p&gt;Make sure your viewport is set to &lt;code&gt;viewport-fit=cover&lt;/code&gt; so you get the full viewport and that you use &lt;code&gt;env(safe-area-inset-*)&lt;/code&gt; so all your content is visible and accessible. That gives users the best experience.&lt;/p&gt;&lt;p&gt;Get started with testing safe areas in Polypane today, and catch real-device layout failures during development, not after shipping.&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/body&gt;&lt;/html&gt;</content>
        <author>
            <name/>
        </author>
        <media:content medium="image" url="https://polypane.app/og-images/using-safe-area-inset-to-build-mobile-safe-layouts.png"/>
        <source>
            <id>tag:feedly.com,2013:cloud/feed/https://polypane.app/rss.xml</id>
            <title type="html">polypane.app</title>
            <link href="https://polypane.app" rel="alternate" type="text/html"/>
            <updated>2026-05-06T12:14:35Z</updated>
        </source>
    </entry>
    <entry>
        <id>tag:feedly.com,2013:cloud/entry/cOXRjoksGIQzU1pdqbdybn+OTWYeI0riptNneGckdsI=_19df28a4f04:bf55cf:ff5f181c</id>
        <title type="html">Design Token Naming Conventions: A Practical Guide</title>
        <published>2026-05-04T10:30:43Z</published>
        <updated>2026-05-04T10:30:47Z</updated>
        <link href="https://www.alwaystwisted.com/articles/design-token-naming-conventions.html" rel="alternate" type="text/html"/>
        <summary type="html">A practical guide to naming design tokens, including token tiers, common conventions, and rules that keep systems consistent and scalable.</summary>
        <content type="html">&lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"&gt;
&lt;html&gt;&lt;body&gt;&lt;div&gt;&lt;div&gt;&lt;article&gt;&lt;p&gt;
    
    &lt;/p&gt;&lt;h1&gt;Design Token Naming Conventions: A Practical Guide&lt;/h1&gt;
  
      &lt;time datetime="2026-04-23T00:00:00.000Z"&gt;23rd of April 2026&lt;/time&gt;&lt;details&gt;&lt;summary&gt;On This Page:&lt;/summary&gt;&lt;/details&gt;&lt;p&gt;If you have ever stared at a long token list and wondered whether a name should be &lt;code&gt;button-primary-bg&lt;/code&gt;, &lt;code&gt;primary-button-background&lt;/code&gt;, or &lt;code&gt;color-button-primary&lt;/code&gt;, you are not alone.&lt;/p&gt;
&lt;p&gt;Naming design tokens can look and feel simple right up until you have to do it for real. Choose a weak pattern and things get inconsistent fast. Choose a clear pattern and you get a shared language that helps both design and engineering move quicker.&lt;/p&gt;
&lt;p&gt;There is no single correct convention. A two-person team shipping one product has very different needs from a large organisation running multiple brands across multiple platforms.&lt;/p&gt;
&lt;p&gt;What you can do is pick a convention that is clear, consistent, and scalable. This article walks through the core token tiers, common naming models, and practical rules that help avoid naming drift.&lt;/p&gt;
&lt;h2 id="why-naming-gets-hard-faster-than-expected" tabindex="-1"&gt;Why naming gets hard faster than expected&lt;/h2&gt;
&lt;p&gt;Most teams don't struggle creating tokens. They struggle to keep token meaning stable as more people and components work on the Design System.&lt;/p&gt;
&lt;p&gt;You can often see multiple names emerge for the same idea:&lt;/p&gt;
&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;Code language&lt;/span&gt;json&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;"spacing.small"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;"space-2"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;"gap-md"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;"paddingStandard"&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;p&gt;Each name might make sense in isolation. Together, they signal a semantic drift. The naming model is no longer acting as a shared language.&lt;/p&gt;
&lt;p&gt;This is why naming quality can affect delivery speed. If token intent is unclear, every usage becomes a local judgement call, and those judgement calls will accumulate into inconsistency.&lt;/p&gt;
&lt;div&gt;&lt;svg&gt;&lt;/svg&gt;&lt;p&gt;The examples in this article follow the &lt;a href="https://www.designtokens.org"&gt;Design Tokens Community Group&lt;/a&gt; specification using &lt;code&gt;$value&lt;/code&gt;, &lt;code&gt;$type&lt;/code&gt;, and dot-notation aliases like &lt;code&gt;{color.brand.primary}&lt;/code&gt;.&lt;/p&gt;
&lt;/div&gt;
&lt;h2 id="what-are-design-tokens%2C-a-quick-recap" tabindex="-1"&gt;What are design tokens, a quick recap&lt;/h2&gt;
&lt;p&gt;Design tokens are a tooling and platform agnostic way to store design decisions as named values: colour, spacing, typography, radius, shadows, motion, and (much) more.&lt;/p&gt;
&lt;p&gt;The key difference is where and how many times you define those values.&lt;/p&gt;
&lt;p&gt;Without tokens, you might use &lt;code&gt;#BADA55&lt;/code&gt; in dozens of places across components, stylesheets, and design files. When that green needs to change, you might have to hunt down every instance to make sure you're updating everything that needs to be updated.&lt;/p&gt;
&lt;p&gt;With tokens, you create a design token file, a JSON object following the &lt;a href="https://www.designtokens.org"&gt;Design Tokens Community Group specification&lt;/a&gt;:&lt;/p&gt;
&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;Code language&lt;/span&gt;json&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;"green"&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;"400"&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      &lt;span&gt;"$value"&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"#BADA55"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      &lt;span&gt;"$type"&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"color"&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;p&gt;You define &lt;code&gt;#BADA55&lt;/code&gt; once as &lt;code&gt;green.400&lt;/code&gt;, then you can reference that name everywhere. The value is still defined somewhere, but it is defined in one place with a meaningful name and consumed by both design tools and code by reference via tooling like Tokens Studio or Style Dictionary.&lt;/p&gt;
&lt;p&gt;This centralisation helps to give you three major benefits:&lt;/p&gt;
&lt;ul&gt;&lt;li&gt;Better consistency across UI&lt;/li&gt;
&lt;li&gt;Easier theming and re-branding&lt;/li&gt;
&lt;li&gt;Safer changes at scale because you update once and propagate everywhere&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;Tokens are not just a technical detail. They are a shared language between design and development (and the whole team) that ends up in the product(s).&lt;/p&gt;
&lt;h2 id="the-three-token-tiers" tabindex="-1"&gt;The three token tiers&lt;/h2&gt;
&lt;p&gt;Most modern token workflows follow a three-tier model. You will see different names for each tier, but the same underlying structure keeps showing up.&lt;/p&gt;
&lt;h3 id="tier-1---primitive-tokens" tabindex="-1"&gt;Tier 1 - Primitive tokens&lt;/h3&gt;
&lt;p&gt;These are your raw values.&lt;/p&gt;
&lt;p&gt;They describe what something is, not why it exists.&lt;/p&gt;
&lt;p&gt;They are also referred to as reference, base, options, or global tokens.&lt;/p&gt;
&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;Code language&lt;/span&gt;json&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;"blue"&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;"500"&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      &lt;span&gt;"$value"&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"#0066CC"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      &lt;span&gt;"$type"&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"color"&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;"spacing"&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;"8"&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      &lt;span&gt;"$value"&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"8px"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      &lt;span&gt;"$type"&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"dimension"&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;p&gt;Primitives should not (imho) and are usually not consumed directly by components. They are source values for more specific design decisions and token tiers.&lt;/p&gt;
&lt;p&gt;A common failure is skipping straight to component naming because it feels productive in the short term. In practice, that locks in naming decisions before your core scales are stable.&lt;/p&gt;
&lt;p&gt;When possible, stabilise primitives first, attach intent later.&lt;/p&gt;
&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;Code language&lt;/span&gt;json&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;"core"&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;"dimension"&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      &lt;span&gt;"100"&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; &lt;span&gt;"$value"&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"8px"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;"$type"&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"dimension"&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      &lt;span&gt;"200"&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; &lt;span&gt;"$value"&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"16px"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;"$type"&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"dimension"&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      &lt;span&gt;"300"&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; &lt;span&gt;"$value"&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"24px"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;"$type"&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"dimension"&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;h3 id="tier-2---semantic-tokens" tabindex="-1"&gt;Tier 2 - Semantic tokens&lt;/h3&gt;
&lt;p&gt;Also known as alias, decision, theme, or system tokens.&lt;/p&gt;
&lt;p&gt;Semantic tokens can express intent. They describe why a value is used or what it is for.&lt;/p&gt;
&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;Code language&lt;/span&gt;json&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;"color"&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;"brand"&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      &lt;span&gt;"primary"&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
        &lt;span&gt;"$value"&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"{blue.500}"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
        &lt;span&gt;"$type"&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"color"&lt;/span&gt;
      &lt;span&gt;}&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;p&gt;This is where tokens become resilient.&lt;/p&gt;
&lt;p&gt;If brand blue changes, the semantic name can stay the same while the underlying primitive reference changes.&lt;/p&gt;
&lt;p&gt;You can update once and it will change everywhere.&lt;/p&gt;
&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;Code language&lt;/span&gt;json&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;"layout"&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;"spacing"&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      &lt;span&gt;"formStack"&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt; &lt;span&gt;"$value"&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"{core.dimension.300}"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;"$type"&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"dimension"&lt;/span&gt; &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      &lt;span&gt;"contentToButton"&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
        &lt;span&gt;"$value"&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"{core.dimension.200}"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
        &lt;span&gt;"$type"&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"dimension"&lt;/span&gt;
      &lt;span&gt;}&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;h3 id="tier-3---component-tokens" tabindex="-1"&gt;Tier 3 - Component tokens&lt;/h3&gt;
&lt;p&gt;Also known as "component specific" or contextual tokens.&lt;/p&gt;
&lt;p&gt;This layer maps semantic intent to specific elements, components, and parts thereof.&lt;/p&gt;
&lt;p&gt;Component tokens should always reference semantic tokens, never primitives directly. That indirection is what can keep the system flexible.&lt;/p&gt;
&lt;p&gt;It is optional, but very useful in larger systems.&lt;/p&gt;
&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;Code language&lt;/span&gt;json&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;"button"&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;"primary"&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      &lt;span&gt;"background"&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
        &lt;span&gt;"$value"&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"{color.brand.primary}"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
        &lt;span&gt;"$type"&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"color"&lt;/span&gt;
      &lt;span&gt;}&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;p&gt;Component tokens give you precision. You can tune one component without creating side effects and explosions elsewhere.&lt;/p&gt;
&lt;h2 id="common-naming-conventions" tabindex="-1"&gt;Common naming conventions&lt;/h2&gt;
&lt;p&gt;There is no universal winner ... "It depends". Different teams will always optimise for different makeup, needs, and constraints.&lt;/p&gt;
&lt;p&gt;These diagrams show the core layers for each convention. Real-world usage may add optional layers like state or namespace for more specificity.&lt;/p&gt;
&lt;h3 id="1.-category-property-modifier-(cpm)" tabindex="-1"&gt;1. Category-Property-Modifier (CPM)&lt;/h3&gt;
&lt;div&gt;
  &lt;p&gt;
    &lt;span&gt;category&lt;/span&gt;
    &lt;span&gt;property&lt;/span&gt;
    &lt;span&gt;role&lt;/span&gt;
    &lt;span&gt;state&lt;/span&gt;
  &lt;/p&gt;
&lt;/div&gt;
&lt;p&gt;This starts broad and gets more specific: category, property, role (and optionally state for interactions). It is easy to scan and keeps naming fairly lightweight.&lt;/p&gt;
&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;Code language&lt;/span&gt;json&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;"color"&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;"button"&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      &lt;span&gt;"primary"&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
        &lt;span&gt;"hover"&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
          &lt;span&gt;"$value"&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"{color.brand.primary-hover}"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
          &lt;span&gt;"$type"&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"color"&lt;/span&gt;
        &lt;span&gt;}&lt;/span&gt;
      &lt;span&gt;}&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;h3 id="2.-tier-based-naming" tabindex="-1"&gt;2. Tier-based naming&lt;/h3&gt;
&lt;div&gt;
  &lt;p&gt;
    &lt;span&gt;primitive&lt;/span&gt;
    &lt;span&gt;semantic&lt;/span&gt;
    &lt;span&gt;component&lt;/span&gt;
    &lt;span&gt;state&lt;/span&gt;
  &lt;/p&gt;
&lt;/div&gt;
&lt;p&gt;Here, the naming pattern changes depending on the tier. Primitives hold values, semantic tokens hold intent, and component tokens hold local UI decisions, which keeps each layer focused. The diagram shows the token tiers; actual nesting depth varies (e.g., primitives are often 2 layers, while component tokens may be 3+).&lt;/p&gt;
&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;Code language&lt;/span&gt;json&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;"blue"&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;"500"&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      &lt;span&gt;"$value"&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"#2196F3"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
      &lt;span&gt;"$type"&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"color"&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;"color"&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;"brand"&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      &lt;span&gt;"primary"&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
        &lt;span&gt;"$value"&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"{blue.500}"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
        &lt;span&gt;"$type"&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"color"&lt;/span&gt;
      &lt;span&gt;}&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;"button"&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;"primary"&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      &lt;span&gt;"background"&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
        &lt;span&gt;"$value"&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"{color.brand.primary}"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
        &lt;span&gt;"$type"&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"color"&lt;/span&gt;
      &lt;span&gt;}&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;h3 id="3.-object-property-modifier-(opm)" tabindex="-1"&gt;3. Object-Property-Modifier (OPM)&lt;/h3&gt;
&lt;div&gt;
  &lt;p&gt;
    &lt;span&gt;object&lt;/span&gt;
    &lt;span&gt;property&lt;/span&gt;
    &lt;span&gt;modifier&lt;/span&gt;
    &lt;span&gt;state&lt;/span&gt;
  &lt;/p&gt;
&lt;/div&gt;
&lt;p&gt;This one starts with the UI object, then narrows to property and variant. It tends to feel natural in component-led workflows because it mirrors how teams talk about UI day to day.&lt;/p&gt;
&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;Code language&lt;/span&gt;json&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;"button"&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;"background"&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      &lt;span&gt;"primary"&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
        &lt;span&gt;"hover"&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
          &lt;span&gt;"$value"&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"{color.brand.primary-hover}"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
          &lt;span&gt;"$type"&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"color"&lt;/span&gt;
        &lt;span&gt;}&lt;/span&gt;
      &lt;span&gt;}&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;h3 id="4.-context-first-naming" tabindex="-1"&gt;4. Context-first naming&lt;/h3&gt;
&lt;div&gt;
  &lt;p&gt;
    &lt;span&gt;tier&lt;/span&gt;
    &lt;span&gt;category&lt;/span&gt;
    &lt;span&gt;property&lt;/span&gt;
    &lt;span&gt;modifier&lt;/span&gt;
  &lt;/p&gt;
&lt;/div&gt;
&lt;p&gt;Context (e.g., 'brand' or 'theme') comes first, followed by category, property, and modifier. This ensures you know the layer before the specific decision. That works well when the same semantic token needs to exist across several themes or brands.&lt;/p&gt;
&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;Code language&lt;/span&gt;json&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;"brand"&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;"color"&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      &lt;span&gt;"primary"&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
        &lt;span&gt;"default"&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
          &lt;span&gt;"$value"&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"{primitive.color.blue.500}"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
          &lt;span&gt;"$type"&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"color"&lt;/span&gt;
        &lt;span&gt;}&lt;/span&gt;
      &lt;span&gt;}&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;h3 id="5.-a-comprehensive-structure" tabindex="-1"&gt;5. A Comprehensive Structure&lt;/h3&gt;
&lt;div&gt;
  &lt;p&gt;
    &lt;span&gt;namespace&lt;/span&gt;
    &lt;span&gt;category&lt;/span&gt;
    &lt;span&gt;concept&lt;/span&gt;
    &lt;span&gt;property&lt;/span&gt;
    &lt;span&gt;variant&lt;/span&gt;
    &lt;span&gt;state&lt;/span&gt;
  &lt;/p&gt;
&lt;/div&gt;
&lt;p&gt;Each segment has a specific job, from namespace through to state. This comprehensive structure allows for up to 6 layers, but start with the core 5 and add as needed for complexity. Names are longer, but they are very explicit, which helps when multiple teams and brands share the same system. Nathan Curtis's article &lt;a href="https://medium.com/eightshapes-llc/naming-tokens-in-design-systems-9e86c7444676"&gt;Naming Tokens in Design Systems&lt;/a&gt; goes much deeper on this taxonomy, it's well worth a reading (and bookmarking).&lt;/p&gt;
&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;Code language&lt;/span&gt;json&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;"acme"&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
    &lt;span&gt;"color"&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
      &lt;span&gt;"button"&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
        &lt;span&gt;"background"&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
          &lt;span&gt;"primary"&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
            &lt;span&gt;"hover"&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
              &lt;span&gt;"$value"&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"{acme.color.brand.primary}"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
              &lt;span&gt;"$type"&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"color"&lt;/span&gt;
            &lt;span&gt;}&lt;/span&gt;
          &lt;span&gt;}&lt;/span&gt;
        &lt;span&gt;}&lt;/span&gt;
      &lt;span&gt;}&lt;/span&gt;
    &lt;span&gt;}&lt;/span&gt;
  &lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;h2 id="handling-variants%2C-states%2C-and-modes" tabindex="-1"&gt;Handling variants, states, and modes&lt;/h2&gt;
&lt;p&gt;Whatever base naming model you choose, you still need clear, predictable state and variant placement patterns.&lt;/p&gt;
&lt;p&gt;One practical way to stay coherent is to define where each concept lives in the name:&lt;/p&gt;
&lt;ol&gt;&lt;li&gt;Variant: what version of the same thing it is (for example &lt;code&gt;primary&lt;/code&gt;, &lt;code&gt;secondary&lt;/code&gt;, &lt;code&gt;danger&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;State: how that version is currently behaving (for example &lt;code&gt;hover&lt;/code&gt;, &lt;code&gt;active&lt;/code&gt;, &lt;code&gt;disabled&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Mode: which environment it belongs to (for example &lt;code&gt;light&lt;/code&gt;, &lt;code&gt;dark&lt;/code&gt;, &lt;code&gt;highContrast&lt;/code&gt;)&lt;/li&gt;
&lt;/ol&gt;&lt;p&gt;If those three ideas float around in different positions, naming gets noisy fast.&lt;/p&gt;
&lt;h3 id="the-same-decision-in-different-conventions" tabindex="-1"&gt;The same decision in different conventions&lt;/h3&gt;
&lt;p&gt;These examples all express the same meaning: primary button background on hover in dark mode.&lt;/p&gt;
&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;Code language&lt;/span&gt;json&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;[&lt;/span&gt;
  &lt;span&gt;"button.primary.background.hover.dark"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;"button-background-primary-hover-dark"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;"dark.button.primary.background.hover"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;"acme.color.button.background.primary.hover.dark"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;"theme.dark.component.button.primary.background.hover"&lt;/span&gt;
&lt;span&gt;]&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;p&gt;Pick one ordering model and enforce it consistently. Mixing these patterns inside one system is where discoverability starts to break down.&lt;/p&gt;
&lt;h3 id="variants" tabindex="-1"&gt;Variants&lt;/h3&gt;
&lt;p&gt;Variants describe alternatives at the same hierarchy level, not interaction changes.&lt;/p&gt;
&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;Code language&lt;/span&gt;json&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;[&lt;/span&gt;
  &lt;span&gt;"button.primary.background"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;"button.secondary.background"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;"button.danger.background"&lt;/span&gt;
&lt;span&gt;]&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;p&gt;Equivalent variant naming in other conventions:&lt;/p&gt;
&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;Code language&lt;/span&gt;json&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;[&lt;/span&gt;
  &lt;span&gt;"button-background-primary"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;"button-background-secondary"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;"button-background-danger"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;"component.button.background.primary"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;"component.button.background.secondary"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;"component.button.background.danger"&lt;/span&gt;
&lt;span&gt;]&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;h3 id="states" tabindex="-1"&gt;States&lt;/h3&gt;
&lt;p&gt;States represent interaction or status changes for the same UI element. Keep state terms predictable (&lt;code&gt;hover&lt;/code&gt;, &lt;code&gt;active&lt;/code&gt;, &lt;code&gt;focus&lt;/code&gt;, &lt;code&gt;disabled&lt;/code&gt;) so engineers can find related tokens quickly.&lt;/p&gt;
&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;Code language&lt;/span&gt;json&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;[&lt;/span&gt;
  &lt;span&gt;"button.primary.background.hover"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;"button.primary.background.active"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;"button.primary.background.disabled"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;"button.primary.background.focus"&lt;/span&gt;
&lt;span&gt;]&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;p&gt;The same state grouping works across naming styles:&lt;/p&gt;
&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;Code language&lt;/span&gt;json&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;[&lt;/span&gt;
  &lt;span&gt;"button-background-primary-hover"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;"button-background-primary-active"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;"button-background-primary-focus"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;"button-background-primary-disabled"&lt;/span&gt;
&lt;span&gt;]&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;Code language&lt;/span&gt;json&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;[&lt;/span&gt;
  &lt;span&gt;"component.button.background.primary.hover"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;"component.button.background.primary.active"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;"component.button.background.primary.focus"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;"component.button.background.primary.disabled"&lt;/span&gt;
&lt;span&gt;]&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;h3 id="scale-sizes" tabindex="-1"&gt;Scale sizes&lt;/h3&gt;
&lt;p&gt;Scale tokens create consistent step-based sizing. Whether you use &lt;code&gt;xs-xl&lt;/code&gt; labels or numeric steps, use one scale pattern and apply it everywhere to avoid drift.&lt;/p&gt;
&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;Code language&lt;/span&gt;json&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;"spacing.sm"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;"spacing.md"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;"spacing.lg"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;p&gt;For type scales, the same rule applies: keep naming sequential so size relationships are obvious at a glance.&lt;/p&gt;
&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;Code language&lt;/span&gt;json&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;"font.size.sm"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;"font.size.md"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;"font.size.lg"&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;h3 id="modes-and-themes" tabindex="-1"&gt;Modes and themes&lt;/h3&gt;
&lt;p&gt;Modes are contextual variants of the same decision (for example light and dark). The token purpose stays the same, only the value changes per mode.&lt;/p&gt;
&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;Code language&lt;/span&gt;json&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;"color.background.default.light"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;"color.background.default.dark"&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;p&gt;Other mode-placement patterns you will see in real systems:&lt;/p&gt;
&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;Code language&lt;/span&gt;json&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;[&lt;/span&gt;
  &lt;span&gt;"light.color.background.default"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;"dark.color.background.default"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;"theme.light.color.background.default"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;"theme.dark.color.background.default"&lt;/span&gt;
&lt;span&gt;]&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;p&gt;Whichever pattern you choose, keep mode placement fixed. A stable suffix or a stable prefix both work; switching between them does not.&lt;/p&gt;
&lt;h3 id="numbered-scales" tabindex="-1"&gt;Numbered scales&lt;/h3&gt;
&lt;p&gt;Numbered scales are useful when you need many fine-grained steps, especially for colour ramps and heading systems. The key is to keep the numbering direction and increments consistent.&lt;/p&gt;
&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;Code language&lt;/span&gt;json&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;[&lt;/span&gt;
  &lt;span&gt;"font.size.heading.1"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;"font.size.heading.2"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;"font.size.heading.3"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;"color.gray.50"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;"color.gray.100"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;"color.gray.200"&lt;/span&gt;
&lt;span&gt;]&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;p&gt;Numbered scales also appear in alternative patterns:&lt;/p&gt;
&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;Code language&lt;/span&gt;json&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;[&lt;/span&gt;
  &lt;span&gt;"size-font-heading-100"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;"size-font-heading-200"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;"size-font-heading-300"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;"scale.type.heading.100"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;"scale.type.heading.200"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;"scale.type.heading.300"&lt;/span&gt;
&lt;span&gt;]&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;h2 id="rules-that-keep-token-naming-healthy" tabindex="-1"&gt;Rules that keep token naming healthy&lt;/h2&gt;
&lt;p&gt;Most strong naming systems follow a predictable anatomy: an optional prefix (namespace or context), a core meaning (category and property), optional modifiers (intent, variant, scale), and an optional suffix (state or mode).&lt;/p&gt;
&lt;p&gt;You do not need every part in every token, but when a part is present it should always appear in the same position.&lt;/p&gt;
&lt;h3 id="1.-consistency-beats-perfection" tabindex="-1"&gt;1. Consistency beats perfection&lt;/h3&gt;
&lt;p&gt;The best naming system is the one your whole team actually follows.&lt;/p&gt;
&lt;p&gt;Drifting tends to start not where the convention is broken outright, but where it is ambiguous enough to invite interpretation. A token set that mixes naming styles makes it impossible to predict what a new token should be called.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Avoid:&lt;/strong&gt; mixing conventions across the same token tier&lt;/p&gt;
&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;Code language&lt;/span&gt;json&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;[&lt;/span&gt;
  &lt;span&gt;"color.button.primary.default"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;"button.background.primary.hover"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;"theme.dark.component.button.primary.background.default"&lt;/span&gt;
&lt;span&gt;]&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;p&gt;&lt;strong&gt;Prefer:&lt;/strong&gt; one convention applied throughout&lt;/p&gt;
&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;Code language&lt;/span&gt;json&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;[&lt;/span&gt;
  &lt;span&gt;"component.button.primary.background.default"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;"component.button.primary.background.hover"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;"component.button.secondary.background.default"&lt;/span&gt;
&lt;span&gt;]&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;h3 id="2.-optimise-for-clarity-over-brevity" tabindex="-1"&gt;2. Optimise for clarity over brevity&lt;/h3&gt;
&lt;p&gt;Autocompletion means typing long names is rarely a problem, but reading a token list at a glance still relies on human pattern recognition. Abbreviating too aggressively trades a small typing convenience for an ongoing cognitive overhead every time someone reads or audits the system.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Avoid&lt;/strong&gt; abbreviations that require decoding&lt;/p&gt;
&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;Code language&lt;/span&gt;json&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;"btn-bg-pri-hvr"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;"c-txt-err"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;"sp-md"&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;p&gt;&lt;strong&gt;Prefer&lt;/strong&gt; names that read on first glance&lt;/p&gt;
&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;Code language&lt;/span&gt;json&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;"button-background-primary-hover"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;"color-text-error"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;"spacing-md"&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;h3 id="3.-keep-semantics-semantic" tabindex="-1"&gt;3. Keep semantics semantic&lt;/h3&gt;
&lt;p&gt;A name like &lt;code&gt;color-text-red&lt;/code&gt; describes a visual value, which means the name breaks as soon as the colour changes. A name like &lt;code&gt;color-text-error&lt;/code&gt; describes intent, which stays stable even when the underlying value does not. At semantic and component levels, name for why, not what.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Avoid:&lt;/strong&gt; naming for the visual value&lt;/p&gt;
&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;Code language&lt;/span&gt;json&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;"color.text.red"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;"color.background.grey"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;"color.border.green"&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;p&gt;&lt;strong&gt;Prefer:&lt;/strong&gt; naming for the intent&lt;/p&gt;
&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;Code language&lt;/span&gt;json&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;"color.text.error"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;"color.background.subtle"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;"color.border.success"&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;h3 id="4.-order-from-broad-to-specific" tabindex="-1"&gt;4. Order from broad to specific&lt;/h3&gt;
&lt;p&gt;Names are easier to scan when they move from the widest concept toward specificity. This also means that alphabetically sorted token lists naturally group related tokens together, which makes auditing and searching much faster.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Avoid:&lt;/strong&gt; specificity before category&lt;/p&gt;
&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;Code language&lt;/span&gt;json&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;"hover.primary.button.color"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;"disabled.background.input"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;"lg.font.heading"&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;p&gt;&lt;strong&gt;Prefer:&lt;/strong&gt; broad category first, narrowing toward specificity&lt;/p&gt;
&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;Code language&lt;/span&gt;json&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;[&lt;/span&gt;
  &lt;span&gt;"color.button.primary.hover"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;"color.input.background.disabled"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;"font.heading.lg"&lt;/span&gt;
&lt;span&gt;]&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;h3 id="5.-make-names-self-explanatory" tabindex="-1"&gt;5. Make names self-explanatory&lt;/h3&gt;
&lt;p&gt;A developer who has never seen your design tokens before should be able to make a reasonable guesstimate about what it does. If a name requires specific knowledge or a colleague to interpret, it is a candidate for renaming.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Avoid:&lt;/strong&gt; names that only make sense in context&lt;/p&gt;
&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;Code language&lt;/span&gt;json&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;"token-1"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;"c-bd-x"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;"primary-a"&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;p&gt;&lt;strong&gt;Prefer:&lt;/strong&gt; names that explain themselves&lt;/p&gt;
&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;Code language&lt;/span&gt;json&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;[&lt;/span&gt;
  &lt;span&gt;"button.border.radius"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;"color.border.focus"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
  &lt;span&gt;"color.button.background.primary"&lt;/span&gt;
&lt;span&gt;]&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;h3 id="6.-design-for-growth" tabindex="-1"&gt;6. Design for growth&lt;/h3&gt;
&lt;p&gt;Your naming model should be able to absorb more components, more themes, and potentially more brands without a heavy structural rewrite. That does not mean you need to "over engineer" from the start, but it does mean avoiding patterns that only work at small scale, like implicit positional meaning or naming steps after their current count.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Avoid:&lt;/strong&gt; patterns that hit a ceiling&lt;/p&gt;
&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;Code language&lt;/span&gt;json&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;"spacing-small"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;"spacing-smaller"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;"spacing-new"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;"spacing-new-2"&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;p&gt;&lt;strong&gt;Prefer:&lt;/strong&gt; a scale that can always grow in either direction&lt;/p&gt;
&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;Code language&lt;/span&gt;json&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;[&lt;/span&gt;&lt;span&gt;"spacing.100"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;"spacing.200"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;"spacing.300"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;"spacing.420"&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;h2 id="choosing-your-convention" tabindex="-1"&gt;Choosing your convention&lt;/h2&gt;
&lt;p&gt;A practical way to decide is to consider your team size, system scope, tooling, and existing patterns together.&lt;/p&gt;
&lt;p&gt;Larger teams will need a stronger structure, single product systems can probably stay simpler, and tools like Figma, Tokens Studio, and your build pipeline may naturally favour one style.&lt;/p&gt;
&lt;p&gt;It is also usually safer to evolve existing patterns than replace everything at once, and the best outcome comes when designers and developers both buy into the same model.&lt;/p&gt;
&lt;p&gt;Start simple, document it clearly, add examples, and revisit as the system grows.&lt;/p&gt;
&lt;p&gt;Token names do not need to be academically perfect. They need to be clear enough that your team can apply them without hesitation.&lt;/p&gt;
&lt;h2 id="keep-naming-alive-with-lightweight-governance" tabindex="-1"&gt;Keep naming alive with lightweight governance&lt;/h2&gt;
&lt;p&gt;Naming is not a one time decision.&lt;/p&gt;
&lt;p&gt;It is an ongoing Design System practice.&lt;/p&gt;
&lt;p&gt;If you want conventions to hold up over time, define lightweight governance:&lt;/p&gt;
&lt;ol&gt;&lt;li&gt;One canonical naming guide with approved examples&lt;/li&gt;
&lt;li&gt;A short review checklist for new tokens&lt;/li&gt;
&lt;li&gt;Periodic cleanup of duplicate or ambiguous names&lt;/li&gt;
&lt;li&gt;Shared ownership across design and engineering&lt;/li&gt;
&lt;/ol&gt;&lt;p&gt;The goal is not "process for process' sake".&lt;/p&gt;
&lt;p&gt;A token system in a Design System that people trust is one they will actually use, and consistent naming is how that trust gets built.&lt;/p&gt;


    
      
      
    
    
    

  &lt;/article&gt;&lt;/div&gt;&lt;/div&gt;&lt;/body&gt;&lt;/html&gt;</content>
        <author>
            <name/>
        </author>
        <media:content medium="image" url="https://news.design.systems/embed?color1=ffffff&amp;amp;color2=2c2c2d&amp;amp;color_bg_button=2c2c2d&amp;amp;color_border=ccc&amp;amp;color_button=ffffff&amp;amp;color_links=979797&amp;amp;color_terms=808080&amp;amp;title=Subscribe+to+Design+Systems+News"/>
        <link href="/images/articles/meta-images/design-tokens-naming.png" rel="enclosure" type="image/png"/>
        <source>
            <id>tag:feedly.com,2013:cloud/feed/https://www.alwaystwisted.com/feed.xml</id>
            <title type="html">Always Twisted</title>
            <link href="https://www.alwaystwisted.com" rel="alternate" type="text/html"/>
            <updated>2026-05-04T10:30:47Z</updated>
        </source>
    </entry>
    <entry>
        <id>tag:feedly.com,2013:cloud/entry/cOXRjoksGIQzU1pdqbdybn+OTWYeI0riptNneGckdsI=_19ded75aef3:8a7f89:3a607295</id>
        <title type="html">Upgrading Forgejo with S3 Object Storage and Actions!</title>
        <published>2026-05-03T10:50:05Z</published>
        <updated>2026-05-03T10:50:09Z</updated>
        <link href="https://blog.alexsguardian.net/posts/2024/06/03/upgradingforgejo" rel="alternate" type="text/html"/>
        <summary type="html">Migrating my Forgejo server to s3 object storage and adding action runners for workflows!</summary>
        <content type="html">&lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"&gt;
&lt;html&gt;&lt;body&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;hr&gt;&lt;p&gt;Awhile ago I spun up my own instance of Forgejo. &lt;a href="https://forgejo.org"&gt;Forgejo&lt;/a&gt;, if you are not familiar, is a hard fork of &lt;a href="https://gitea.com"&gt;Gitea&lt;/a&gt; when they became a &lt;em&gt;for-profit&lt;/em&gt; company and is run/managed by &lt;a href="https://codeberg.org"&gt;Codeberg&lt;/a&gt;. If you want more details on why Forgejo came to be, you can read their blog post &lt;a href="https://forgejo.org/2022-12-15-hello-forgejo/"&gt;here&lt;/a&gt;. As of now it’s basically a drop-in replacement for Gitea. Though over time this will change as the two projects drift apart. So it may not be one in the future! Anyhow, I’ve been rolling my own instance since September 2023 and, recently I have been moving my entire lab towards a more &lt;a href="https://about.gitlab.com/topics/gitops/"&gt;GitOps&lt;/a&gt; approach. To do this and do it securely (in the confines of my internal network), I’ve decided to turn Forgejo into my full CI/CD manager and config storage place. I’ll also be using &lt;a href="https://bitwarden.com/products/secrets-manager/"&gt;Bitwarden’s Secret Manager&lt;/a&gt; to manage, well, secrets.&lt;/p&gt;&lt;p&gt;Before I begin re-doing my lab for the umpteenth time, I need to change how Forgejo operates currently. That way Forgejo can support the transition from ‘FileOps’ (aka loose files) to GitOps. Now this isn’t the ‘perfect’ setup process as the repo storage still lives in /data, and it’s not currently replicated across my small swarm cluster. This will change when I get to actually rebuilding the cluster from the ground up to utilize NFS mounts for shared storage. Though that requires more hardware, which the wife probably won’t be happy about. &lt;span&gt;&#128517;&lt;/span&gt;&lt;/p&gt;&lt;h2 id="migrating-to-s3-object-storage"&gt;Migrating to S3 Object Storage&lt;/h2&gt;&lt;p&gt;Forgejo (in Docker), by default, has you map a &lt;code&gt;/data&lt;/code&gt; directory where all the necessary configs/repos/logs/etc. live. As you can see by the example compose file &lt;a href="https://forgejo.org/docs/latest/admin/installation-docker/"&gt;here&lt;/a&gt;.&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;docker-compose.yml&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;image&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;codeberg.org/forgejo/forgejo:7&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;./forgejo:/data&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;# &amp;lt;---- data volume&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;/etc/timezone:/etc/timezone:ro&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;/etc/localtime:/etc/localtime:ro&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;This is great if you plan on just running a simple setup, or you already have shared storage. For me though, I have no shared storage, and a &lt;a href="https://min.io/"&gt;MinIO&lt;/a&gt; server that is currently doing nothing but holding attachments for &lt;a href="https://github.com/outline/outline"&gt;Outline&lt;/a&gt;. I figured it’s time to put it to more use. That, and my day job involves doing AWS things so figured it would be good practice.&lt;/p&gt;&lt;p&gt;Currently, Forgejo supports the following directories (under &lt;code&gt;/appdata&lt;/code&gt;) for object storage.&lt;/p&gt;&lt;div&gt;&lt;table&gt;&lt;tbody&gt;&lt;tr&gt;&lt;th&gt;Subsystem&lt;/th&gt;&lt;th&gt;Directory&lt;/th&gt;&lt;th&gt;app.ini Sections&lt;/th&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Attachments&lt;/td&gt;&lt;td&gt;attachments/&lt;/td&gt;&lt;td&gt;[attachment]&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;LFS&lt;/td&gt;&lt;td&gt;lfs/&lt;/td&gt;&lt;td&gt;[lfs]&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Avatars&lt;/td&gt;&lt;td&gt;avatars/&lt;/td&gt;&lt;td&gt;[avatar]&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Repository avatars&lt;/td&gt;&lt;td&gt;repo-avatars/&lt;/td&gt;&lt;td&gt;[repo-avatar]&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Repository archives&lt;/td&gt;&lt;td&gt;repo-archive/&lt;/td&gt;&lt;td&gt;[repo-archive]&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Packages&lt;/td&gt;&lt;td&gt;packages/&lt;/td&gt;&lt;td&gt;[packages]&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Actions logs&lt;/td&gt;&lt;td&gt;actions_log/&lt;/td&gt;&lt;td&gt;[storage.actions_log]&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Actions Artifacts&lt;/td&gt;&lt;td&gt;actions_artifacts/&lt;/td&gt;&lt;td&gt;[actions.artifacts]&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;caption&gt;&lt;a href="https://forgejo.org/docs/latest/admin/storage/"&gt;Forgejo Docs: Storage Settings&lt;/a&gt;&lt;/caption&gt;&lt;/table&gt;&lt;/div&gt;&lt;p&gt;As you can see in the table above there are a few storage directories missing. Mainly repositories. Since repositories do not work well in object storage they are unsupported. Which means they will still be living on one of my hosts temporarily until I finalize my cross cluster storage solution. In order migrate the storage to MinIO, I first need to create a bucket (named &lt;code&gt;forgejo&lt;/code&gt;), an Access Identity, and an ACL for it.&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;access-credentials&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;p&gt;&lt;span&gt;Access_key:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;O3n9bNhrbadkeyAGKOuhIUzMGF&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;span&gt;Secret_key:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;czgQT1fb1l3GD2nRQEPebadsecretbWG0xGIu20hjOeBfoSGo&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;Access Key User Policy&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;"s3:ListBucketMultipartUploads"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;"s3:AbortMultipartUpload"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;"s3:ListMultipartUploadParts"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;The above ACL replaces the default one under the “Current User Policy” after enabling the “ON” switch for “Restrict beyond user policy” setting in &lt;code&gt;User &amp;gt; Access Keys &amp;gt; Create access key&lt;/code&gt;. It limits the access key to the &lt;code&gt;forgejo&lt;/code&gt; bucket. The bucket is also private by default, so I do not have to change the bucket ACL.&lt;/p&gt;&lt;p&gt;&lt;img alt="Create Access Key" src="https://blog.alexsguardian.net/_astro/fj-create-access-key.2JyFthvY_11P6bC.webp"&gt;&lt;span&gt;MinIO Create Access Key with a custom user policy&lt;/span&gt;&lt;/p&gt;&lt;p&gt;Next was to upload the existing files to MinIO so that when Forgejo restarted with the new s3 config it would pick them back up. As you can see in the picture below there are a few absent directories. I decided to condense a few related directories into parent ones for my ease of use. Also, some of them do not exist yet and will be created when needed (e.g. packages/).&lt;/p&gt;&lt;p&gt;&lt;img alt="forgejo bucket dir 1" src="https://blog.alexsguardian.net/_astro/fj-s3-dir_1.DFDEWvjv_wh5yr.webp"&gt;&lt;span&gt;Example MinIO bucket directory layout&lt;/span&gt;&lt;/p&gt;&lt;p&gt;My current s3 bucket directory layout:&lt;/p&gt;&lt;p&gt;With MinIO ready to go, all I had to do was update the Forgejo container environment variables for the MinIO connection and re-deploy. Forgejo environment variables can be defined using the &lt;code&gt;env -&amp;gt; ini&lt;/code&gt; notation. So in your &lt;code&gt;app.ini&lt;/code&gt; you have sections that are labeled, such as ‘[security].’ In order to update the INI value for say, ‘INSTALL_LOCK’, you’d need to define it as &lt;code&gt;FORGEJO__security__INSTALL_LOCK&lt;/code&gt;.&lt;/p&gt;&lt;p&gt;As you can see in the example snippet file below, I am defining the MinIO connection information, as well as, where to look for specific directories in the bucket.&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;forgejo-minio-env-example&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;p&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;FORGEJO__storage__STORAGE_TYPE="minio"&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;FORGEJO__storage__MINIO_USE_SSL="true"&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;FORGEJO__storage__MINIO_ENDPOINT="s3.${LAB_DOMAIN}"&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;FORGEJO__storage__MINIO_ACCESS_KEY_ID=${FJ_S3_ACCESS_ID}&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;FORGEJO__storage__MINIO_SECRET_ACCESS_KEY=${FJ_S3_ACCESS_KEY}&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;FORGEJO__storage__MINIO_BUCKET="forgejo"&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;FORGEJO__storage__MINIO_LOCATION="us-east-1"&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;FORGEJO__attachment__MINIO_BASE_PATH="attachments/"&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;FORGEJO__lfs__MINIO_BASE_PATH="lfs/"&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;FORGEJO__avatar__MINIO_BASE_PATH="avatars/users/"&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;FORGEJO__repo-avatar__MINIO_BASE_PATH="avatars/repositories/"&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;FORGEJO__repo-archive__MINIO_BASE_PATH="archives/"&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;FORGEJO__packages__MINIO_BASE_PATH="packages/"&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;FORGEJO__storage.actions_log__MINIO_BASE_PATH="actions/logs/"&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;FORGEJO__actions.artifacts__MINIO_BASE_PATH="actions/artifacts/"&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;&lt;a name="compose-example"&gt;&lt;/a&gt;&lt;details&gt;&lt;summary&gt;Forgejo Compose Example [Expand me]&lt;/summary&gt;&lt;div&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;forgejo.yml&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;test&lt;/span&gt;&lt;span&gt;: [&lt;/span&gt;&lt;span&gt;"CMD"&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;"curl"&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;"-fSs"&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;"localhost:3000/api/healthz"&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;image&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;codeberg.org/forgejo/forgejo:7.0&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;FORGEJO_APP_NAME="Forgejo"&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;FORGEJO_RUN_MODE="prod"&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;FORGEJO_WORK_PATH="/data/gitea"&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;FORGEJO__repository__ROOT="/data/git/repositories"&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;FORGEJO__repository.local__LOCAL_COPY_PATH="/data/gitea/tmp/local-repo"&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;FORGEJO__repository.pull-request__DEFAULT_MERGE_STYLE="merge"&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;FORGEJO__repository.signing__DEFAULT_TRUST_MODEL="committer"&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;FORGEJO__server__APP_DATA_PATH="/data/gitea"&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;FORGEJO__server__DOMAIN="git.${LAB_DOMAIN}"&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;FORGEJO__server__SSH_DOMAIN="git.${LAB_DOMAIN}"&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;FORGEJO__server__HTTP_PORT="3000"&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;FORGEJO__server__ROOT_URL="https://git.${LAB_DOMAIN}"&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;FORGEJO__server__DISABLE_SSH="false"&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;FORGEJO__server__SSH_PORT="22"&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;FORGEJO__server__SSH_LISTEN_PORT="22"&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;FORGEJO__server__LFS_START_SERVER="true"&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;FORGEJO__server__LFS_JWT_SECRET="${FJ_JWT_SECRET}"&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;FORGEJO__server__OFFLINE_MODE="false"&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;FORGEJO__database__DB_TYPE="postgres"&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;FORGEJO__database__HOST="${FJ_DB_HOST}"&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;FORGEJO__database__NAME="${FJ_DB}"&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;FORGEJO__database__USER="${FJ_DB_USER}"&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;FORGEJO__database__PASSWD="${FJ_DB_PASS}"&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;FORGEJO__database__LOG_SQL="false"&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;FORGEJO__database__SCHEMA=""&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;FORGEJO__database__SSL_MODE="disable"&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;FORGEJO__indexer__ISSUE_INDEXER_PATH="/data/gitea/indexers/issues.bleve"&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;FORGEJO__session__PROVIDER_CONFIG="/data/gitea/sessions"&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;FORGEJO__session__PROVIDER="file"&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;FORGEJO__session__COOKIE_SECURE="true"&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;FORGEJO__log__MODE="console"&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;FORGEJO__log__LEVEL="info"&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;FORGEJO__log__ROOT_PATH="/data/gitea/log"&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;FORGEJO__security__INSTALL_LOCK="true"&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;FORGEJO__security__SECRET_KEY="${FJ_SECRET_KEY}"&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;FORGEJO__security__REVERSE_PROXY_LIMIT="1"&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;FORGEJO__security__REVERSE_PROXY_TRUSTED_PROXIES="${CADDY_INT_IP}"&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;FORGEJO__security__INTERNAL_TOKEN="${FJ_INT_TOKEN}"&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;FORGEJO__security__PASSWORD_HASH_ALGO="${FJ_PASS_ALGO}"&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;FORGEJO__service__DISABLE_REGISTRATION="true"&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;FORGEJO__service__REQUIRE_SIGNIN_VIEW="false"&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;FORGEJO__service__REGISTER_EMAIL_CONFIRM="false"&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;FORGEJO__service__ENABLE_NOTIFY_MAIL="false"&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;FORGEJO__service__ALLOW_ONLY_EXTERNAL_REGISTRATION="false"&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;FORGEJO__service__ENABLE_CAPTCHA="true"&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;FORGEJO__service__CAPTCHA_TYPE="cfturnstile"&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;FORGEJO__service__REQUIRE_CAPTCHA_FOR_LOGIN="true"&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;FORGEJO__service__CF_TURNSTILE_SECRET="${FJ_CFT_SECRET}"&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;FORGEJO__service__CF_TURNSTILE_SITEKEY="${FJ_CFT_SITE_KEY}"&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;FORGEJO__service__DEFAULT_KEEP_EMAIL_PRIVATE="true"&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;FORGEJO__service__DEFAULT_ALLOW_CREATE_ORGANIZATION="true"&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;FORGEJO__service__DEFAULT_ENABLE_TIMETRACKING="true"&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;FORGEJO__service__NO_REPLY_ADDRESS="noreply@${FJ_DOMAIN}"&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;FORGEJO__mailier__ENABLED="false"&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;FORGEJO__openid__ENABLE_OPENID_SIGNIN="false"&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;FORGEJO__openid__ENABLE_OPENID_SIGNUP="false"&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;FROGEJO__cron.update_checker__ENABLED="false"&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;FORGEJO__oauth2__JWT_SECRET="${FJ_OAUTH_SEC}"&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;FORGEJO__actions__ENABLED="true"&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;FORGEJO__actions__DEFAULT_ACTIONS_URL="https://git.${LAB_DOMAIN}"&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;FORGEJO__actions__ARTIFACT_RETENTION_DAYS="90"&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;FORGEJO__storage__STORAGE_TYPE="minio"&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;FORGEJO__storage__MINIO_USE_SSL="true"&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;FORGEJO__storage__MINIO_ENDPOINT="s3.${LAB_DOMAIN}"&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;FORGEJO__storage__MINIO_ACCESS_KEY_ID=${FJ_S3_ACCESS_ID}&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;FORGEJO__storage__MINIO_SECRET_ACCESS_KEY=${FJ_S3_ACCESS_KEY}&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;FORGEJO__storage__MINIO_BUCKET="forgejo"&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;FORGEJO__storage__MINIO_LOCATION="us-east-1"&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;FORGEJO__attachment__MINIO_BASE_PATH="attachments/"&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;FORGEJO__lfs__MINIO_BASE_PATH="lfs/"&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;FORGEJO__avatar__MINIO_BASE_PATH="avatars/users/"&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;FORGEJO__repo-avatar__MINIO_BASE_PATH="avatars/repositories/"&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;FORGEJO__repo-archive__MINIO_BASE_PATH="archives/"&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;FORGEJO__packages__MINIO_BASE_PATH="packages/"&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;FORGEJO__storage.actions_log__MINIO_BASE_PATH="actions/logs/"&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;FORGEJO__actions.artifacts__MINIO_BASE_PATH="actions/artifacts/"&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;/etc/timezone:/etc/timezone:ro&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;/etc/localtime:/etc/localtime:ro&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;&lt;aside&gt;&lt;p&gt;&lt;svg&gt;&lt;/svg&gt; Tip&lt;/p&gt;&lt;section&gt;&lt;p&gt;If you are curious about all the configuration options for Forgejo, you can check out the configuration cheat sheet &lt;a href="https://forgejo.org/docs/latest/admin/config-cheat-sheet/"&gt;here&lt;/a&gt;.&lt;/p&gt;&lt;/section&gt;&lt;/aside&gt;&lt;/div&gt;&lt;/details&gt;&lt;p&gt;With the environment variables updated, and a quick redeploy of Forgejo, I was up and running with object storage via MinIO! A quick trip into the &lt;code&gt;Site Administration &amp;gt; Configuration &amp;gt; Summary&lt;/code&gt; in Forgejo shows that LFS was now using MinIO as seen in the below image.&lt;/p&gt;&lt;p&gt;&lt;img alt="FJ Site Admin Config for LFS" src="https://blog.alexsguardian.net/_astro/fj-site-admin.BuBy27dw_1f0fNQ.webp"&gt;&lt;span&gt;Forgejo site admin config showing LFS using MinIO&lt;/span&gt;&lt;/p&gt;&lt;h2 id="setting-up-forgejo-actions"&gt;Setting up Forgejo Actions&lt;/h2&gt;&lt;p&gt;&lt;img alt="FJ Actions Meme" src="https://blog.alexsguardian.net/_astro/fj_actions_meme.CHp604cc_1rm3bC.webp"&gt;&lt;/p&gt;&lt;p&gt;All jokes aside, Forgejo Actions is actually fairly decent. The runner is not production ready (according to &lt;a href="https://code.forgejo.org/forgejo/runner#forgejo-runner"&gt;Forgejo themselves&lt;/a&gt;) but it works well enough that I am going all in on it for my lab. Setting up the action runner was fairly straight forward. It’s got a similar setup process to the self-hosted GitHub runner.&lt;/p&gt;&lt;ol&gt;&lt;li&gt;Enable Actions in the &lt;code&gt;app.ini&lt;/code&gt; or using environment variables. Restart/Redeploy Forgejo.&lt;/li&gt;&lt;/ol&gt;&lt;ul&gt;&lt;li&gt;&lt;a href="https://blog.alexsguardian.net/posts/2024/06/03/upgradingforgejo#compose-example"&gt;See above&lt;/a&gt; for an example using environment variables under &lt;code&gt;#Actions Configs&lt;/code&gt;.&lt;/li&gt;&lt;/ul&gt;&lt;ol start="2"&gt;&lt;li&gt;Create a new runner in &lt;code&gt;Site Administration &amp;gt; Actions &amp;gt; Runners &amp;gt; Create new runner&lt;/code&gt;.&lt;/li&gt;&lt;li&gt;Copy the new runner registration token.&lt;/li&gt;&lt;li&gt;Deploy Forgejo Runner and register it with the token from step 3.&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;Now there are a few ways you could deploy the runner, but I decided to deploy it in Docker. You can bind the runner container to the Docker sock, but I did not. I really do not like doing that so, I ended up using &lt;a href="https://hub.docker.com/_/docker"&gt;Docker in Docker (DIND)&lt;/a&gt;. It’s not really recommended to use Docker this way but for my lab it will do fine. The compose file was also pretty easy to set up.&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;forgejo-actions.yml&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;command&lt;/span&gt;&lt;span&gt;: [&lt;/span&gt;&lt;span&gt;'dockerd'&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;'-H'&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;'tcp://0.0.0.0:2375'&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;'--tls=false'&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;image&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;code.forgejo.org/forgejo/runner:3.4.1&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;condition&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;service_started&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;command&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;'/bin/sh -c "while : ; do sleep 1 ; done ;"'&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;# Change to this command after registration&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;#command: '/bin/sh -c "sleep 5; forgejo-runner daemon"'&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;DOCKER_HOST=tcp://dind:2375&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;As you can see above there are two &lt;code&gt;commands:&lt;/code&gt;. &lt;code&gt;'/bin/sh -c "while : ; do sleep 1 ; done ;"'&lt;/code&gt; starts up the runner container and does nothing. This allows you to enter the container via &lt;code&gt;docker exec&lt;/code&gt; and run the registration command using the token generated in the Site Administration settings. After registering the container, it can be brought down and the current command can be replaced with the commented out one (&lt;code&gt;'/bin/sh -c "sleep 5; forgejo-runner daemon"'&lt;/code&gt;). Then after redeploying the container the runner will become available in Forgejo globally. Having a global runner means it can be used by all repositories. Which for me works fine since my Forgejo instance is mainly private except for a few public repositories and a few GitHub mirrors.&lt;/p&gt;&lt;p&gt;&lt;img alt="FJ Manage Runners" src="https://blog.alexsguardian.net/_astro/fj-manage-runners.Bgx3xUe8_ZdqLTf.webp"&gt;&lt;span&gt;Forgejo Manage Runners&lt;/span&gt;&lt;/p&gt;&lt;p&gt;With my runner online and working, I am now able to have workflows kick off from steps defined in files located in &lt;code&gt;.forgejo/workflows/&lt;/code&gt;. The syntax is fairly similar to GitHub actions so most of the actions you can run there can run in Forgejo. The best part is I can mirror the action repositories and have my runner pull them directly from my Forgejo instance. The first thing I set up was Renovate Bot to keep my Docker images updated. The bot runs on a cron based workflow as seen below and opens a PR for image updates.&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;.forgejo/workflows/renovate.yml&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;&lt;span&gt;    &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;cron&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;'0 0/6 * * *'&lt;/span&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;# every 6 hours, every day&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;RENOVATE_REPOSITORIES&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;${{ github.repository }}&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;if&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;${{ secrets.RENOVATE_TOKEN != '' }}&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;image&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;renovate/renovate:full&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;          &lt;/span&gt;&lt;span&gt;GITHUB_COM_TOKEN&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;${{ secrets.RENOVATE_GITHUB_COM_TOKEN }}&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;          &lt;/span&gt;&lt;span&gt;RENOVATE_BASE_DIR&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;${{ github.workspace }}/.tmp&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;          &lt;/span&gt;&lt;span&gt;RENOVATE_ENDPOINT&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;${{ github.server_url }}&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;          &lt;/span&gt;&lt;span&gt;RENOVATE_REPOSITORY_CACHE&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;'enabled'&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;          &lt;/span&gt;&lt;span&gt;RENOVATE_TOKEN&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;${{ secrets.RENOVATE_TOKEN }}&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;          &lt;/span&gt;&lt;span&gt;RENOVATE_GIT_AUTHOR&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;'Renovate Bot &amp;lt;renovate@mydomain.dev&amp;gt;'&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;          &lt;/span&gt;&lt;span&gt;GIT_AUTHOR_NAME&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;'Renovate'&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;          &lt;/span&gt;&lt;span&gt;GIT_AUTHOR_EMAIL&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;'renovate@mydomain.dev'&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;          &lt;/span&gt;&lt;span&gt;GIT_COMMITTER_NAME&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;'Renovate Bot'&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;          &lt;/span&gt;&lt;span&gt;GIT_COMMITTER_EMAIL&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;'renovate@mydomain.dev'&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;name&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;Get Ntfy Token from SM&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;uses&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;actions/sm-action@v4&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;access_token&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;${{ secrets.BITWARDEN_LAB_KEY }}&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;&lt;span&gt;          &lt;/span&gt;&lt;/span&gt;&lt;span&gt;some-secret-id-string &amp;gt; NTFY_TOKEN&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;&lt;span&gt;          &lt;/span&gt;&lt;/span&gt;&lt;span&gt;some-secret-id-string &amp;gt; LAB_DOMAIN&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;        &lt;/span&gt;&lt;span&gt;uses&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;actions/ntfy-action@master&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;          &lt;/span&gt;&lt;span&gt;url&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;'https://ntfy.${LAB_DOMAIN}'&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;          &lt;/span&gt;&lt;span&gt;headers&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;'{"authorization": "bearer $NTFY_TOKEN"}'&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;          &lt;/span&gt;&lt;span&gt;details&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;Renovate Bot failed to run!&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;The bot is configured via a &lt;code&gt;renovate.json&lt;/code&gt; file located in the root of the repository.&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;renovate.json&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;"$schema"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;"https://docs.renovatebot.com/renovate-schema.json"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;"extends"&lt;/span&gt;&lt;span&gt;: [&lt;/span&gt;&lt;span&gt;"config:base"&lt;/span&gt;&lt;span&gt;],&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;"assignees"&lt;/span&gt;&lt;span&gt;: [&lt;/span&gt;&lt;span&gt;"alexandzors"&lt;/span&gt;&lt;span&gt;],&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;"dependencyDashboard"&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;"labels"&lt;/span&gt;&lt;span&gt;: [&lt;/span&gt;&lt;span&gt;"maintenance"&lt;/span&gt;&lt;span&gt;],&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;  &lt;/span&gt;&lt;span&gt;"enabledManagers"&lt;/span&gt;&lt;span&gt;: [&lt;/span&gt;&lt;span&gt;"docker-compose"&lt;/span&gt;&lt;span&gt;],&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;"fileMatch"&lt;/span&gt;&lt;span&gt;: [&lt;/span&gt;&lt;span&gt;"^.*&lt;/span&gt;&lt;span&gt;\\&lt;/span&gt;&lt;span&gt;.yml$"&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;"managers"&lt;/span&gt;&lt;span&gt;: [&lt;/span&gt;&lt;span&gt;"docker-compose"&lt;/span&gt;&lt;span&gt;],&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;"depTypeList"&lt;/span&gt;&lt;span&gt;: [&lt;/span&gt;&lt;span&gt;"services"&lt;/span&gt;&lt;span&gt;],&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;      &lt;/span&gt;&lt;span&gt;"datasources"&lt;/span&gt;&lt;span&gt;: [&lt;/span&gt;&lt;span&gt;"docker"&lt;/span&gt;&lt;span&gt;]&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;At the moment I have it scanning through all YAML files since I do not name my compose files &lt;code&gt;docker-compose.yml&lt;/code&gt;. They are named as the service so: &lt;code&gt;forge.yml&lt;/code&gt;, &lt;code&gt;caddy.yml&lt;/code&gt;, &lt;code&gt;authelia.yml&lt;/code&gt;, etc. They are also located in their own folders. This will be changing as I move more to GitOps but for now it’s fine.&lt;/p&gt;&lt;p&gt;&lt;img alt="FJ Renovate PR" src="https://blog.alexsguardian.net/_astro/fj-renovate-pr.BKbIoFtc_28Kiq4.webp"&gt;&lt;span&gt;PR Opened by Renovate Bot using the Forgejo Action workflow&lt;/span&gt;&lt;/p&gt;&lt;h2 id="conclusion"&gt;Conclusion&lt;/h2&gt;&lt;p&gt;This was a fun project to work on in between work and family stuff. Hopefully this inspires one of you to upgrade your own local git setup with an ‘Actions’ runner or object storage! I’ll have a full config dump for MinIO, Forgejo, Forgejo Actions, and an example workflow in the &lt;a href="https://github.com/alexandzors/blog-files/tree/main/posts/upgradingforgejo"&gt;blog-files repo&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;&lt;em&gt;On to the next project. &lt;span&gt;&#128640;&lt;/span&gt;&lt;/em&gt;&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/body&gt;&lt;/html&gt;</content>
        <author>
            <name/>
        </author>
        <media:content medium="image" url="https://blog.alexsguardian.net/_astro/fj_actions_banner.Ly7zaxsc_C1Dxb.webp"/>
    </entry>
    <entry>
        <id>tag:feedly.com,2013:cloud/entry/cOXRjoksGIQzU1pdqbdybn+OTWYeI0riptNneGckdsI=_19ded720baf:89c617:5071632d</id>
        <title type="html">Rebuilding My Home Lab</title>
        <published>2026-05-03T10:46:07Z</published>
        <updated>2026-05-03T10:46:12Z</updated>
        <link href="https://blog.alexsguardian.net/posts/2025/02/25/rebuildingmyhomelab" rel="alternate" type="text/html"/>
        <summary type="html">Rebuilding my homelab 'cluster' into a proper 'cluster'.</summary>
        <content type="html">&lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"&gt;
&lt;html&gt;&lt;body&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;p&gt;&lt;sub&gt;FTC: Some links in this post are income earning affiliate links.&lt;/sub&gt;&lt;/p&gt;&lt;p&gt;It’s been a while since I last posted about my homelab or well posted on the blog. So I figured I’d start off with a banger for 2025. Rebuilding my homelab into a Docker Swarm cluster that supports high availability. Hope you’re ready for a ride, because this was definitely a journey. &lt;span&gt;&#128517;&lt;/span&gt;&lt;/p&gt;&lt;sub&gt;&lt;em&gt;Side note: I also recently acquired a Bambu Lab A1 3D printer (with the AMS :)) to replace my old Anet A8. I’d say it’s mine, but I’ve only been printing stuff for my wife so far..&lt;/em&gt;&lt;/sub&gt;&lt;h2 id="the-plan"&gt;The Plan&lt;/h2&gt;&lt;p&gt;The plan for this project was to:&lt;/p&gt;&lt;ol type="a"&gt;&lt;li&gt;Create a cluster that supported high availability &lt;span&gt;✅&lt;/span&gt;&lt;/li&gt;&lt;li&gt;Setup a distributed storage solution that supported the above &lt;span&gt;✅&lt;/span&gt;&lt;/li&gt;&lt;li&gt;Setup a load balancer &lt;span&gt;✅&lt;/span&gt;&lt;/li&gt;&lt;li&gt;Have the cluster communicate over 2.5gb or faster networking &lt;span&gt;✅&lt;/span&gt;&lt;/li&gt;&lt;li&gt;Have a minimum of 3 nodes &lt;span&gt;✅&lt;/span&gt;&lt;/li&gt;&lt;li&gt;Be small and efficient &lt;span&gt;✅&lt;/span&gt;&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;Easy enough right?… &lt;em&gt;right?&lt;/em&gt;&lt;/p&gt;&lt;p&gt;Since I had two Dell Optiplex 5050 Micros that I was using already, I decided to grab a third micro to complete the 3 node cluster. As for the actual orchestration software I decided to stick with Docker and use Docker Swarm. I’m a Docker guy but never had a chance to use Swarm properly with a shared storage system. Figured it was time to give it a proper go.&lt;/p&gt;&lt;div&gt;&lt;strong&gt;Wait… Docker Swarm? Alex you should use Kubernetes!!!!!!&lt;/strong&gt;&lt;img alt="Dwight Schrute saying 'Let's put it this way... no' from The Office" src="https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExeWZ2eWN6czc3NWN1OXJyeDRvZ210azVzamF4ejNzejVkeWQ3djZkdCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/daPCSjwus6UR2JxRX1/giphy.gif"&gt;&lt;/div&gt;&lt;h2 id="current-v2-cluster-specs"&gt;Current v2 Cluster Specs&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;2x Dell Optiplex 5050 Micros&lt;/li&gt;&lt;li&gt;1x Dell Optiplex 7040 Micros&lt;/li&gt;&lt;li&gt;3x Realtek 8125B 2.5gb A+E NICs&lt;/li&gt;&lt;li&gt;3x Intel 700p 256GB SSDs&lt;/li&gt;&lt;li&gt;3x Crucial BX500 1TB SSDs&lt;/li&gt;&lt;li&gt;Ubiquiti Flex-2.5g Switch&lt;/li&gt;&lt;li&gt;Ubuntu 24.04 LTS&lt;/li&gt;&lt;li&gt;GlusterFS&lt;/li&gt;&lt;/ul&gt;&lt;h2 id="cluster-v1"&gt;Cluster v1&lt;/h2&gt;&lt;sub&gt;circa 2023&lt;/sub&gt;&lt;p&gt;Cluster v1 never actually made it to ‘production’. It ended up being a test bed and a learning experience on gotchas when it came to Linux, Intel, drivers, and poor research on my part…&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;TLDR:&lt;/strong&gt; I originally bought some Intel i225-v 2.5gb m.2 cards from IOCrest thinking they’d work. Come to find out these particular chips have some &lt;a href="https://duckduckgo.com/?t=h_&amp;amp;q=intel+225-v+linux+driver+issues&amp;amp;ia=web" target="_blank"&gt;driver issues&lt;/a&gt; on Linux (specifically Ubuntu).&lt;/p&gt;&lt;/blockquote&gt;&lt;p&gt;When originally designing the cluster I had planned on using M.2 2.5g cards since the Micro’s had M.2 M-key slots. That way I could get a less expensive SATA SSD for storage. After doing some research I settled on some &lt;a href="https://www.aliexpress.us/item/3256805389320511.html" target="_blank"&gt;Intel 225-V 2.5g cards&lt;/a&gt; from IOCrest on AliExpress. The one downside to these cards was the vertical connector pins. The chassis did not have enough space with a SATA SSD installed. So I did what any tinkerer would do and replaced the pins with right angle ones. The first two cards went good, but my old soldering iron doesnt keep temp well. On the third card’s NIC module I messed up and destroyed one of the traces. So I had to fix it with a jumper wire. Live and learn I suppose.&lt;/p&gt;&lt;p&gt;As for the NICs themselves, I started experiencing driver issues after getting two of the nodes up and running in a ‘mock’ swarm. These issues manifested as random disconnects or complete NIC power loss. I tried to fix it by messing with grub configs for pcie_aspm thinking it was related to Active State Power Management (ASPM) at first. I then tried manually setting the autonegotiation via ethtool. Also double checked UEFI settings, etc. Nothing seemed to work. So I ended up scrapping the Intel cards and going with Realtek 8125B cards for cluster v2.&lt;/p&gt;&lt;p&gt;Anyways, I’ll leave you with a few pictures from the v1 prototype, and we can move on to v2.&lt;/p&gt;&lt;h2 id="cluster-v2"&gt;Cluster v2&lt;/h2&gt;&lt;sub&gt;circa 2024&lt;/sub&gt;&lt;p&gt;My second attempt at the cluster faired a bit better. Well mostly (more on that later). After my first purchase mistake I went back and bought &lt;a href="https://www.aliexpress.us/item/3256803984886712.html" target="_blank"&gt;different M.2 adapters&lt;/a&gt;. These were A+E keyed and used the WLAN slot rather than the M.2 M-keyed slot which got freed up for more storage. These new adapters also use a Realtek chipset (8125B) rather than an Intel one. So no more odd driver issues.&lt;/p&gt;&lt;p&gt;&lt;img alt="M.2 A+E keyed adapter installed" src="https://blog.alexsguardian.net/_astro/PXL_20241022_172523762.MP.CizhE_LT_1pzDqf.webp"&gt;&lt;span&gt;M.2 A+E keyed adapter installed&lt;/span&gt;&lt;/p&gt;&lt;p&gt;With the new adapters using the A+E slot, I decided to use the M slot for a boot drive and the SATA port for cluster storage. For the boot drive I went with some Intel 700p 256GB SSDs I bought off someone from the Ubiquiti Discord server. As for cluster storage I went with &lt;a href="https://amzn.to/4hCYdg4" target="_blank"&gt;1TB Crucial BX500 SSDs&lt;/a&gt; from Amazon. These are not the fastest drives by any means but for my use case they should do just fine.&lt;/p&gt;&lt;p&gt;&lt;img alt="Storage SSDs" src="https://blog.alexsguardian.net/_astro/PXL_20241023_170004511.MP.ZdGI53cL_Z1W8jpI.webp"&gt;&lt;span&gt;Storage SSDs&lt;/span&gt;&lt;/p&gt;&lt;p&gt;There was one downside to the Intel SSDs though. Their physical size. They are 2230 and the Micro chassis only supports 2260 and 2280. So in order to use them I had to print out some 2230 to 2280 adapter brackets. I also printed out a 2.5in tray for the third Micro since it did not come with one. The print files are linked below.&lt;/p&gt;&lt;h3 id="the-os"&gt;The OS&lt;/h3&gt;&lt;p&gt;For the OS I decided to stick with my tried and true &lt;a href="https://ubuntu.com/server"&gt;Ubuntu &lt;strong&gt;Server&lt;/strong&gt;&lt;/a&gt; (24.04 LTS btw). It’s stable and easy to manage. Though, I may eventually re-image to &lt;a href="https://rockylinux.org/"&gt;Rocky Linux&lt;/a&gt; for easy live patching when I rebuild again. The process was pretty standard. I booted via my &lt;a href="https://netboot.xyz/"&gt;netboot.xyz&lt;/a&gt; PXE server, ran through the installer and then used &lt;a href="https://docs.ansible.com/"&gt;Ansible&lt;/a&gt; to configure the OS. I’ll have the playbooks + task files in the blog-files repo after this post is live.&lt;/p&gt;&lt;aside&gt;&lt;p&gt;&lt;svg&gt;&lt;/svg&gt; Tip&lt;/p&gt;&lt;section&gt;&lt;p&gt;Looking at getting started with Ansible? I highly recommend Jeff Geerling’s &lt;a href="https://www.ansiblefordevops.com/"&gt;Ansible for DevOps&lt;/a&gt; handbook.&lt;/p&gt;&lt;/section&gt;&lt;/aside&gt;&lt;aside&gt;&lt;p&gt;&lt;svg&gt;&lt;/svg&gt; Tip&lt;/p&gt;&lt;section&gt;&lt;p&gt;If you want to know more about the netboot.xyz server, Techno Tim has a great video on it &lt;a href="https://www.youtube.com/watch?v=4btW5x_clpg&amp;amp;pp=ygUSdGVjaG5vIHRpbSBuZXRib290"&gt;here&lt;/a&gt;.&lt;/p&gt;&lt;/section&gt;&lt;/aside&gt;&lt;video&gt;&lt;source src="https://blog.alexsguardian.net/_astro/ansible_prep.BIf03Imk.webm" type="video/webm"&gt;Your browser does not support webm.&lt;/source&gt;&lt;/video&gt;&lt;span&gt;Ansible playbook running on the cluster nodes&lt;/span&gt;&lt;h3 id="networking"&gt;Networking&lt;/h3&gt;&lt;p&gt;After the OS was set up on all the nodes, I went ahead configured the network interfaces. During the v2 build Ubiquiti launched their Flex 2.5g switch line. Since this was an SFF cluster the &lt;a href="https://store.ui.com/us/en/category/all-switching/products/usw-flex-2-5g-5" target="_blank"&gt;Flex Mini 2.5g&lt;/a&gt; was a perfect fit as the cluster’s network backbone. Also with it being powered by POE made for one less cable to deal with. The cluster network is also a fairly simple one. It has no access to the internet and no access to any other networks. Other networks also can’t access it. Don’t need stuff snooping on my unencrypted SQL traffic..&lt;/p&gt;&lt;p&gt;With the network interfaces configured and working, it was time to test the speed using &lt;a href="https://iperf.fr/" target="_blank"&gt;iperf&lt;/a&gt;. As you can see by the short video below, I was able to get roughly 2.5gbps of bandwidth between the nodes.&lt;/p&gt;&lt;video&gt;&lt;source src="https://blog.alexsguardian.net/_astro/cluster_speed.Crkfr9Hw.webm" type="video/webm"&gt;Your browser does not support webm.&lt;/source&gt;&lt;/video&gt;&lt;span&gt;iperf testing&lt;/span&gt;&lt;h3 id="distributed-file-system"&gt;Distributed File System&lt;/h3&gt;&lt;p&gt;This was probably the most difficult part of the cluster build. At the time I had really no experience dealing with distributed file systems. I narrowed down my options to GlusterFS and Ceph. I ended up going with &lt;a href="https://www.gluster.org/" target="_blank"&gt;GlusterFS&lt;/a&gt; for this iteration. Using the &lt;a href="https://docs.gluster.org/en/main/Quick-Start-Guide/Quickstart/" target="_blank"&gt;official documentation&lt;/a&gt; I was able to get a basic cluster up and running fairly quickly. I also created a dedicated Docker user ‘doc’ and set its home directory to the GlusterFS volume on each of the nodes. I thought this would be a great idea, but it ended up being a pain later on.&lt;/p&gt;&lt;p&gt;&lt;img alt="GlusterFS setup" src="https://blog.alexsguardian.net/_astro/gluster-setup.1YBYIxUu_Z1NJ65g.webp"&gt;&lt;span&gt;GlusterFS setup&lt;/span&gt;&lt;/p&gt;&lt;h3 id="docker-swarm"&gt;Docker Swarm&lt;/h3&gt;&lt;p&gt;With the OS, Networking, and DFS setup, it was time to set up Docker Swarm. Setting up Docker and Docker Swarm is fairly straight forward using the &lt;a href="https://docs.docker.com/engine/install/ubuntu/" target="_blank"&gt;official documentation&lt;/a&gt;. You basically follow the installation on each of the nodes. Then you initialize the swarm on one and join the other nodes to it. In my case my commands were:&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;Terminal window&lt;/span&gt;&lt;/figcaption&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;docker&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;swarm&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;init&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--advertise-addr&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;192.168.100.2&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;## init the swarm on the cluster network interface&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;docker&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;swarm&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;join&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--token&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;SWMTKN-1-4c2d6gncjh15gaznem5m8t2twwfn09o0paqpjxwdkqr1n76h8j-chr42vcbaobx7v2nrk86q9e93&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;192.168.100.2:2377&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;## joining other nodes to the swarm&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;I then promoted the other two nodes to managers. This way I had proper high availability as it requires a minimum of 3 managers to be in the swarm.&lt;/p&gt;&lt;p&gt;&lt;img alt="Docker Swarm cluster" src="https://blog.alexsguardian.net/_astro/swarm-cluster.MJ-L-vim_2ga04o.webp"&gt;&lt;span&gt;Docker Swarm cluster&lt;/span&gt;&lt;/p&gt;&lt;h3 id="keepalived"&gt;Keepalived&lt;/h3&gt;&lt;p&gt;Next up was setting up &lt;a href="https://www.keepalived.org/" target="_blank"&gt;Keepalived&lt;/a&gt; to handle &lt;a href="https://en.wikipedia.org/wiki/Virtual_Router_Redundancy_Protocol" target="_blank"&gt;VRRP&lt;/a&gt; for the cluster. This allowed me to have a single IP address that automatically failed over to the next node automatically. The video below shows the failover in action (sped up to keep the video short).&lt;/p&gt;&lt;video&gt;&lt;source src="https://blog.alexsguardian.net/_astro/keepalived_node_switching.JYJzVdxF.webm" type="video/webm"&gt;Your browser does not support webm.&lt;/source&gt;&lt;/video&gt;&lt;span&gt;Keepalived VRRP failover&lt;/span&gt;&lt;p&gt;As you can see the active node switches as each keepalived service is terminated. The lower right shows a constant ping to the VRRP virtual IP (VIP). In this case 10.8.8.11. Having this VIP allows me to bring down hosts for maintenance or updates without having to worry about service connectivity.&lt;/p&gt;&lt;h3 id="ha-caddy"&gt;HA Caddy&lt;/h3&gt;&lt;p&gt;The final step before I could start using the cluster was to set up a load balancer/reverse proxy. There are a few different options but, if you’ve read any of my &lt;a href="https://blog.alexsguardian.net/tag/caddy" target="_blank"&gt;previous posts&lt;/a&gt; you know I love using &lt;a href="https://caddyserver.com/" target="_blank"&gt;Caddy&lt;/a&gt;. With its easy-to-use configuration and automatic certificate management makes it hard to beat. Also, Caddy is a great choice for this since it automatically clusters itself if all instances use the same storage volume.&lt;/p&gt;&lt;p&gt;First thing I did was create the new overlay network for Caddy. This network would be for any services that I wanted to be able to access from outside the cluster.&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;Terminal window&lt;/span&gt;&lt;/figcaption&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;docker&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;network&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;create&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--opt&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;encrypted=&lt;/span&gt;&lt;span&gt;true&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--driver&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;overlay&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--attachable&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--internal&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--subnet=172.0.96.0/20&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;caddy-internal&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;Creating the Caddy service file.&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;caddy.yml&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;    &lt;/span&gt;&lt;span&gt;image&lt;/span&gt;&lt;span&gt;: &lt;/span&gt;&lt;span&gt;alexandzors/caddy:2.9.1&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;/swarm/volumes/doc/caddy/Caddyfile:/etc/caddy/Caddyfile:ro&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;/swarm/volumes/doc/caddy/.data:/data&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;&lt;span&gt;      &lt;/span&gt;&lt;/span&gt;&lt;span&gt;- &lt;/span&gt;&lt;span&gt;/swarm/volumes/doc/caddy/configs:/etc/caddy/configs:ro&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;Then deploying Caddy to the swarm.&lt;/p&gt;&lt;div&gt;&lt;figure&gt;&lt;figcaption&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;Terminal window&lt;/span&gt;&lt;/figcaption&gt;&lt;pre&gt;&lt;code&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;span&gt;sudo&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;docker&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;stack&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;deploy&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;--c&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;caddy.yml&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;caddy&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;&lt;/div&gt;&lt;p&gt;&lt;img alt="docker service ls command showing Caddy" src="https://blog.alexsguardian.net/_astro/deployed_caddy.Ci8ZMIoI_1MONUY.webp"&gt;&lt;span&gt;docker service ls command showing Caddy running on all 3 nodes&lt;/span&gt;&lt;/p&gt;&lt;p&gt;I eventually need to update the Caddy deployment to use host mode for port assignments. That way I can get the source IP address of incoming requests and not the docker proxy IP. But that will be part of v3.&lt;/p&gt;&lt;h2 id="cluster-v3"&gt;Cluster v3&lt;/h2&gt;&lt;sub&gt;circa 2025&lt;/sub&gt;&lt;p&gt;So here we are. Cluster v2 has been running great for a while now, but I’ve been having some issues with it. The main, and arguably the largest, issue I have been running into is the slow performance of GlusterFS. I’m not sure if it’s the hardware choices I made, or a misconfiguration? I tried the performance tuning tips from the &lt;a href="https://docs.gluster.org/en/main/Administrator-Guide/Performance-Tuning/" target="_blank"&gt;official documentation&lt;/a&gt; but was still running into issues. Like switching to the cluster user ‘doc’ would take anywhere from 30 to 45 seconds to complete. Also, deploying stacks of services could take up to 5 minutes before they were alive and running.&lt;/p&gt;&lt;p&gt;Either way for v3 I’m probably going to move to &lt;a href="https://ceph.com/" target="_blank"&gt;Ceph&lt;/a&gt; as my DFS solution. Since it seems to be the more popular of the two after digging around more. I may need to upgrade the RAM on the nodes to handle it though. I’m also going to try and add a 4th node to the cluster with beefier hardware since I still have one 2.5g port left on the switch. Maybe one with some GPU compute to handle local LLMs?&lt;/p&gt;&lt;p&gt;&lt;img alt="docker services ls" src="https://blog.alexsguardian.net/_astro/docker_services_ls.GNY8tZvm_Z1uGAAF.webp"&gt;&lt;span&gt;Current services running on the cluster&lt;/span&gt;&lt;/p&gt;&lt;p&gt;However, what won’t be changing is my use of Docker Swarm. &lt;span&gt;&#128521;&lt;/span&gt;&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/body&gt;&lt;/html&gt;</content>
        <author>
            <name/>
        </author>
        <media:content medium="image" url="https://blog.alexsguardian.net/_astro/rebuild_banner.AfLrHjH8_ZS3Mfz.webp"/>
    </entry>
    <entry>
        <id>tag:feedly.com,2013:cloud/entry/cOXRjoksGIQzU1pdqbdybn+OTWYeI0riptNneGckdsI=_19de73bd2a9:21797a:4bf1e6c6</id>
        <title type="html">How to smoke</title>
        <published>2026-05-02T05:49:10Z</published>
        <updated>2026-05-02T05:49:14Z</updated>
        <link href="https://buttondown.com/monteiro/archive/how-to-smoke/" rel="alternate" type="text/html"/>
        <summary type="html">I used to love to smoke. If it weren’t for the whole lung cancer, emphysema, death thing, would you recommend smoking?</summary>
        <content type="html">&lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"&gt;
&lt;html&gt;&lt;body&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;
    
        &lt;figure&gt;&lt;img alt="Four zippo lighters stacked up, sitting on pink paper." src="https://assets.buttondown.email/images/c2f95460-4ccf-4823-a800-01c37d5a8ced.jpeg?w=960&amp;amp;fit=max"&gt;&lt;figcaption&gt;&lt;em&gt;The best part of smoking was always the click of the Zippo.&lt;/em&gt;&lt;/figcaption&gt;&lt;/figure&gt;&lt;hr&gt;&lt;p&gt;&lt;em&gt;This week’s question comes to us from Mat Honan:&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;I used to love to smoke. If it weren’t for the whole lung cancer, emphysema, death thing, would you recommend smoking?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;You’re forgetting the smell.&lt;/p&gt;
&lt;p&gt;Last week I was coming back from the record store and feeling lazy, so I jumped on the bus. A couple of stops after I got on, a dude got on and sat down next to me. Well-dressed dude, also carrying an Amoeba Records shopping bag. Out and about on a Sunday afternoon, doing his record shopping just like me. Within seconds it became clear this dude had &lt;em&gt;just&lt;/em&gt; had a cigarette. And he stunk. The bus was also packed at this point, so I was pretty much stuck in that seat until I got off, which luckily wasn’t for too much longer. Also, it feels kinda shitty when you sit down next to somebody on the bus and they immediately get up. (Unless you’re getting a creeper vibe, of course.) I just had to suck it up for a few more stops. But man, it was rough. I’m not trying to disparage the guy or anything. Especially because I used to be that guy, and you used to be that guy, and I’m guessing a lot of our readers used to be that guy. We just walked around smelling awful.&lt;/p&gt;
&lt;p&gt;Which is not to say that’s the worst part of smoking, but you took lung cancer, emphysema, and the dead thing off the table. Which leaves us with the smell.&lt;/p&gt;
&lt;p&gt;I started smoking my freshman year in college. All reasons to take up smoking are stupid, but this one might be the stupidest. This being the mid-80s our dorm had a cigarette machine in the lobby. One of those old machines you might still see in old man dive bars, with two rows of big ka-chunky knobs, which felt objectively good to pull. It was like you could feel the entire mechanism of the machine come to life when you pulled that knob. And it took some strength to do it! But you could feel the knob hit a gear, you could hear the gear spin, you could feel a pack of cigarettes get pushed free from deep inside the machine. You could hear it slide down a little ramp, and then you’d see it appear down in the landing zone, where you’d push your hand past the trap door, grab it, and somehow have the pack open, and a cigarette in your mouth before the rest of the pack hit your pocket.&lt;/p&gt;
&lt;p&gt;If memory serves, a pack of smokes was between $1.25 and $1.50 around that time. (Fun fact, I attempted to Google this and the slop top on the search results page said 26¢ a pack, which is… not true. But please, continue to rebuild society around such amazing technology.) Being the art school miscreants that we were, and also broke, we discovered that if you pulled a knob halfway out, inserted a quarter, and then pulled the knob the rest of the way out it would release a pack of cigarettes. (Ok, that may not have been the &lt;em&gt;actual&lt;/em&gt; process, but it was a long time ago, and it’s very close to the &lt;em&gt;spirit&lt;/em&gt; of the actual process, so let’s run with it. So I guess we &lt;em&gt;were&lt;/em&gt; getting a pack of cigarettes for what Google’s stupid slop robot said, but it included doing crimes.) The knowledge of how to hack the cigarette machine spread through the dorms like wildfire, and soon we were all smoking. Because we were idiots. Also, it felt like we were getting one over on The Man. But mostly because we were idiots.&lt;/p&gt;
&lt;p&gt;Also, being art school kids we were very visually-driven people, and all the photos of cool people that we’d hang up in our dorm rooms showed them holding a cigarette. And we very much wanted to be cool people. (True fact: take a photo of Humphrey Bogart, replace the ever-present cigarette with a vape and Humphrey Bogart will look like an herb.) Again, mostly we were idiots. &lt;/p&gt;
&lt;p&gt;The vending machine company tried to patch the hack several times, eventually gave up and just took the machine away. This was our first lesson that sometimes doing crime is in the public interest, but that lesson didn’t occur to us right away. At the time, we were just pissed that we had to pay retail for cigarettes again. &lt;/p&gt;
&lt;p&gt;By the time I started smoking society was pretty much done with the pretense that smoking was doing anything but murdering you slowly. I know this because we’d sit around in art classes making collages using old cigarette ads where doctors would tell you smoking was good for your nerves, and we would laugh at people for believing this, as we lit cigarette after cigarette. (Yes, you could smoke in class.) And we thought “Boy, our grandparents sure were chumps for believing cigarettes were healthy.” Then we would have a coughing fit. But there was definitely the sense that doing this thing that we all knew had a very very high probability of killing us wasn’t a big deal, mostly because we were in our 20s when nothing can hurt you, Reagan was president and, just to reiterate—we were idiots.&lt;/p&gt;
&lt;p&gt;My first post-college job was at a copy shop, and you got one 15 minute break during your shift. Unless you smoked, then you could get as many breaks as you needed. Several people started smoking while working there.&lt;/p&gt;
&lt;p&gt;We all stunk. We’d come in from smoking out back, and immediately walk up to the service counter to help a customer. A customer who either stunk as bad as we did, or had become inured to the stench because it was all around them, emanating from everyone.&lt;/p&gt;
&lt;p&gt;We smoked in class. We smoked at the movies. We smoked at the supermarket. We smoked at sporting events. My friend Jeff, who grew up in Boston, tells a good story about going to Celtics games as a kid and having to look past the hovering cloud of smoke between the cheap seats and the court. We smoked in restaurants, where the smoking and non-smoking sections were often divided by nothing more than a paper sign denoting the territorial boundary. We smoked on planes, man.&lt;/p&gt;
&lt;p&gt;It wasn’t too long after college that the world began to shift. In 2003 New York City banned smoking in bars. And I was visiting at the time. By 2003, I was no longer “a smoker” but I was very much someone who would look for reasons to bum a smoke from someone if the situation arose, and very likely to put myself in situations where it might. But I remember the rage from several friends and from the owners and bartenders of any bar we’d walk into. The ban was going to kill bars all over the city. It was going to kill nightlife. It was going to completely take down the economy. New York, as we know it, would cease to exist. Which of course, it didn’t. Everyone adjusted. They went outside. They eventually started smoking less because it was cold outside. People enjoyed being able to hang out in rooms that weren’t making them sick, and they enjoyed going home not reeking of cigarette smoke. If I could go back in time I’d reassure all those bartenders that it wasn’t the smoking ban they had to worry about. It was the kids who’d stop going out at all because they needed to sit at home and tend to their AI agents. &lt;/p&gt;
&lt;p&gt;I selected your question this week because I’ve actually been thinking of the smoking ban lately. We grew up in a time when smoking, or dealing with other smokers was an inevitability. Even if you didn’t smoke, you’d most likely work next to someone who did, or sit down next to someone who did at a restaurant, or at the movies. And even if they weren’t actively smoking, and covering you in second-hand smoke, you’d go home with the stench of smoke all over you. Airing your clothes out was an inevitability. Having to wash the stench out of your hair was an inevitability. Society smoked, so you did too. Whether you wanted to or not. And then it changed. Most cities in America now have smoking bans and rules about how far away you have to be from a public entrance to smoke, which get enforced to various degrees. &lt;/p&gt;
&lt;p&gt;The change came in a couple of very interesting ways. One, cigarettes are now hovering between $12 to $14 a pack. (I had to look this up!) They’re also available in less places. (You used to be able to buy cigarettes at the drug store!) Secondly, people just look at you weird if you start smoking now. Like, what the fuck dude, did you just light a cigarette!? Are you from the past? Gross.&lt;/p&gt;
&lt;p&gt;Which of course makes me think of some of the things that we have currently accepted as a society, things which we &lt;em&gt;fully know&lt;/em&gt; are not healthy for society, that we are currently tolerating. And also thinking there’s no way it will ever change, because we appear to be in an era of “what if everyone modeled themselves off the stupidest people?”&lt;/p&gt;
&lt;p&gt;Right now there is someone firing up ChatGPT because it’s cheap. Right now there is someone writing a prompt in Claude because it brings him closer to his co-workers. Right now there is someone walking a co-worker through his agentic workflow, in the same way we attempted to impress one another by blowing smoke rings. Right now there is someone parking a Cybertruck on your street, believing that leaving his divorce where everyone can see it is somehow impressive. We have always been good at ignoring the warnings that came with the pack. &lt;/p&gt;
&lt;p&gt;Our parents packed their homes with asbestos. They heated their homes with coal. They packed their Big Macs in styrofoam. Making mistakes will always be cheaper than fixing them. But nothing is more expensive than ignoring them. &lt;/p&gt;
&lt;p&gt;Cultural norms are an ever-changing thing. History is the story of what was once desirable becoming unacceptable. Something that used to be an inevitability is no longer inevitable. Something that used to be tolerated is no longer tolerated. Something that was seen as a cultural norm no longer is. Even when those things were backed by entire industries with very strong lobbies, as the tobacco lobby once was. The same fate will someday befall the NRA. The same fate will someday befall AIPAC. The same fate will someday befall the slop lobby.&lt;/p&gt;
&lt;p&gt;There was a time we thought if we prohibited people from smoking in bars it would lead to societal collapse. I think it was a good idea. More importantly, it was an idea that worked. It improved not just our personal health but the health of our communities.&lt;/p&gt;
&lt;p&gt;The basic strategy of all addictive technologies is very simple. They make you feel extra capable, they addict you, then they make you feel inadequate without them. They start by making you feel cool, and confident. Relax. Put your feet up. Hang with the fellas. Social anxiety? It’s toasted, dog! Let me write that résumé for you. Anniversary card for your wife? I can write that for you. Light one up. It’s a great way to start a relationship. But that initial boost eventually turns to reliance, and suddenly you can’t get out of bed without a hit. You can’t write your kid a love note without firing up a slop engine. And suddenly an entire industry is telling you that you’re not capable of moving through your day without their help. An entire industry gaslighting you, until it becomes easier to just gaslight yourself into believing that you were never truly capable of things you are very much capable of.&lt;/p&gt;
&lt;p&gt;I don’t miss smoking. Maybe I did at one point. But eventually the whiff of cigarette smoke went from smelling nostalgic to just smelling bad. Thankfully, knock on wood, I’ve been able to escape years of smoking without any &lt;em&gt;major&lt;/em&gt; lasting effects, but trust that I carry every pack I ever smoked with me every time I walk up a flight of stairs. If I’m doing anything strenuous, it’s always my lungs that give up first. &lt;/p&gt;
&lt;p&gt;Thankfully, I still have some lung capacity. I enjoy using it. You should too.&lt;/p&gt;
&lt;p&gt;&#128684;&lt;/p&gt;
&lt;hr&gt;&lt;p&gt;&#128587; Got a question? &lt;a href="https://www.mikemonteiro.com/ask-a-question?utm_source=monteiro&amp;amp;utm_medium=email&amp;amp;utm_campaign=how-to-smoke" rel="noopener noreferrer nofollow" target="_blank"&gt;Ask it&lt;/a&gt;! I will somewhat answer it!&lt;/p&gt;
&lt;p&gt;&#128211; Get your sexy copy of my new book &lt;a href="https://www.mulebooks.com/store/how-to-die-and-other-stories?utm_source=monteiro&amp;amp;utm_medium=email&amp;amp;utm_campaign=how-to-smoke" rel="noopener noreferrer nofollow" target="_blank"&gt;How to Die (and other stories)&lt;/a&gt;! And if you’re in the Bay Area, &lt;a href="https://booksmith.com/event/monteiro26?utm_source=monteiro&amp;amp;utm_medium=email&amp;amp;utm_campaign=how-to-smoke" rel="noopener noreferrer nofollow" target="_blank"&gt;come see me and Annalee Newitz talk about it on May 11 at Booksmith&lt;/a&gt;!&lt;/p&gt;
&lt;p&gt;&#128674; My friend Lucy Bellwood made a &lt;a href="https://lucybellwood.com/new-comic-the-scale-of-a-man/?utm_source=monteiro&amp;amp;utm_medium=email&amp;amp;utm_campaign=how-to-smoke" rel="noopener noreferrer nofollow" target="_blank"&gt;wonderful comic about loss and building&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&#128581; My friend Jason Cosper made a &lt;a href="https://jasoncosper.com/kill-yr-substack/?utm_source=monteiro&amp;amp;utm_medium=email&amp;amp;utm_campaign=how-to-smoke" rel="noopener noreferrer nofollow" target="_blank"&gt;great tool for fucking with Substack’s dollar&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&#128227; I’ve got a few seats left in next week’s &lt;a href="https://www.eventbrite.com/e/1987951529539?aff=oddtdtcreator&amp;amp;utm_source=monteiro&amp;amp;utm_medium=email&amp;amp;utm_campaign=how-to-smoke" rel="noopener noreferrer nofollow" target="_blank"&gt;Presenting w/Confidence&lt;/a&gt; workshop where you can learn &lt;em&gt;true&lt;/em&gt; confidence. The one inside you.&lt;/p&gt;
&lt;p&gt;&#127817; Please support &lt;a href="https://www.pcrf.net/?utm_source=monteiro&amp;amp;utm_medium=email&amp;amp;utm_campaign=how-to-smoke" rel="noopener noreferrer nofollow" target="_blank"&gt;the children of Palestine&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&#127987;️‍⚧️ Please &lt;a href="https://translifeline.org/?utm_source=monteiro&amp;amp;utm_medium=email&amp;amp;utm_campaign=how-to-smoke" rel="noopener noreferrer nofollow" target="_blank"&gt;support trans kids&lt;/a&gt;. And if there is a trans person in your life please tell them you love them.&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;
    
&lt;/div&gt;


                


    &lt;/div&gt;&lt;/div&gt;&lt;/body&gt;&lt;/html&gt;</content>
        <author>
            <name/>
        </author>
        <media:content medium="image" url="https://assets.buttondown.email/images/c2f95460-4ccf-4823-a800-01c37d5a8ced.jpeg?w=960&amp;fit=max"/>
        <source>
            <id>tag:feedly.com,2013:cloud/feed/https://buttondown.com/monteiro/rss</id>
            <title type="html">buttondown.com</title>
            <link href="https://buttondown.com" rel="alternate" type="text/html"/>
            <updated>2026-05-02T05:49:14Z</updated>
        </source>
    </entry>
    <entry>
        <id>tag:feedly.com,2013:cloud/entry/cOXRjoksGIQzU1pdqbdybn+OTWYeI0riptNneGckdsI=_19dcdfde5b1:853577:18edc835</id>
        <title type="html">Crashing hard: why talking about bubbles obscures the real social cost of overinvesting into “Artificial Intelligence”</title>
        <published>2026-04-27T08:11:01Z</published>
        <updated>2026-04-27T08:11:06Z</updated>
        <link href="https://www.structural-integrity.eu/crashing-hard-why-talking-about-bubbles-obscures-the-real-social-cost-of-overinvesting-into-artificial-intelligence/" rel="alternate" type="text/html"/>
        <summary type="html">More and more commentators talk about and warn of an “AI bubble”, and everybody seems to congratulate each other on being such a smart financial analyst. BUT: A bubble pops and you are left with air and maybe a splash of soap somewhere on the floor. A fairly clean affair. This kind of investor speak</summary>
        <content type="html">&lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"&gt;
&lt;html&gt;&lt;body&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;
&lt;p&gt;&lt;a href="https://observer.co.uk/news/science-technology/article/a-new-dotcom-bubble-ai-hype-has-yet-to-translate-into-profits"&gt;More&lt;/a&gt; &lt;a href="https://fluxus.io/article/a-hitchhikers-guide-to-the-ai-bubble"&gt;and&lt;/a&gt; &lt;a href="https://www.goldmansachs.com/insights/top-of-mind/gen-ai-too-much-spend-too-little-benefit"&gt;more&lt;/a&gt; &lt;a href="https://www.economist.com/business/2025/05/21/welcome-to-the-ai-trough-of-disillusionment"&gt;commentators&lt;/a&gt; &lt;a href="https://paulkedrosky.com/honey-ai-capex-ate-the-economy/"&gt;talk&lt;/a&gt; about and warn of an “AI bubble”, and everybody seems to congratulate each other on being such a smart financial analyst. BUT: A bubble pops and you are left with air and maybe a splash of soap somewhere on the floor. A fairly clean affair. This kind of investor speak obscures the severe consequences economic crashes cause, coming from someone’s point of view for whom this is more likely to be a spectacle than a direct threat.&lt;/p&gt;



&lt;p&gt;When the “AI” market crashes, there will be NO “reset button”, NO “rollercoaster” continuing on an orderly path after having come down, NO “bubble” that just lets off hot air. These are all metaphors that heavily misrepresent what it means for markets to crash, or, as they say, “correct”. We might be in for a long and painful struggle to at least reduce the grip of “AI” on current core societal functions like government administration, education and research funding. In this article, I want to illustrate the broad range of costs that BOTH the buildup of “AI” overvaluations AND their coming down will have. The current “AI” investments will have long-term costs by creating significant path dependencies: They make harmful things cheaper, speed up the commodification of human labour and shift social norms. Just to be clear: I am referring to the current “AI” boom which is driven mostly by generative AI (“genAI”) applications, not necessarily the things that have been around for decades (e.g. various forms of pattern recognition) and that did not induce companies to spend hundreds of billions on data centres.&lt;/p&gt;



&lt;p&gt;To better understand what is going on, let’s first look at the outcomes of previous instances of overinvestment, including the 2000 dot-com “bubble” and the significant piles of money Uber burnt for many years, before turning to contemporary “AI” path dependencies.&lt;/p&gt;



&lt;h3 class="wp-block-heading has-custom-pink-color has-text-color has-link-color wp-elements-b1db5c78d832fe90a6c94a3d6c122255"&gt;&lt;strong&gt;&lt;strong&gt;Overinvestments shape technological paths&lt;/strong&gt;&lt;/strong&gt;&lt;/h3&gt;



&lt;p&gt;Let’s start with the obvious: The so-called dot-com boom crashed between 2000 and 2002 – this already hints at the fact that “bubble bursting” is a long period during which no one knows when it will end. When it did end, investors lost money, and it is mostly their perspective that was &lt;a href="https://www.businessinsider.com/speculative-bubble-lessons-stock-crypto-outlook-dotcom-era-henry-blodget-2021-11"&gt;covered&lt;/a&gt; in the media (and they whined about a crash being less bad than the pain inflicted by missing out on a boom). Many people lost their jobs, their livelihoods, and needed to find other ways to make ends meet (&lt;a href="https://www.reddit.com/r/programming/comments/1cgf1fd/ask_hn_what_was_the_job_market_like_during_the/"&gt;developers on Reddit&lt;/a&gt; gave an account, but they were probably among the more privileged). Unemployment in the US increased from about 4% to almost 6%.&lt;br&gt;What might be a little less obvious: A few key developments that the dot-com boom had started persisted long after. While the internet was still a mostly academic affair until the 1990s, the dot-com boom kicked off the scale-over-everything, ad-based internet we know today. We saw &lt;a href="https://manifold.umn.edu/read/profit-over-privacy/section/ee270b37-d3d9-4312-b318-57ea01c2328f"&gt;alignment of advertising business and finance&lt;/a&gt; as well as a massive drive to consolidation during the crash. Google, eBay, Amazon, Nvidia, they all became central players in the commercial internet. What now seems inevitable to most people seemed coincidental before the boom – but then today’s driving forces crowded out most other, less commercial forms of existing on the internet.&lt;/p&gt;



&lt;hr class="wp-block-separator has-text-color has-custom-pink-color has-alpha-channel-opacity has-custom-pink-background-color has-background is-style-wide"&gt;&lt;h2 class="wp-block-heading has-text-align-center has-custom-pink-color has-text-color has-link-color wp-elements-f4e142a27545cd194d5a3365a0e90d36"&gt;“AI” investments make harmful things cheaper, speed up the commodification of human labour and shift social norms.&lt;/h2&gt;



&lt;hr class="wp-block-separator has-text-color has-custom-pink-color has-alpha-channel-opacity has-custom-pink-background-color has-background is-style-wide"&gt;&lt;p&gt;The investment logic has shaped the internet ever since: Uber accumulated almost $34bn USD in losses (excluding losses while it was not yet public between 2009 and 2014) before it started to generate profits in &lt;a href="https://www.theguardian.com/technology/2024/feb/07/landmark-moment-as-uber-unveils-first-annual-profit-as-limited-company"&gt;2023&lt;/a&gt;. (And also various &lt;a href="https://wolfstreet.com/2021/07/05/todays-unicorns-have-bigger-cumulative-losses-than-amazon-had-lost-money-far-longer-than-amazon-still-dont-show-a-turnaround/"&gt;other unicorns&lt;/a&gt; incur massive losses they may never recoup.) Money also creates habits and legitimises actions: Uber “&lt;a href="https://www.theguardian.com/news/2022/jul/10/uber-files-leak-reveals-global-lobbying-campaign"&gt;broke laws, duped police and secretly lobbied governments&lt;/a&gt;”, as the Guardian titled in 2022 and its controversies got their own &lt;a href="https://en.wikipedia.org/wiki/Controversies_surrounding_Uber"&gt;Wikipedia page&lt;/a&gt;. But also their very official business model is based on a) normalising precarious working conditions by eroding labour protections as they fought countless lawsuits over giving their workers only freelance and not an employee status, thereby avoiding sick pay, holidays etc., and b) making human work seem more automated and less visible by mediating passengers and drivers through an algorithm in an app, reducing the need for actual human interaction. Both developments shift social norms in ways that are likely to be profitable for businesses even beyond Uber: &lt;strong&gt;Uber’s mission can be understood as reducing the value of workers and human interaction, and with that long-term goal it is commercially rational to rack up significant losses in the short to medium term.&lt;/strong&gt;&lt;/p&gt;



&lt;h3 class="wp-block-heading has-custom-pink-color has-text-color has-link-color wp-elements-ef31312ad6f1dee58b7bf2382857e854"&gt;&lt;strong&gt;The “AI” path dependencies we will not correct&lt;/strong&gt;&lt;/h3&gt;



&lt;p&gt;It is plausible to expect something similar to happen with “AI”. The ongoing investment boom is creating significant overcapacity with effects lasting long after many “AI” startups have gone bust. These range from very visible and direct to the more indirect and structural.&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;Lower costs for compute and energy&lt;/strong&gt;: In order to sustain the boom, investments follow projections of endless “AI” growth, which translate into hundreds of billions currently being invested into data centre construction, alongside an expansion of energy infrastructure. And once they are built, it does not make sense to stop them, does it? Keeping them running is much cheaper than constructing them. This puts us on a path of energy-intensive technology even once these data centres are no longer needed for “AI” applications. This eradicates any incentive for resource-efficient coding or low-computation technology. At the same time, this infrastructure is not costless to maintain (to my knowledge, chips need to be replaced about every 7 years) – but possibly that cost is still lower than doing anything else, hence continuing on that trajectory will remain cheaper than alternatives for quite some time to come. These artificially low costs of compute and energy will be even further away from the “real” costs when factoring in just the environmental harms they produce. That is very bad news for anybody still hoping for a combined digital and environmental transition.&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;Sectoral knowledge destruction&lt;/strong&gt;: Some people may get used to using “AI” for a variety of tasks, even where this is neither actually helpful nor profitable for the “AI” providers. As is widely reported, managers often &lt;a href="https://www.telegraph.co.uk/business/2025/07/21/bosses-warn-workers-use-ai-or-face-the-sack/"&gt;encourage&lt;/a&gt; or &lt;a href="https://www.pcgamer.com/gaming-industry/ai-is-no-longer-optional-microsoft-is-allegedly-pressuring-employees-to-use-ai-tools-through-manager-evaluations/"&gt;force&lt;/a&gt; their employees to e.g. code using genAI applications, &lt;a href="https://www.nature.com/articles/s41598-025-92937-2"&gt;university students use genAI&lt;/a&gt; applications for writing, public bodies are continuing to move onto fancy “AI” clouds and &lt;a href="https://berthub.eu/articles/posts/our-self-inflicted-cloud-crisis/"&gt;forget how to do on-premise computing&lt;/a&gt;, and we are likely to see more diffusion before the boom crashes. Just as Uber’s mission was broader than just individual transport, “AI” has an inbuilt contempt for human interaction (as it is built to automate speech while avoiding any interpersonal &lt;a href="https://tante.cc/2025/07/30/friction-and-not-being-touched/"&gt;friction&lt;/a&gt;) and workers (as it seeks to make them even more interchangeable and subordinate to machine processes). Hence, using “AI” often destroys established processes of developing skills and sharing knowledge. Rebuilding them will take much longer and possibly cost more than might have been saved in the meantime.&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;Again more economic inequality&lt;/strong&gt;: An economic crisis is not equally dangerous for everyone. Only few companies are benefitting from the “AI” boom and most of the stock market gains in recent years were driven by Big Tech valuations going absolutely through the roof. However, we can expect that the losses will be shared more widely, based on the experience of past financial crises. Big Tech is trying to portray itself as &lt;a href="https://ainowinstitute.org/publications/research/1-2-too-big-to-fail-infrastructure-and-capital-push"&gt;too big to fail&lt;/a&gt;, which means that their &lt;a href="https://www.wheresyoured.at/ai-is-a-money-trap/"&gt;systemic relevance&lt;/a&gt; would prompt governments to inject tax money to reduce any losses they might incur. A financial crisis has ripple effects that go far beyond the market in question – just as the 2008 US housing crisis did not only lead to people losing their homes, but caused a huge &lt;a href="https://www.federalreservehistory.org/essays/great-recession-and-its-aftermath"&gt;recession&lt;/a&gt; with a stark increase in unemployment and financial instability.  &lt;/p&gt;



&lt;h3 class="wp-block-heading has-custom-pink-color has-text-color has-link-color wp-elements-bec4897a635f69b0ad9ad50bc79da3ef"&gt;&lt;strong&gt;&lt;strong&gt;&lt;strong&gt;Why “AI” overinvestments might take a long time to unwind&lt;/strong&gt;&lt;/strong&gt;&lt;/strong&gt;&lt;/h3&gt;



&lt;p&gt;There is no way of knowing when the “AI” boom is likely to crash – that is the whole point of markets that are supposed to create collective rationality from individual choices. I see a few reasons to suspect an even longer and more painful struggle than the dot-com crash in 2000. First, the boom is &lt;a href="https://papers.ssrn.com/sol3/papers.cfm?abstract_id=5377426"&gt;orchestrated&lt;/a&gt; or arguably &lt;a href="https://www.tandfonline.com/doi/full/10.1080/09692290.2024.2365757"&gt;planned&lt;/a&gt; by a handful of extremely powerful companies. Being few increases the scope to act strategically. Second, these companies are very close not only to the US government, but through their start-up investments also to governments across the world, selling “AI” promises and lies to politicians. The push of small and large “AI” firms into military tech aggravates this dynamic: It is the area in which talking of an “AI race” carries quite intuitive meaning because having more destructive power translates into military power, though not necessarily into better societal outcomes. And third, the intention of reaching systematic relevance is bearing some fruit as more and more institutions are becoming financially invested into “AI success”. Not only VC investors, but &lt;a href="https://www.noahpinion.blog/p/will-data-centers-crash-the-economy"&gt;large parts of society including life insurance and pension funds&lt;/a&gt; will bear the cost of its failure, giving them an incentive to prolong the boom at fairly high costs.&lt;/p&gt;



&lt;hr class="wp-block-separator has-text-color has-custom-pink-color has-alpha-channel-opacity has-custom-pink-background-color has-background is-style-wide"&gt;&lt;h2 class="wp-block-heading has-text-align-center has-custom-pink-color has-text-color has-link-color wp-elements-345884f2ed11f719063a6e54d5330c77"&gt;There are a few reasons to suspect an even longer and more painful struggle than the dot-com crash: market concentration, closeness to governments, and financial actors being invested into &lt;strong&gt;&lt;strong&gt;&lt;strong&gt;“&lt;/strong&gt;&lt;/strong&gt;&lt;/strong&gt;AI success&lt;strong&gt;&lt;strong&gt;&lt;strong&gt;”&lt;/strong&gt;&lt;/strong&gt;&lt;/strong&gt;.&lt;/h2&gt;



&lt;hr class="wp-block-separator has-text-color has-custom-pink-color has-alpha-channel-opacity has-custom-pink-background-color has-background is-style-wide"&gt;&lt;h3 class="wp-block-heading has-custom-pink-color has-text-color has-link-color wp-elements-5e90c772184a31765a55f8b88dde9ff6"&gt;&lt;strong&gt;&lt;strong&gt;&lt;strong&gt;&lt;strong&gt;What to do and why not to despair&lt;/strong&gt;&lt;/strong&gt;&lt;/strong&gt;&lt;/strong&gt;&lt;/h3&gt;



&lt;p&gt;It is important to analyse the abyss, but don’t stare into the abyss, as Jathan Sadowski sometimes says on my currently favourite podcast &lt;a href="https://soundcloud.com/thismachinekillspod"&gt;This Machine Kills&lt;/a&gt;. Understanding what is happening is essential to figure out a plan and I am keen to do that with others (i.e. I am aware my suggestions are insufficient). Anything that contributes to not making the boom bigger than it needs to be is helpful (e.g. do not invest into “AI”, tell your friends not to, do not use genAI applications or pay for them). Anything that helps us to talk in less delusional terms about what is going on is helpful (e.g. do not join those talking about “bubbles” suggesting they are merely financial events or that have one moment of coming down after which everything will be okay again). And let’s try to preserve that knowledge that companies are keen to replace with “AI”. We will need it.&lt;/p&gt;



&lt;p&gt;Photo by &lt;a href="https://unsplash.com/@nampoh?utm_content=creditCopyText&amp;amp;utm_medium=referral&amp;amp;utm_source=unsplash"&gt;Maxim Hopman&lt;/a&gt; on &lt;a href="https://unsplash.com/photos/red-and-blue-light-streaks-fiXLQXAhCfk?utm_content=creditCopyText&amp;amp;utm_medium=referral&amp;amp;utm_source=unsplash"&gt;Unsplash&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;

&lt;/div&gt;&lt;/div&gt;&lt;/body&gt;&lt;/html&gt;</content>
        <author>
            <name/>
        </author>
        <media:content medium="image" url="https://www.structural-integrity.eu/wp-content/uploads/2025/08/maxim-hopman-fiXLQXAhCfk-unsplash-scaled.jpg"/>
        <source>
            <id>tag:feedly.com,2013:cloud/feed/https://www.structural-integrity.eu/feed/</id>
            <title type="html">Structural Integrity</title>
            <link href="https://www.structural-integrity.eu" rel="alternate" type="text/html"/>
            <updated>2026-04-27T08:11:06Z</updated>
        </source>
    </entry>
    <entry>
        <id>tag:feedly.com,2013:cloud/entry/cOXRjoksGIQzU1pdqbdybn+OTWYeI0riptNneGckdsI=_19dc5b77d61:2d20d7:348926d</id>
        <title type="html">Wrap Text Around Images with CSS shape-outside</title>
        <published>2026-04-25T17:37:09Z</published>
        <updated>2026-04-25T17:37:14Z</updated>
        <link href="https://theosoti.com/short/wrap-text-around-images/" rel="alternate" type="text/html"/>
        <summary type="html">Use shape-outside in CSS to wrap text around custom image shapes—no JavaScript, just clean, creative layout control.</summary>
        <content type="html">&lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"&gt;
&lt;html&gt;&lt;body&gt;&lt;div&gt;&lt;div&gt;&lt;article&gt;&lt;source type="image/webp"&gt;&lt;img src="https://theosoti.com/short/07-2025/shape-outside-image.avif" alt="Text wrapping tightly around the shape of an image using CSS shape-outside and float for a refined layout"&gt;&lt;/source&gt;&lt;h2 id="make-your-text-wrap-around-images-perfectly"&gt;Make your text wrap around images perfectly.&lt;/h2&gt;
&lt;p&gt;By default, text wraps around a boring rectangle.
But what if you could make it hug the actual shape of the image?&lt;/p&gt;
&lt;p&gt;You can.
With just one CSS property:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;shape-outside: url(your-img.png);&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Add a &lt;code&gt;float&lt;/code&gt; and a bit of margin with &lt;code&gt;shape-margin&lt;/code&gt;,
and your layout feels instantly more refined.&lt;/p&gt;
&lt;p&gt;No JavaScript. No layout hacks.
Just native CSS support and it’s supported in over 95% of browsers.&lt;/p&gt;
&lt;p&gt;Use it with transparent PNGs, SVGs, or even basic shapes like &lt;code&gt;circle()&lt;/code&gt; or &lt;code&gt;polygon()&lt;/code&gt;.
Ideal for editorial layouts, landing pages, or any design that needs more personality.&lt;/p&gt;
&lt;p&gt;Checkout the codepen: &lt;a href="https://codepen.io/theosoti/pen/ogjjged"&gt;https://codepen.io/theosoti/pen/ogjjged&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;shape-outside&lt;/code&gt; can produce elegant editorial layouts when image silhouettes stay simple. Test long paragraphs and varied image ratios, because float-based wrapping can become fragile in edge cases.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;shape-outside&lt;/code&gt; can create editorial layouts with strong flow, but test with varied image sizes and long text. Float-based wrapping can break faster than block layouts.&lt;/p&gt;
&lt;p&gt;Use this as a readability tool, not only a visual effect. The best result is when style improves scanning speed without adding cognitive load.&lt;/p&gt;
&lt;p&gt;To roll this out safely, start by applying &lt;code&gt;shape-outside: url(your-img.png);&lt;/code&gt; in a single UI surface where the benefit is obvious. Then reuse that same pattern in similar contexts so behavior stays consistent and review time stays low.&lt;/p&gt;
&lt;p&gt;Before shipping, test &lt;code&gt;shape-outside: url(your-img.png);&lt;/code&gt; with both short and long content, then verify behavior in narrow and wide containers.&lt;/p&gt;
&lt;hr&gt;&lt;p&gt;If you liked this tip, you might enjoy the book, which is packed with similar insights to help you build better websites without relying on JavaScript.&lt;/p&gt;
&lt;p&gt;Go check it out &lt;a href="https://theosoti.com/you-dont-need-js/"&gt;https://theosoti.com/you-dont-need-js/&lt;/a&gt; and enjoy 20% OFF for a limited time!&lt;/p&gt;  &lt;/article&gt;&lt;/div&gt;&lt;/div&gt;&lt;/body&gt;&lt;/html&gt;</content>
        <author>
            <name/>
        </author>
        <media:content medium="image" url="https://theosoti.com/short/07-2025/shape-outside-image.avif"/>
    </entry>
    <entry>
        <id>tag:feedly.com,2013:cloud/entry/cOXRjoksGIQzU1pdqbdybn+OTWYeI0riptNneGckdsI=_19dbdc617f2:2a260d:93a1d9ea</id>
        <title type="html">Why The Split? - MeshCore Blog</title>
        <published>2026-04-24T04:36:09Z</published>
        <updated>2026-04-24T04:36:13Z</updated>
        <link href="https://blog.meshcore.io/2026/04/23/the-split" rel="alternate" type="text/html"/>
        <summary type="html">Migrating to the new meshcore.io site</summary>
        <content type="html">&lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"&gt;
&lt;html&gt;&lt;body&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;
        &lt;p&gt;Since inception, the MeshCore development team have been working hard to build MeshCore.&lt;/p&gt;

&lt;p&gt;We’ve released more than 85 versions of the MeshCore Companion, Repeater and Room Server firmwares with support for more than 75 hardware variants.
All of this has been hand crafted, by humans.&lt;/p&gt;

&lt;p&gt;We have always been wary of AI generated code, but felt everyone is free to do what
they want and experiment, etc. But, one of our own, Andy Kirby, decided to branch out
and extensively use Claude Code, and has decided to aggressively take over
all of the components of the MeshCore ecosystem: standalone devices, mobile app, 
web flasher and web config tools.&lt;/p&gt;

&lt;p&gt;And, he’s kept that &lt;em&gt;small&lt;/em&gt; detail a secret - that it’s all majority &lt;em&gt;vibe coded&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;We ran a poll recently, and asked in the MeshCore Discord about AI and trust, and these are the results:&lt;/p&gt;

&lt;p&gt;&lt;img alt="" src="https://blog.meshcore.io/assets/images/2026/04/23/trust-ai-gen-firmware.png"&gt;&lt;/p&gt;

&lt;p&gt;&lt;img alt="" src="https://blog.meshcore.io/assets/images/2026/04/23/have-right-to-know.png"&gt;&lt;/p&gt;

&lt;p&gt;The team didn’t feel it was our place to protest, until we recently discovered that Andy
applied for the MeshCore Trademark (on the 29th March, according to filings) and didn’t tell
any of us. We have tried discussing this, and what his intentions are, but those broke down
and we now have no communication with Andy.&lt;/p&gt;

&lt;p&gt;It’s been a stressful few months trying to sort this out, and is now a sad day
to bring this out to the public. It’s been a slap in the face to the team that
have worked so hard on this project, to have an insider team up with a robot
and a lawyer.&lt;/p&gt;

&lt;h2 id="official-meshcore"&gt;“Official” MeshCore&lt;/h2&gt;

&lt;p&gt;The use of the ‘official’ status is what is currently being contested. Andy is adamant 
that he &lt;em&gt;owns&lt;/em&gt; the brand, and is using the word very heavily with his MeshOS line.&lt;/p&gt;

&lt;p&gt;Meanwhile, in reality, the only ‘official’ MeshCore is the github repo. It’s the
&lt;em&gt;source of truth&lt;/em&gt; in terms of what is MeshCore, and Andy has &lt;em&gt;never&lt;/em&gt; contributed
to that.&lt;/p&gt;

&lt;p&gt;Since the internal split, we launched the &lt;a href="https://meshcore.io"&gt;meshcore.io&lt;/a&gt; site, as Andy controls
the meshcore.co.uk site and original discord server. We’ve been left with little other recourse. And, since 
launching the site, Andy copied the look and feel (again, using Claude) even though
we asked him not to.&lt;/p&gt;

&lt;h2 id="project-growth"&gt;Project Growth&lt;/h2&gt;

&lt;p&gt;The MeshCore project has been on an incredible journey.&lt;/p&gt;

&lt;p&gt;Having only started in January 2025, we have grown extremely fast!&lt;/p&gt;

&lt;p&gt;As of this post, the official &lt;a href="https://map.meshcore.io"&gt;MeshCore Map&lt;/a&gt; shows 38,000+ nodes around the world, and the official &lt;a href="https://meshcore.io"&gt;MeshCore App&lt;/a&gt; has more than 100,000+ active users across Android and iOS.&lt;/p&gt;

&lt;p&gt;It’s pretty epic how we’ve all built such an incredible community in such as a short time!&lt;/p&gt;

&lt;p&gt;As the project grows, so does our need for a dedicated space that provides you with official information from the &lt;em&gt;core team&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;In recent times, we’ve seen an explosion of growth in MeshCore web sites dedicated to specific countries and mesh communities.&lt;/p&gt;

&lt;p&gt;To name a few, we’ve seen:&lt;/p&gt;



&lt;p&gt;Andy Kirby did do an amazing job helping to promote the MeshCore project on his personal YouTube, but only promotes his own products now.&lt;/p&gt;

&lt;h2 id="where-to-from-here"&gt;Where To From Here?&lt;/h2&gt;

&lt;p&gt;So, the core team are pushing ahead with the &lt;a href="https://meshcore.io"&gt;meshcore.io&lt;/a&gt; website, the ongoing work of firmware feature development,
bug fixes, managing PR’s and developer discussions, etc.&lt;/p&gt;

&lt;p&gt;We now release change logs, blog posts and technical documentation for all of our new firmware and app releases here.&lt;/p&gt;



&lt;p&gt;You’ll also find some familiar faces on our blog posts, such as:&lt;/p&gt;

&lt;ul&gt;&lt;li&gt;&lt;strong&gt;Scott&lt;/strong&gt; our project founder, lead firmware engineer and developer of the Ripple firmware!&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Recrof&lt;/strong&gt; our official MeshCore Map developer and Firmware Flasher guru. He has shared some insights into the early development of the MeshCore Map.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Liam Cottle&lt;/strong&gt; the official MeshCore App developer who will be posting useful guides for getting started with the MeshCore App.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;FDLamotte&lt;/strong&gt; who has done epic work on the Python tooling for MeshCore, as well as the STM32 firmware variants.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Oltaco&lt;/strong&gt; (Che Aporeps) who has done amazing work on the new OTA Fix bootloader that makes firmware updates much more reliable.&lt;/li&gt;
&lt;/ul&gt;&lt;h2 id="the-core-team"&gt;The Core Team&lt;/h2&gt;

&lt;p&gt;The MeshCore team, now consisting of &lt;strong&gt;Scott&lt;/strong&gt;, &lt;strong&gt;Liam&lt;/strong&gt;, &lt;strong&gt;Recrof&lt;/strong&gt;, &lt;strong&gt;FDLamotte&lt;/strong&gt; and now &lt;strong&gt;Oltaco&lt;/strong&gt; remain committed to designing and developing high quality, &lt;em&gt;human-written&lt;/em&gt; software.&lt;/p&gt;

&lt;h2 id="our-new-home"&gt;Our New Home&lt;/h2&gt;

&lt;p&gt;Please update your bookmarks!&lt;/p&gt;

&lt;p&gt;This is where we will be hosting all official releases, technical documentation, and community discussions moving forward.&lt;/p&gt;

&lt;p&gt;With the new website, we are also starting fresh with a new Discord server!&lt;/p&gt;

&lt;p&gt;This is where you can interact directly with the MeshCore developers, get help with your projects, and contribute to the future of MeshCore.&lt;/p&gt;



&lt;p&gt;Thanks for being a part of this journey!&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The MeshCore Team&lt;/em&gt;&lt;/p&gt;

    &lt;/div&gt;

    

&lt;/div&gt;&lt;/div&gt;&lt;/body&gt;&lt;/html&gt;</content>
        <author>
            <name/>
        </author>
        <media:content medium="image" url="https://blog.meshcore.io/assets/images/icon.png"/>
        <source>
            <id>tag:feedly.com,2013:cloud/feed/https://blog.meshcore.io/feed.xml</id>
            <title type="html">blog.meshcore.io</title>
            <link href="https://blog.meshcore.io" rel="alternate" type="text/html"/>
            <updated>2026-04-24T04:36:13Z</updated>
        </source>
    </entry>
    <entry>
        <id>tag:feedly.com,2013:cloud/entry/cOXRjoksGIQzU1pdqbdybn+OTWYeI0riptNneGckdsI=_19dba76e5d6:36e5c9:335607a2</id>
        <title type="html">How to Stop A Data Center in Your Backyard ~ L.A. TACO</title>
        <published>2026-04-23T13:10:47Z</published>
        <updated>2026-04-23T13:10:51Z</updated>
        <link href="https://lataco.com/stop-sgv-data-center-building" rel="alternate" type="text/html"/>
        <summary type="html">These are lessons from San Gabriel Valley neighbors and activists who outsmarted developers and lobbyists.</summary>
        <content type="html">&lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"&gt;
&lt;html&gt;&lt;body&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;p&gt;When the people of Monterey Park found that their local government was going to approve a &lt;a href="https://www.datacenterdynamics.com/en/news/proposal-for-250000-sq-ft-data-center-in-monterey-park-california-facing-opposition/" target="_blank" rel="noreferrer noopener"&gt;&lt;strong&gt;250,000&lt;/strong&gt;&lt;/a&gt;-square-foot data center just 500 feet from their homes, they organized. &lt;/p&gt;&lt;p&gt;And within a few months, the developer withdrew their application.&lt;/p&gt;&lt;p&gt;Andrew Yip, an organizer with&lt;a href="https://www.sgvprogressiveaction.org/" target="_blank" rel="noreferrer noopener"&gt; &lt;strong&gt;SGV Progressive Action&lt;/strong&gt;&lt;/a&gt;, tells L.A. TACO that the organization’s success started with their “existing network of volunteers,” noting that “the community was able to jump in at a moment's notice.” &lt;/p&gt;&lt;p&gt;SGV Progressive Action was founded in 2020 to “address the Black Lives Matter uprisings," Yip says. "To support our Black community." &lt;/p&gt;&lt;p&gt;Then it organized &lt;a href="https://lapublicpress.org/2024/05/ceasefire-resolutions-have-spread-across-san-gabriel-valley-and-southeast-la/" target="_blank" rel="noreferrer noopener"&gt;&lt;strong&gt;local resolutions&lt;/strong&gt;&lt;/a&gt; advocating for a ceasefire in Palestine, and built a lending library in El Monte called &lt;a href="https://www.instagram.com/matilijacollective/" target="_blank" rel="noreferrer noopener"&gt;&lt;strong&gt;Matilija&lt;/strong&gt; &lt;strong&gt;Collective&lt;/strong&gt;&lt;/a&gt;, where they trained volunteers in community defense against ICE, hosted organizers, and stored 20 canopies and a speaker system. &lt;/p&gt;&lt;p&gt;"So that existed," Yip says.&lt;/p&gt;&lt;p&gt;In November, a community member who had come to a council meeting for other business saw the data center on the agenda and called on SGV Progressive Action. &lt;/p&gt;&lt;p&gt;"They asked if we can take a look at this," Yip says. "And see if that's something that communities should be concerned about."&lt;/p&gt;&lt;p&gt;All that was needed was one last council vote. But the developer requested a delay to the next meeting. &lt;/p&gt;&lt;p&gt;"Had they voted that day, it would have been done, right? It would have been done," Yip says. “But we found out about it, and we turned out hundreds of people to the next meeting.”&lt;/p&gt;&lt;h3 class="wp-block-heading"&gt;CA PUBLIC RECORDS ACT&lt;/h3&gt;&lt;p&gt;Under &lt;a href="https://oag.ca.gov/open-meetings" target="_blank" rel="noreferrer noopener"&gt;&lt;strong&gt;California's Sunshine Laws&lt;/strong&gt;&lt;/a&gt;, local governments are required to turn over agendas, meeting minutes, attendance records, and emails. SGV Progressive Action immediately filed public records requests. &lt;/p&gt;&lt;p&gt;"That's really how we found out," Yip says.&lt;/p&gt;&lt;p&gt;The records showed city planners had given their &lt;a href="https://www.montereypark.ca.gov/DocumentCenter/View/16863/1977-Saturn-Data-Center---Notice-of-Intent-to-Adopt-a-Mitigated-Negative-Declaration" target="_blank" rel="noreferrer noopener"&gt;&lt;strong&gt;blessings&lt;/strong&gt;&lt;/a&gt; to the data center, saying it would not result in any “significant environmental impacts.” They had used the developer's own impact &lt;a href="https://ceqanet.lci.ca.gov/2024101397#:~:text=The%20Project%20would%20demolish%20the,of%20mechanical%20equipment%20platform%20screening)." target="_blank" rel="noreferrer noopener"&gt;&lt;strong&gt;assessment&lt;/strong&gt;&lt;/a&gt; in place of a more thorough state environmental review. &lt;/p&gt;&lt;p&gt;The records also showed the city had held a series of community meetings to ask residents what should be built at 1977 Saturn Street, but only notified people living within 500 feet. Each meeting drew between 20 and 60 people. Residents who were there told Yip and other organizers that the city clerk brought in people who backed the data center. It won with roughly 20 votes.&lt;/p&gt;&lt;p&gt;“Twenty-something votes determined [that] residents here wanted a data center,” Yip says. ”That just seemed like a weird recommendation coming out of a community town hall.”&lt;/p&gt;&lt;p&gt;Council Member Thomas Wong, who would vote for having the data center, also works at the power company that would sell its electricity.&lt;/p&gt;&lt;p&gt;The developer also bought a larger property at 1980 Saturn Street across the street. The data center trade magazines &lt;a href="https://www.latimes.com/b2b/ai-technology/story/2024-12-20/vacant-monterey-park-office-building-to-be-converted-into-data-center" target="_blank" rel="noreferrer noopener"&gt;&lt;strong&gt;reported&lt;/strong&gt;&lt;/a&gt; that these were part of a 13-parcel assembly, all meant for data centers. Yip asked the developers what they planned to do with 1980 Saturn; they said they were not authorized to discuss it.&lt;/p&gt;&lt;h3 class="wp-block-heading"&gt;NO DATA CENTER MPK &amp;amp; THE INFORMATION CAMPAIGN&lt;/h3&gt;&lt;p&gt;The residents of Monterey Park bought the domain &lt;a href="https://www.nodatacentermpk.org/" target="_blank" rel="noreferrer noopener"&gt;&lt;strong&gt;No Data Center MPK&lt;/strong&gt;&lt;/a&gt;. &lt;/p&gt;&lt;p&gt;“And we had a ton of people come out to support, whether it's walking the neighborhoods, distributing fliers, calling folks, creating artwork,” Yip says. “It was a big showing.”&lt;/p&gt;&lt;p&gt;They went door-to-door to tell their neighbors what the city had not told them: The data center would use twice as much electricity as all of Monterey Park. The 14 “backup” &lt;a href="https://lapublicpress.org/2026/01/a-data-center-boom-is-coming-to-the-san-gabriel-valley-residents-had-no-idea/" target="_blank" rel="noreferrer noopener"&gt;&lt;strong&gt;generators&lt;/strong&gt;&lt;/a&gt; would burn 200,000 gallons of diesel every year, without a blackout. And more when the grid price was high.&lt;/p&gt;&lt;p&gt;They held a teach-in. 150 people came. &lt;/p&gt;&lt;p&gt;“We have a lot of very smart residents who were able to do a lot of this research and fact-finding. Many of the residents we work with are researchers or hold PhDs, and they work in universities. So they know how to find this information,” Yip says. &lt;/p&gt;&lt;p&gt;One resident 3D-printed a model of the data center to show just how much of the neighborhood’s space it would take up. Another resident mixed noise recordings from data centers and played them over a loudspeaker. You couldn't hear the birds. &lt;/p&gt;&lt;p&gt;Two dozen people in Virginia who lived near a data center were ready to fly out to testify on their own dime. &lt;/p&gt;&lt;p&gt;“Virginia became ground zero for data center proliferation,” says Yip. “The people didn't know enough about data centers at the time.”&lt;/p&gt;&lt;p&gt;The vibrations and noise never stop, the people from Virginia warned. The reported sound levels of 60db and higher are far from the data center. They have taken to &lt;a href="https://www.businessinsider.com/data-centers-northern-virginia-noise-air-pollution-cost-2025-5" target="_blank" rel="noreferrer noopener"&gt;&lt;strong&gt;sleeping&lt;/strong&gt;&lt;/a&gt; in their basements. Neither a decibel meter nor the law measures vibration.&lt;/p&gt;&lt;p&gt;Yip and the organizers found that almost nobody in Monterey Park that they spoke to had heard of the data center. The city’s notification had only reached 40 people living within 500 feet of its proposed location, in English. &lt;/p&gt;&lt;p&gt;The neighborhood is 65 percent Asian and 27 percent Latino. The community’s outreach was done in at least three languages, five when necessary. It extended to all the surrounding neighborhoods.&lt;/p&gt;&lt;p&gt;“Data centers, their pollution, and their effects don't just stop at the border,” Yip says.&lt;/p&gt;&lt;p&gt;They started a petition in English, Chinese, and Spanish. It grew to 4,500 signatures.&lt;/p&gt;&lt;h3 class="wp-block-heading"&gt;THE DEVELOPERS &amp;amp; THE DISINFORMATION CAMPAIGN&lt;/h3&gt;&lt;p&gt;The developers retained a law firm with 1,000 attorneys on four continents. They &lt;a href="https://www.sgvtribune.com/2026/03/03/monterey-park-data-center-plan-back-in-front-of-city-council/" target="_blank" rel="noreferrer noopener"&gt;&lt;strong&gt;hired&lt;/strong&gt;&lt;/a&gt; &lt;a href="https://actumllc.com/" target="_blank" rel="noreferrer noopener"&gt;&lt;strong&gt;Actum&lt;/strong&gt;&lt;/a&gt;, the lobbying firm that represents Amazon and Clorox. Actum lists Trump’s former chief of staff as a &lt;a href="https://actumllc.com/people/mick-mulvaney/" target="_blank" rel="noreferrer noopener"&gt;&lt;strong&gt;partner&lt;/strong&gt;&lt;/a&gt; and another who exploited a loophole so large that California had to &lt;a href="https://www.sacbee.com/news/politics-government/capitol-alert/article314044368.html" target="_blank" rel="noreferrer noopener"&gt;&lt;strong&gt;legislate&lt;/strong&gt;&lt;/a&gt; to close it.&lt;/p&gt;&lt;p&gt;"The applicant sent a letter to the city council talking about misinformation being spread, and that was exactly what the council members said," Yip tells us. "They just parroted the same talking points."&lt;/p&gt;&lt;p&gt;The political lobbyists canvassed neighborhoods and local shops. &lt;/p&gt;&lt;p&gt;"One of the public benefits of the project they were touting was a pocket park ... The pocket park is basically just leftover land on their property that they didn't really need for the data center," Yip says.&lt;/p&gt;&lt;p&gt;They promised 200 jobs and, in the same conversation, no traffic. &lt;/p&gt;&lt;p&gt;“Our people would poke holes in it,” Yip says. “Why wouldn't there be cars in that facility if you're going to have a lot of employees?”&lt;/p&gt;&lt;p&gt;No Data Center MPK retained an environmental and land use attorney. They recommended an ordinance banning data centers immediately, then to reinforce it with a ballot measure.&lt;/p&gt;&lt;p&gt;They set up a one-click email so residents could send comments to City Council demanding both.&lt;/p&gt;&lt;p&gt;Hundreds showed up to the next three council meetings. &lt;/p&gt;&lt;p&gt;“We played mahjong on the City Hall lawn while we waited. We had a lion dance right outside to cheer people on as they entered the council chambers,” Yip says. &lt;/p&gt;&lt;p&gt;The chambers filled, overflowing into the aisles and hallways.&lt;/p&gt;&lt;p&gt;The first meeting produced a 45-day ban on data centers, the second brought a ballot measure that would ban them forever. &lt;/p&gt;&lt;p&gt;During the third meeting, opponents of the data center called for a rally at 5:30 p.m. before the 6:30 p.m. meeting. Steven Kung, the Monterey Park resident who purchased the No Data Center MPK domain, addressed the developers, their lawyers, and the lobby firm directly.&lt;/p&gt;&lt;p&gt;“You’re fighting an uphill battle against an entire city that doesn’t want you here and yet you continue to bully your way into this community of color, to pollute the air we breathe, to make electricity more expensive, to devalue our homes, to drain our energy and resources like a parasite,” Kung &lt;a href="https://www.instagram.com/reels/DUYeFzXgCYe/" target="_blank" rel="noreferrer noopener"&gt;&lt;strong&gt;says&lt;/strong&gt;&lt;/a&gt;. “You think you can take us on? You’ve messed with the wrong city. ”&lt;/p&gt;&lt;p&gt;On March 31, the developer withdrew its application.&lt;/p&gt;&lt;p&gt;“They underestimated the community's passion. And we never underestimated them. And I think that was a good strategy,” says Yip.&lt;/p&gt;&lt;p&gt;The &lt;a href="https://www.ca.gov/departments/235/" target="_blank" rel="noreferrer noopener"&gt;&lt;strong&gt;FPPC&lt;/strong&gt;&lt;/a&gt;, California’s political ethics commission, saw the data center would have a “material financial effect” on councilmember Wong. His power to vote on them was &lt;a href="https://www.fppc.ca.gov/siteassets/documents/legal_div/advice_letters/2020-2026/2026/25147.pdf" target="_blank" rel="noreferrer noopener"&gt;&lt;strong&gt;stripped&lt;/strong&gt;&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;Yip says the people who joined for the fight over 1977 Saturn Street have overwhelmingly stayed active with SGV Progressive Action.&lt;/p&gt;&lt;p&gt;"They recognize it's not just about data centers. It's about building community and protecting your community," he says.&lt;/p&gt;&lt;p&gt;The volunteers who organized against the data center are now working to &lt;a href="https://www.instagram.com/p/DVrs4wxDwKk/?img_index=1" target="_blank" rel="noreferrer noopener"&gt;&lt;strong&gt;strengthen&lt;/strong&gt;&lt;/a&gt; sanctuary ordinances to protect their communities from ICE.&lt;/p&gt;&lt;p&gt;“And now we have a coalition of all these community members coming from La Puente, Avocado Heights, Rowland Heights, and Hacienda Heights coming together to fight this common enemy. And I'm just going to name it the City of Industry,” Yip says.&lt;/p&gt;&lt;h3 class="wp-block-heading"&gt;NO DATA CENTERS SGV COALITION&lt;/h3&gt;&lt;p&gt;Samuel Brown Vazquez rode ten miles horseback from Avocado Heights to the Monterey Park council meeting. Vazquez is a community organizer and founding member of the &lt;a href="https://www.instagram.com/avocadoheightsvaqueros/" target="_blank" rel="noreferrer noopener"&gt;&lt;strong&gt;Avocado Heights Vaqueros&lt;/strong&gt;&lt;/a&gt;. &lt;/p&gt;&lt;p&gt;The City of Industry is in the process of approving zoning changes that would clear the way for three data centers and battery energy storage that would affect the residents of Hacienda Heights, La Puente, Walnut, Diamond Bar, West Covina and others.&lt;/p&gt;&lt;p&gt;The Avocado Heights Vaqueros, SGV Progressive Action, and others organized across city and county lines. &lt;/p&gt;&lt;p&gt;“We created an infographic and started mobilizing folks,” Vazquez says. They are &lt;a href="https://www.nodatacenterssgvcoalition.org/" target="_blank" rel="noreferrer noopener"&gt;&lt;strong&gt;No Data Centers SGV Coalition&lt;/strong&gt;&lt;/a&gt;.&lt;/p&gt;&lt;p&gt;Like Monterey Park, they canvassed door-to-door, showed up to every City of Industry meeting, started a petition, and filed public records requests.&lt;/p&gt;&lt;p&gt;The record requests showed the City of Industry had been &lt;a href="https://www.documentcloud.org/documents/27693914-re-quick-call-07-05-2025-produce/" target="_blank" rel="noreferrer noopener"&gt;&lt;strong&gt;discussing&lt;/strong&gt;&lt;/a&gt; zoning changes with developers at Puente Hills Mall, Madrid Middle School, and two battery storage facilities near Hacienda Heights. All just feet from homes and schools.&lt;/p&gt;&lt;p&gt;In February, the city unanimously rezoned Puente Hills Mall for battery storage. Months earlier, the city manager, Joshua Nelson, &lt;a href="https://investigatela.org/2026/03/02/city-of-industry-discussed-data-center-sites-months-before-zoning-vote/" target="_blank" rel="noreferrer noopener"&gt;&lt;strong&gt;emailed the developers that&lt;/strong&gt;&lt;/a&gt; they were working to allow data centers anywhere in the city.&lt;/p&gt;&lt;p&gt;Again, organizers found that people had no idea what the city was planning to put next to their homes. Battery centers burned for days when they caught fire. One at Moss Landing had &lt;a href="https://www.youtube.com/watch?v=ooYmpF0utVs" target="_blank" rel="noreferrer noopener"&gt;&lt;strong&gt;spilled&lt;/strong&gt;&lt;/a&gt; 55,000 tons of nickel, cobalt, and lithium.&lt;/p&gt;&lt;p&gt;“Maybe only one or two people had heard," said Sophia Ramirez, an organizer and the daughter of Zacatecan immigrants. “That was pretty shocking.”' &lt;/p&gt;&lt;p&gt;Ramirez, a Cal Poly biology grad, explained in the outreach, in plain language, how PM2.5 and PM10 particles could slip past the body's filters into the lungs, then the blood. &lt;/p&gt;&lt;p&gt;The No Data Center SGV petition grew to 18,000 signatures.&lt;/p&gt;&lt;p&gt;In Monterey Park, the people who organized were also the people who vote. In the City of Industry, there hasn’t been a competitive election in &lt;a href="https://www.latimes.com/local/politics/la-me-industry-audit-20160129-story.html" target="_blank" rel="noreferrer noopener"&gt;&lt;strong&gt;10 years&lt;/strong&gt;&lt;/a&gt;, and only four &lt;a href="https://www.latimes.com/archives/la-xpm-1992-03-22-ga-7418-story.html" target="_blank" rel="noreferrer noopener"&gt;&lt;strong&gt;since 1957&lt;/strong&gt;&lt;/a&gt;. &lt;/p&gt;&lt;p&gt;State law says when fewer people run than there are open seats for, a city doesn't have to hold an election. The City of Industry council appoints its council members and some of its members are descendents of the original founders.&lt;/p&gt;&lt;p&gt;The City of Industry has 256 residents, the largest financial reserves of any city in the San Gabriel Valley, and a &lt;a href="https://www.bibliovault.org/BV.book.epl?ISBN=9780813551920" target="_blank" rel="noreferrer noopener"&gt;&lt;strong&gt;history&lt;/strong&gt;&lt;/a&gt; of building its wealth at the expense of the surrounding working-class Latino and Asian Pacific Islander communities. &lt;/p&gt;&lt;p&gt;"I had normalized what it was like to live next to City of Industry,” Vazquez says. “I thought it was normal to just grow up near all these warehouses."&lt;/p&gt;&lt;p&gt;The people of the surrounding areas, Covina, Diamond Bar, El Monte, La Puente, Pomona, Walnut, and West Covina, would all be affected by the data centers, but have no political power over the City of Industry.&lt;/p&gt;&lt;p&gt;“To think of it in the context of environmental racism, environmental injustices, it’s really crazy that it's like the city of industry has decided that these communities that live around them are not valuable lives,” Ramirez says. “Zonas de sacrificio.”&lt;/p&gt;&lt;p&gt;People from La Puente, Avocado Heights, Rowland Heights, Diamond Bar, Walnut, and Hacienda Heights came to the City of Industry’s March 26 council meeting where they planned to change the city's zoning code to allow data centers. &lt;/p&gt;&lt;p&gt;The city’s email went down before the &lt;a href="https://www.instagram.com/reels/DWXiRobj0OI/" target="_blank" rel="noreferrer noopener"&gt;&lt;strong&gt;meeting&lt;/strong&gt;&lt;/a&gt;. People said it often does. The only way to comment is to show up. More than 100 people did, at 9 a.m. on a workday. Before public comment, the council went into closed session. &lt;/p&gt;&lt;p&gt;“They made us all wait outside in the heat,” Ramirez says. &lt;/p&gt;&lt;p&gt;Ramirez said that it was 90 degrees, and there were many elders. After two hours, roughly 40 were let inside. Outside, there was no livestream. They chanted, "Let us in." &lt;/p&gt;&lt;p&gt;When the doors opened, only about 30 people were allowed to speak. Their comment time was cut from three minutes to one minute. Mayor Pro Tem Greubel got up and walked out halfway through.&lt;/p&gt;&lt;p&gt;The City of Industry does not provide interpreters, translated materials, or any way for people who speak other languages to comment. They haven't posted meeting minutes in over a year. More than a quarter of their meetings are called with 24 hours notice.&lt;/p&gt;&lt;p&gt;“Everything about City of Industry is designed to minimize participation of the public,” Vazquez tells us. ”Like, that is not an exaggeration to say that.”&lt;/p&gt;&lt;h3 class="wp-block-heading"&gt;VALLE IMPERIAL RESISTE VS. IMPERIAL COUNTY&lt;/h3&gt;&lt;p&gt;Gilberto Manzanarez, an organizer and founder of &lt;a href="https://www.instagram.com/valleimperialresiste/" target="_blank" rel="noreferrer noopener"&gt;&lt;strong&gt;Valle Imperial Resiste&lt;/strong&gt;&lt;/a&gt;, learned of the data center on Facebook. Someone had leaked the planning commission’s map over Thanksgiving break. &lt;/p&gt;&lt;p&gt;Manzanarez grabbed his camera, drove to the site, and &lt;a href="https://www.instagram.com/p/DRnBkV9ktI2/" target="_blank" rel="noreferrer noopener"&gt;&lt;strong&gt;posted&lt;/strong&gt;&lt;/a&gt; a video that spread across the valley. &lt;/p&gt;&lt;p&gt;The Imperial Valley data center would be one of the world’s largest at nearly one million square feet. It would use double the electricity of Imperial Valley and 750,000 gallons of water every day. County planning staff decided they “qualified as a permitted industrial use,” and there would be no need for environmental review.&lt;/p&gt;&lt;p&gt;Manzanarez, also a history teacher, says, “Public health always takes a backseat to economic development in the Imperial Valley.” &lt;/p&gt;&lt;p&gt;The &lt;a href="https://www.niehs.nih.gov/research/supported/translational/community/imperial#:~:text=Air%20pollution%20in%20Imperial%20County%20comes%20from%20agricultural%20burns%2C%20diesel,sources%20such%20as%20automobile%20exhaust." target="_blank" rel="noreferrer noopener"&gt;&lt;strong&gt;air carries&lt;/strong&gt;&lt;/a&gt; cropland burnings, diesel fumes, and dust from the drying Salton Sea, laced with pesticides and heavy metals. More than &lt;a href="https://keck.usc.edu/news/children-living-near-the-salton-sea-in-southern-california-show-slower-lung-function-growth/" target="_blank" rel="noreferrer noopener"&gt;&lt;strong&gt;one in five children&lt;/strong&gt;&lt;/a&gt; that live around the Salton Sea have asthma.&lt;/p&gt;&lt;p&gt;Despite decades of investment, solar farms, geothermal plants, military bases, and canals, the 80 percent Latino region’s unemployment sits at &lt;a href="https://labormarketinfo.edd.ca.gov/file/lfmonth/lf_geomaps.pdf" target="_blank" rel="noreferrer noopener"&gt;&lt;strong&gt;three times&lt;/strong&gt;&lt;/a&gt; the national average.&lt;/p&gt;&lt;p&gt;“The promise of economic development and jobs, the same thing, these are stories that we already heard when we had the solar farm boom. Those solar panel projects were sold to us,” Manzanarez says. “It's like, this is going to save us. This is going to lift us out of poverty ... Thousands of people across the Imperial Valley were hired, and they were excited to go work. Fast forward 10 years, 2026, guess what? We have the highest unemployment rate in the state of California. Again.”&lt;/p&gt;&lt;p&gt;Bryan Vega, another local organizer, saw the Valle Imperial Resiste post and joined the other activists. They knocked on doors, handed out flyers, and flooded Instagram with informational videos. &lt;/p&gt;&lt;p&gt;“We started to share information about what the data center is and invited folks to submit public comment,” Vega says.&lt;/p&gt;&lt;p&gt;In January, they held a protest on Main Street and Imperial Avenue. They started a petition to enact the Imperial County Data Center Prohibition Act. &lt;/p&gt;&lt;p&gt;On March 26, the Imperial County Board of Supervisors called for an evening meeting, outside of their normal schedule, specifically about the data center. &lt;/p&gt;&lt;p&gt;The main chamber filled 30 minutes before it started. Two overflow rooms opened in a separate building. And when those filled, too, more than 60 people stood in the parking lot. &lt;/p&gt;&lt;p&gt;The developer, Sebastian Rucci, spoke. There were to be no questions.&lt;/p&gt;&lt;p&gt;Vega recalled standing out in the parking lot. &lt;/p&gt;&lt;p&gt;“Someone outside said, ‘Why are we just standing here? Why are we not more upset? They're making decisions about us in there. And we're out here. And we should be in there. We go to the door and we're like, ‘No Data Center. No Data Center.’ And it's like a battle cry from our soul,” he says.&lt;/p&gt;&lt;p&gt;Vega said one of the organizers, from &lt;a href="https://www.instagram.com/ivforpalestine/" target="_blank" rel="noreferrer noopener"&gt;&lt;strong&gt;Imperial Valley for Palestine&lt;/strong&gt;&lt;/a&gt;, had a bullhorn in her car, “because, duh, she's an organizer and she's always prepared for these things."&lt;/p&gt;&lt;p&gt;&lt;em&gt;KPBS&lt;/em&gt; &lt;a href="https://www.kpbs.org/news/environment/2026/04/03/imperial-county-supervisors-to-hold-key-vote-on-controversial-data-center-project" target="_blank" rel="noreferrer noopener"&gt;&lt;strong&gt;reported&lt;/strong&gt;&lt;/a&gt; that “county officials paused the meeting and Rucci departed early after protestors drowned him out.” &lt;/p&gt;&lt;p&gt;Videos &lt;a href="https://www.instagram.com/p/DWZ-xwCkpns/" target="_blank" rel="noreferrer noopener"&gt;&lt;strong&gt;show&lt;/strong&gt;&lt;/a&gt; Imperial County sheriff’s deputies escorting Rucci and his business partner, Hector Casas, to their car. &lt;/p&gt;&lt;p&gt;Outside, the 60 people who weren’t allowed in chanted “Fuera! Fuera!” at Rucci and Casas as they drove off.&lt;/p&gt;&lt;p&gt;"This meeting was a sham,” Manzanarez says. “They didn't want to educate us. They didn't want to hear people. They just wanted to check a box."&lt;/p&gt;&lt;p&gt;&lt;em&gt;KPBS&lt;/em&gt; &lt;a href="https://www.kpbs.org/news/environment/2026/01/22/4-takeaways-from-kpbs-investigation-into-a-massive-data-center-project-in-imperial-county" target="_blank" rel="noreferrer noopener"&gt;&lt;strong&gt;reported&lt;/strong&gt;&lt;/a&gt; that in 2010, Ohio prosecutors charged Rucci with money laundering, promoting prostitution, and perjury at a Youngstown nightclub. The felonies were thrown out, but he served 30 days for selling alcohol on an expired license. He later opened an addiction treatment center. The state revoked its certification after finding falsified records. &lt;/p&gt;&lt;p&gt;In 2021, the FBI raided the treatment center and seized more than $600,000. No criminal charges were filed. Rucci sued, got the money back, and is still &lt;a href="https://www.opn.ca6.uscourts.gov/opinions.pdf/25a0306p-06.pdf" target="_blank" rel="noreferrer noopener"&gt;&lt;strong&gt;fighting&lt;/strong&gt;&lt;/a&gt; in federal court. &lt;/p&gt;&lt;p&gt;In an &lt;a href="https://www.kpbs.org/news/environment/2026/01/21/the-plan-to-build-a-massive-data-center-in-imperial-county-without-environmental-review" target="_blank" rel="noreferrer noopener"&gt;&lt;strong&gt;interview&lt;/strong&gt;&lt;/a&gt; with &lt;em&gt;KPBS&lt;/em&gt;, he said, "I won them all but one."&lt;/p&gt;&lt;p&gt;Rucci and his partner, Hector Casas, targeted Imperial County because the zoning laws allowed them to skip environmental review. &lt;/p&gt;&lt;p&gt;"Our whole goal is speed," Rucci &lt;a href="https://www.kpbs.org/news/environment/2026/01/21/the-plan-to-build-a-massive-data-center-in-imperial-county-without-environmental-review" target="_blank" rel="noreferrer noopener"&gt;&lt;strong&gt;told&lt;/strong&gt;&lt;/a&gt;&lt;strong&gt; &lt;/strong&gt;&lt;em&gt;KPBS&lt;/em&gt;. "That is not sneaky. That's just smart."&lt;/p&gt;&lt;p&gt;On April 7, at 8 a.m., 50 men got off a tour bus with identical orange vests that said, "Data centers equal jobs and prosperity." &lt;/p&gt;&lt;p&gt;They were led through the side entrance of the County Administration Building before any members of the community were allowed through the front. A leaked internal county email sent the night before warned of high turnout and increased security. Despite that, the county provided no overflow room. &lt;/p&gt;&lt;p&gt;Over 100 community members were left outside in 96-degree heat for five to six hours. Elders with walkers. No shade, no water, no chairs. One community member got kicked out for calling out the outsiders taking seats from residents.&lt;/p&gt;&lt;p&gt;The board of supervisors voted to approve the lot merger. Only one supervisor voted no.&lt;/p&gt;&lt;p&gt;Senator Padilla's SB 887 would require all new data centers in California to go through environmental review. The bill does not ban data centers, it only requires developers to study their impact and hear from the public before building.&lt;/p&gt;&lt;p&gt;On April 2, the organizers filed the Imperial County Data Center Prohibition Act, the first step toward a ballot measure that would ban data centers across the county. &lt;/p&gt;&lt;p&gt;“Our parents and grandparents sacrificed so much to be able to be in the Imperial Valley ... part of the sacrifice was having to accept a hard hand,“ Vegas says. “Like when I think about what my Mexican farmworker parents had to undergo, it makes it almost intuitive to fight for the environment, intuitive to fight for the things that I know are true to them, but also very simply put, we would not be here without that."&lt;/p&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/body&gt;&lt;/html&gt;</content>
        <author>
            <name>Julianne Le</name>
        </author>
        <media:content medium="image" url="https://lede-admin.lataco.com/wp-content/uploads/sites/45/2026/04/Document-e1776358247494.jpeg"/>
    </entry>
    <entry>
        <id>tag:feedly.com,2013:cloud/entry/cOXRjoksGIQzU1pdqbdybn+OTWYeI0riptNneGckdsI=_19d9992bf3a:36b762:9f9ae75a</id>
        <title type="html">How one programmer's pet project changed how we think about software</title>
        <published>2026-04-17T03:53:44Z</published>
        <updated>2026-04-17T03:54:07Z</updated>
        <link href="https://www.youtube.com/watch?v=Y24vK_QDLFg" rel="alternate" type="text/html"/>
        <summary type="html">This is the story of how one programmer's obsession with simplicity quietly reshaped how the software world thinks about time, immutability, and what it mean...</summary>
        <content type="html">&lt;div&gt;&lt;iframe width="560" height="315" src="https://www.youtube.com/embed/Y24vK_QDLFg"&gt;&lt;/iframe&gt;&lt;br&gt;&lt;p&gt;This is the story of how one programmer's obsession with simplicity quietly reshaped how the software world thinks about time, immutability, and what it means to write code that lasts. From a sabbatical pet-project to the backbone of one of the world's largest fintechs and a global community that treats their language like a philosophy. This is the story of Clojure. 

---

This documentary wouldn't exist without the kind support of Nubank: https://building.nubank.com/engineering/

Thanks to Railway, our channel sponsor, for supporting all of our films:
Railway is the all-in-one intelligent cloud provider ➡️ https://railway.com/?referralCode=cultrepo

---

The Clojure Documentary features:

Alessandra Sierra, Alex Miller, Chris Houser, David Nolen, Ed Wible, Eric Normand, Eric Thorsen, Lucas Cavalcanti, Michael Fogus, Nathan Marz, Rich Hickey, Steph Hickey, and Stuart Halloway.

Film Credits: 
Directed by: Cormac Dunne
Produced by: Emma Tracey
Additional direction: Joey Bania 
Music supervision and sound design: Tomás Malara

---

For a full overview of all the papers, talks, essays, etc. that appear in the film, check this link: https://clojure.org/about/documentary


---

Follow us:
X: x.com/CultRepo
Bluesky: cultrepo.bsky.social
Instagram: www.instagram.com/cult.repo
LinkedIn: https://www.linkedin.com/company/cult-repo&lt;/p&gt;&lt;/div&gt;</content>
        <author>
            <name>TLDR News EU</name>
        </author>
        <media:content medium="image" url="https://i.ytimg.com/vi/Y24vK_QDLFg/maxresdefault.jpg"/>
        <link href="https://www.youtube.com/embed/Y24vK_QDLFg" rel="enclosure" type="text/html"/>
    </entry>
    <entry>
        <id>tag:feedly.com,2013:cloud/entry/cOXRjoksGIQzU1pdqbdybn+OTWYeI0riptNneGckdsI=_19d96383117:a4ef3:e8c372da</id>
        <title type="html">‘By Design’ Flaw in MCP Could Enable Widespread AI Supply Chain Attacks</title>
        <published>2026-04-16T12:15:58Z</published>
        <updated>2026-04-16T12:16:01Z</updated>
        <link href="https://www.securityweek.com/by-design-flaw-in-mcp-could-enable-widespread-ai-supply-chain-attacks/" rel="alternate" type="text/html"/>
        <summary type="html">Researchers warn that a flaw in Anthropic’s Model Context Protocol allows unsanitized commands to execute silently, enabling full system compromise across widely used AI environments.</summary>
        <content type="html">&lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"&gt;
&lt;html&gt;&lt;body&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;
		
&lt;p&gt;&lt;strong&gt;Model Context Protocol (MCP) has been a boon to agentic AI users and is widely used and trusted locally by companies adopting agentic AI internally. &lt;/strong&gt;



&lt;/p&gt;&lt;p&gt;Introduced by Anthropic in November 2024, it provides a standard connector between agents and data. Enterprises use it locally to avoid the pain of developing their own connectors, and it is in widespread use as a local STDIO MCP server.



&lt;/p&gt;&lt;p&gt;There are multiple providers of MCP servers, almost all inheriting Anthropic’s code. The problem, &lt;a href="https://20204725.hs-sites.com/the-mother-of-all-ai-supply-chains" target="_blank"&gt;reports&lt;/a&gt; OX Security, is what it terms an architectural flaw in Anthropic’s MCP code embedded within most of these local STDIO MCPs.



&lt;/p&gt;&lt;p&gt;In a nutshell, OX Security says this flaw can result in a complete adversarial takeover over the user’s computer system. “And the exploit mechanism is straightforward, MCP’s STDIO interface was designed to launch a local server process. But the command is executed regardless of whether the process starts successfully,” reports OX.



&lt;/p&gt;&lt;p&gt;“Pass in a malicious command, receive an error – and the command still runs. No sanitization warnings. No red flags in the developer toolchain. Nothing.”



&lt;/p&gt;&lt;p&gt;OX extensively tested whether this ‘flaw’ was exploitable, extensively succeeded, and extensively disclosed its findings to the MCP providers; from Anthropic downward. Initially it had little response. Eventually, the common response was inaction coupled with the suggestion that this behavior was ‘by design’. &lt;/p&gt;&lt;div&gt;&lt;span&gt;Advertisement. Scroll to continue reading.&lt;/span&gt;&lt;/div&gt;



&lt;p&gt;But OX discovered, and demonstrated, that this ‘by design’ behavior could be easily exploited, leaving potentially millions of downstream users exposed to sensitive data, API key and internal corporate data theft, the exposure of chat histories, and more. If the process that MCP failed included malware, that malware could be silently installed, potentially leading to complete system takeover.



&lt;/p&gt;&lt;p&gt;Eventually, the only apparent action from Anthropic was to quietly update its security guidance to recommend MCP adapters be used ‘with caution’ – “leaving the flaw intact and shifting responsibility to developers”.



&lt;/p&gt;&lt;p&gt;This is an interesting position to take. It suggests that developers are responsible for the security of what they develop, which is fair. It possibly also suggests that any company so breached is not the responsibility of Anthropic, but the fault of misconfiguring the MCP installation – which certainly &lt;a href="https://www.securityweek.com/the-wild-wild-west-of-agentic-ai-an-attack-surface-cisos-cant-afford-to-ignore/"&gt;does happen&lt;/a&gt;. And to be fair, GitHub’s own installation was an exception to the OX testing, proving that security gating on installation is possible.



&lt;/p&gt;&lt;p&gt;&lt;a href="https://www.airisksummit.com/" target="_blank"&gt;&lt;strong&gt;Learn More at the AI Risk Summit | Ritz-Carlton, Half Moon Bay&lt;/strong&gt;&lt;/a&gt;



&lt;/p&gt;&lt;p&gt;But the sheer volume of successful compromises conducted by OX demonstrates that the developers installing MCP servers are failing to install successfully. This should be no surprise when AI is automating so many aspects of security and lowering the bar of security competence among developers.



&lt;/p&gt;&lt;p&gt;The OX position is that Anthropic should take responsibility and fix this ‘architectural flaw’ itself. Without doing so, it is leaving industry open to “the mother of all supply chain attacks”, starting from Anthropic, fanning out to many thousands of local MCP users, and from those compromised systems to who knows how many other servers.



&lt;/p&gt;&lt;p&gt;During its research, OX adopted a coordinated disclosure process, leading to more than 30 accepted disclosures and more than 10 high and critical vulnerabilities patched. But the underlying design flaw, it says, remains, leaving millions of users and thousands of systems exposed to unauthorized access. “The current implementation of the Model Context Protocol places the entire burden of security on the downstream developer – a structural failure that guarantees vulnerability at scale.”



&lt;/p&gt;&lt;p&gt;The OX report on its findings includes details on how Anthropic could solve the problem by deprecating unsanitized STDIO connections, introducing protocol level command sandboxing, including a ‘dangerous mode’ explicit opt-in, and developing marketplace verification standards to include a standardized security manifest.



&lt;/p&gt;&lt;p&gt;In the meantime, any company adopting STDIO MCP as part of an agentic AI development should do so ‘with caution’.



&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Related&lt;/strong&gt;: &lt;a href="https://www.securityweek.com/anthropic-mcp-server-flaws-lead-to-code-execution-data-exposure/"&gt;Anthropic MCP Server Flaws Lead to Code Execution, Data Exposure&lt;/a&gt;



&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Related&lt;/strong&gt;: &lt;a href="https://www.securityweek.com/top-25-mcp-vulnerabilities-reveal-how-ai-agents-can-be-exploited/"&gt;Top 25 MCP Vulnerabilities Reveal How AI Agents Can Be Exploited&lt;/a&gt;



&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Related&lt;/strong&gt;: &lt;a href="https://www.securityweek.com/the-new-rules-of-engagement-matching-agentic-attack-speed/"&gt;The New Rules of Engagement: Matching Agentic Attack Speed&lt;/a&gt;



&lt;/p&gt;&lt;p&gt;&lt;strong&gt;Related&lt;/strong&gt;: &lt;a href="https://www.securityweek.com/anthropic-unveils-claude-mythos-a-cybersecurity-breakthrough-that-could-also-supercharge-attacks/"&gt;Anthropic Unveils ‘Claude Mythos’ – A Cybersecurity Breakthrough That Could Also Supercharge Attacks&lt;/a&gt;
			&lt;/p&gt;&lt;/div&gt;
	&lt;/div&gt;&lt;/div&gt;&lt;/body&gt;&lt;/html&gt;</content>
        <author>
            <name>Kevin Townsend</name>
        </author>
        <media:content medium="image" url="https://www.securityweek.com/wp-content/uploads/2026/04/MCP_Vulnerability.jpg"/>
        <source>
            <id>tag:feedly.com,2013:cloud/feed/https://www.securityweek.com/feed/</id>
            <title type="html">SecurityWeek</title>
            <link href="https://www.securityweek.com" rel="alternate" type="text/html"/>
            <updated>2026-04-16T12:16:01Z</updated>
        </source>
    </entry>
    <entry>
        <id>tag:feedly.com,2013:cloud/entry/cOXRjoksGIQzU1pdqbdybn+OTWYeI0riptNneGckdsI=_19d95ec6bfd:3e533:44dea31a</id>
        <title type="html">font-family Doesn’t Fall Back the Way You Think</title>
        <published>2026-04-16T10:53:12Z</published>
        <updated>2026-04-16T10:53:17Z</updated>
        <link href="https://csswizardry.com/2026/04/font-family-doesnt-fall-back-the-way-you-think/" rel="alternate" type="text/html"/>
        <summary type="html">A quick but important reminder that font-family declarations don’t inherit fallback stacks the way many developers assume.</summary>
        <content type="html">&lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"&gt;
&lt;html&gt;&lt;body&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;

        

        &lt;p&gt;
          &lt;time datetime="2026-04-10T11:30:00+00:00"&gt;10 April, 2026&lt;/time&gt;&lt;/p&gt;

          &lt;h1&gt;font-family Doesn’t Fall Back the Way You Think&lt;/h1&gt;

          &lt;p&gt;Written by &lt;b&gt;Harry Roberts&lt;/b&gt; on &lt;b&gt;CSS Wizardry&lt;/b&gt;.&lt;/p&gt;

          

          
            &lt;details&gt;&lt;summary&gt;Table of Contents&lt;/summary&gt;&lt;p&gt;Independent writing is brought to you via my wonderful
                  &lt;a href="https://csswizardry.com/supporters/"&gt;Supporters&lt;/a&gt;.&lt;/p&gt;

                &lt;ol&gt;&lt;li&gt;&lt;a href="https://csswizardry.com/2026/04/font-family-doesnt-fall-back-the-way-you-think/#font-family-fallbacks-are-self-contained"&gt;&lt;code&gt;font-family&lt;/code&gt; Fallbacks Are Self-Contained&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://csswizardry.com/2026/04/font-family-doesnt-fall-back-the-way-you-think/#why-you-get-a-flash-of-times"&gt;Why You Get a Flash of Times&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://csswizardry.com/2026/04/font-family-doesnt-fall-back-the-way-you-think/#the-fix-is-simple"&gt;The Fix Is Simple&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://csswizardry.com/2026/04/font-family-doesnt-fall-back-the-way-you-think/#why-this-matters"&gt;Why This Matters&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://csswizardry.com/2026/04/font-family-doesnt-fall-back-the-way-you-think/#closing-thoughts"&gt;Closing Thoughts&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;&lt;/details&gt;&lt;p&gt;There is a small but surprisingly important nuance in the way &lt;code&gt;font-family&lt;/code&gt;
works that seems to catch a lot of people out. In my continuing series on &lt;a href="https://csswizardry.com/2026/04/what-is-css-containment-and-how-can-i-use-it/"&gt;web
performance&lt;/a&gt; for &lt;a href="https://csswizardry.com/2026/03/when-all-you-can-do-is-all-or-nothing-do-nothing/"&gt;design
systems&lt;/a&gt;, today
we’ll look at font stacks and how, when improperly configured, they can cause
unsightly flashes of inappropriate or unexpected fallback text, and in more
extreme cases, layout shifts.&lt;/p&gt;

&lt;p&gt;Correctly, developers for the most part know that &lt;code&gt;font-family&lt;/code&gt; is an inherited
property: set a font family on the &lt;code&gt;:root&lt;/code&gt;/&lt;code&gt;html&lt;/code&gt;/&lt;code&gt;body&lt;/code&gt; and, unless told
otherwise, descendants will inherit that font:&lt;/p&gt;

&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;body&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;font-family&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;system-ui&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;sans-serif&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;So far, so good!&lt;/p&gt;

&lt;p&gt;The confusion tends to arrive when we introduce a web or custom font on a child
element, e.g.:&lt;/p&gt;

&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;h1&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;font-family&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"Open Sans"&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;At a glance, this can feel perfectly sensible. The page should use &lt;code&gt;system-ui,
sans-serif&lt;/code&gt;; the heading uses &lt;code&gt;"Open Sans"&lt;/code&gt;; and while the web font is loading,
the browser will presumably just fall back to the parent’s stack—&lt;code&gt;system-ui,
sans-serif&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Unfortunately, that isn’t the case.&lt;/p&gt;

&lt;h2 id="font-family-fallbacks-are-self-contained"&gt;&lt;code&gt;font-family&lt;/code&gt; Fallbacks Are Self-Contained&lt;/h2&gt;

&lt;p&gt;Once you declare a &lt;code&gt;font-family&lt;/code&gt; on an element, that declaration stands on its
own. The element does not say: &lt;q&gt;I would like &lt;code&gt;"Open Sans"&lt;/code&gt;, and if that is
unavailable right now, please work your way back up the DOM and inherit whatever
fallbacks the nearest ancestor might have.&lt;/q&gt;&lt;/p&gt;

&lt;p&gt;Instead, it says: &lt;q&gt;My &lt;code&gt;font-family&lt;/code&gt; is &lt;code&gt;"Open Sans"&lt;/code&gt;.&lt;/q&gt; And that’s all it
says.&lt;/p&gt;

&lt;p&gt;And if the browser does not yet have &lt;code&gt;"Open Sans"&lt;/code&gt; available (yet), it resolves
fallback from &lt;em&gt;that declaration&lt;/em&gt;, not from the parent’s.&lt;/p&gt;

&lt;p&gt;Put another way:&lt;/p&gt;

&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;h1&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;font-family&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"Open Sans"&lt;/span&gt;&lt;span&gt;;&lt;/span&gt; &lt;span&gt;/* « The fallback happens inside here… */&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;/**
 * …not here.
 */&lt;/span&gt;
&lt;span&gt;body&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;font-family&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;system-ui&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;sans-serif&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id="why-you-get-a-flash-of-times"&gt;Why You Get a Flash of Times&lt;/h2&gt;

&lt;p&gt;If the current element’s &lt;code&gt;font-family&lt;/code&gt; declaration contains only one value, and
that value is not currently available, the browser falls back to its default
&lt;strong&gt;for that element&lt;/strong&gt;, and not to an inheritable &lt;code&gt;font-family&lt;/code&gt; from somewhere
higher up. For most browsers in their default state, that fallback is likely
&lt;em&gt;Times&lt;/em&gt; or &lt;em&gt;Times New Roman&lt;/em&gt;. That is why you so often see a brief flash of
Times New Roman where you were expecting something much more sympathetic or
appropriate.&lt;/p&gt;

&lt;p&gt;The browser is not &lt;em&gt;forgetting&lt;/em&gt; the parent’s font stack; it’s obeying the
child’s declaration exactly as written, then exhausting the options available in
that declaration, and then falling back to the browser default.&lt;/p&gt;

&lt;h2 id="the-fix-is-simple"&gt;The Fix Is Simple&lt;/h2&gt;

&lt;p&gt;Whenever you specify a &lt;code&gt;font-family&lt;/code&gt;, specify a &lt;strong&gt;complete stack&lt;/strong&gt;. I’m looking
at a client’s site right now and I can see this right at the very top of their
CSS:&lt;/p&gt;

&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;:root&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;--hero-hero&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"Clan Pro"&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;--heading-x-large&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"Clan Pro"&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;--heading-large&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"Clan Pro"&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;--heading-medium&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"Clan Pro"&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;--heading-medium-subtle&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"Clan Pro"&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;--heading-small&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"Clan Pro"&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;--heading-small-subtle&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"Clan Pro"&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;--heading-x-small&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"Clan Pro"&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;--paragraph-title-x-large&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"Bernina Sans"&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;--paragraph-title-large&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"Bernina Sans"&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;--paragraph-title-medium&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"Bernina Sans"&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;--paragraph-title-small&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"Bernina Sans"&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;--paragraph-title-x-small&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"Bernina Sans"&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;--paragraph-body-x-large&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"Bernina Sans"&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;--paragraph-body-large&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"Bernina Sans"&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;--paragraph-body-medium&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"Bernina Sans"&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;--paragraph-body-small&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"Bernina Sans"&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;--paragraph-body-x-small&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"Bernina Sans"&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;--label-3-x-large&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"Clan Pro"&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;--label-2-x-large&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"Clan Pro"&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;--label-x-large&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"Clan Pro"&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;--label-large&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"Clan Pro"&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;--label-medium&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"Clan Pro"&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;--label-small&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"Clan Pro"&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;--label-x-small&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"Clan Pro"&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;At the very least, all of these simply need to read:&lt;/p&gt;

&lt;div&gt;&lt;div&gt;&lt;pre&gt;&lt;code&gt;&lt;span&gt;:root&lt;/span&gt; &lt;span&gt;{&lt;/span&gt;
  &lt;span&gt;--hero-hero&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"Clan Pro"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;sans-serif&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;--heading-x-large&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"Clan Pro"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;sans-serif&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;--heading-large&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"Clan Pro"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;sans-serif&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;--heading-medium&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"Clan Pro"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;sans-serif&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;--heading-medium-subtle&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"Clan Pro"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;sans-serif&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;--heading-small&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"Clan Pro"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;sans-serif&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;--heading-small-subtle&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"Clan Pro"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;sans-serif&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;--heading-x-small&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"Clan Pro"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;sans-serif&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;--paragraph-title-x-large&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"Bernina Sans"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;sans-serif&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;--paragraph-title-large&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"Bernina Sans"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;sans-serif&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;--paragraph-title-medium&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"Bernina Sans"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;sans-serif&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;--paragraph-title-small&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"Bernina Sans"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;sans-serif&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;--paragraph-title-x-small&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"Bernina Sans"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;sans-serif&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;--paragraph-body-x-large&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"Bernina Sans"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;sans-serif&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;--paragraph-body-large&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"Bernina Sans"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;sans-serif&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;--paragraph-body-medium&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"Bernina Sans"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;sans-serif&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;--paragraph-body-small&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"Bernina Sans"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;sans-serif&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;--paragraph-body-x-small&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"Bernina Sans"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;sans-serif&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;--label-3-x-large&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"Clan Pro"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;sans-serif&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;--label-2-x-large&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"Clan Pro"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;sans-serif&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;--label-x-large&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"Clan Pro"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;sans-serif&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;--label-large&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"Clan Pro"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;sans-serif&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;--label-medium&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"Clan Pro"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;sans-serif&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;--label-small&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"Clan Pro"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;sans-serif&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
  &lt;span&gt;--label-x-small&lt;/span&gt;&lt;span&gt;:&lt;/span&gt; &lt;span&gt;"Clan Pro"&lt;/span&gt;&lt;span&gt;,&lt;/span&gt; &lt;span&gt;sans-serif&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Remember, any time you declare a &lt;code&gt;font-family&lt;/code&gt;, declare the whole thing. Even if
that is just a broad
&lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/generic-family"&gt;&lt;code&gt;&amp;lt;generic-family&amp;gt;&lt;/code&gt;&lt;/a&gt;
And while this is the bare minimum, at least sans-serif web fonts will actually fall
back to sans.&lt;/p&gt;

&lt;p&gt;To do a much more thorough job, you can simply &lt;a href="https://csswizardry.com/contact/"&gt;hire me&lt;/a&gt; to run my
&lt;cite&gt;Web Performance for Design Systems&lt;/cite&gt; workshop.&lt;/p&gt;

&lt;h2 id="why-this-matters"&gt;Why This Matters&lt;/h2&gt;

&lt;p&gt;At its most simple, this is a trivial visual issue: a nascent sans heading
briefly rendered in serif just looks wrong.&lt;/p&gt;

&lt;p&gt;At the other end of the spectrum, it can have real knock-on effects on Core Web
Vitals: if the fallback face is excessively different in width, height, or
overall proportions, the eventual swap to the web font can have an impact on
your CLS scores.&lt;/p&gt;

&lt;h2 id="closing-thoughts"&gt;Closing Thoughts&lt;/h2&gt;

&lt;p&gt;If a &lt;code&gt;font-family&lt;/code&gt; matters enough to override, it matters enough to define
properly. This is one of those small details that feels too small to matter
right up until you notice it everywhere.&lt;/p&gt;

&lt;p&gt;My client has had complaints of noticeable layout shifts while migrating to
a new design system, and at the size and scale they’re working at, they were
really, really struggling to pin it down. It only took me a few minutes because
&lt;em&gt;it’s easy when you know the answer&lt;/em&gt;. That’s exactly why you &lt;a href="https://csswizardry.com/consultancy/"&gt;hire
consultants&lt;/a&gt;.&lt;/p&gt;


      &lt;/div&gt;

        

        

        

        

        &lt;/div&gt;&lt;/div&gt;&lt;/body&gt;&lt;/html&gt;</content>
        <author>
            <name>Harry Roberts</name>
        </author>
        <media:content medium="image" url="https://cdn.requestmetrics.com/agent/current/rm.js"/>
        <source>
            <id>tag:feedly.com,2013:cloud/feed/https://csswizardry.com/feed.xml</id>
            <title type="html">csswizardry.com</title>
            <link href="https://csswizardry.com" rel="alternate" type="text/html"/>
            <updated>2026-04-16T10:53:17Z</updated>
        </source>
    </entry>
    <entry>
        <id>tag:feedly.com,2013:cloud/entry/cOXRjoksGIQzU1pdqbdybn+OTWYeI0riptNneGckdsI=_19d7d22e1ed:2f12e0:29a1775c</id>
        <title type="html">Under the hood of MDN's new frontend</title>
        <published>2026-04-11T15:22:11Z</published>
        <updated>2026-04-11T15:22:17Z</updated>
        <link href="https://developer.mozilla.org/en-US/blog/mdn-front-end-deep-dive/" rel="alternate" type="text/html"/>
        <content type="html">&lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"&gt;
&lt;html&gt;&lt;body&gt;&lt;div&gt;&lt;div&gt;
            
    &lt;section&gt;&lt;p&gt;Last year, we &lt;a href="https://developer.mozilla.org/en-US/blog/launching-new-front-end/"&gt;launched a new frontend for MDN&lt;/a&gt;.
The most noticeable changes were adjustments to our styles; we simplified and unified the MDN design across all of our pages.
In truth, the biggest changes were not reader-visible, but rather in the overhauled code that powers our frontend.
This post describes what we've done, the technologies we chose, and why we did it at all.&lt;/p&gt;
  &lt;/section&gt;&lt;section&gt;&lt;p&gt;To fully understand the changes we made to MDN's frontend, I should provide some context on how MDN content is assembled into the website you all know and love.
MDN's architecture is probably worthy of its own blog post, but to simplify for the sake of this post, pages are published to the site via these major steps:&lt;/p&gt;
&lt;ol&gt;&lt;li&gt;The documentation is written and maintained in Markdown, across a couple of git repositories, by our fantastic team of technical writers, partners, and invited experts, alongside our enormous community of contributors and translators.&lt;/li&gt;
&lt;li&gt;A build tool ingests these Markdown files, converts them into HTML, and saves them as a set of JSON files with supplemental metadata about each page.&lt;/li&gt;
&lt;li&gt;Our frontend traverses these JSON files and compiles fully-featured pages, complete with browser compatibility tables, l10n support, navigation menus, and so on - in a step we name (or perhaps misname) &lt;a href="https://developer.mozilla.org/en-US/docs/Glossary/SSR"&gt;server-side rendering&lt;/a&gt; (SSR).&lt;/li&gt;
&lt;li&gt;At this point, the resulting HTML, CSS, and JavaScript files are uploaded to cloud buckets and delivered to our readers globally.&lt;/li&gt;
&lt;/ol&gt;&lt;p&gt;&lt;img src="https://developer.mozilla.org/en-US/blog/mdn-front-end-deep-dive/architecture.svg" alt="A flow diagram showing MDN's build pipeline in four steps: 1. Markdown from content and translated-content repositories, written by writers, partners, and community; 2. A build tool converting Markdown to HTML and JSON metadata; 3. Frontend SSR compiling JSON into full pages with compat tables, l10n, navigation, Web Components, and Server Components; 4. Cloud delivery of HTML, CSS, and JS via CDN to readers globally."&gt;&lt;/p&gt;
  &lt;/section&gt;&lt;section&gt;&lt;p&gt;Rebuilding our frontend had been a long time coming because of how tricky it was to work on MDN's UI.
Our previous frontend (called &lt;strong&gt;yari&lt;/strong&gt;) was a React app that, unfortunately, had accumulated quite a lot of technical debt.
Maintenance wasn't exactly impossible, but was certainly painful to undertake.
Whenever we fixed issues or added new site functionality, we inevitably ended up piling on more technical debt.
But how did we get there?&lt;/p&gt;
&lt;p&gt;The React app had started life as a "Create React App", but a number of the built-in defaults didn't work for us.
Of course, this led to a series of workarounds, and we eventually had to &lt;a href="https://create-react-app.dev/docs/available-scripts/#npm-run-eject" target="_blank"&gt;"eject" the configuration&lt;/a&gt;. We ended up with an extremely complicated Webpack config as well as some very hacky build scripts.&lt;/p&gt;
&lt;p&gt;On the CSS side as well, things were starting to get out of control.
We used &lt;a href="https://sass-lang.com/" target="_blank"&gt;Sass&lt;/a&gt; extensively, then added modern CSS features like CSS variables, which meant we had a bizarre mix of both idioms spread across our files.&lt;/p&gt;
&lt;p&gt;The CSS was also incredibly entangled, with poor or nonexistent scoping.
When we made a change in one UI component, we'd frequently spot unintended changes in others.
These issues, and a lack of build tools to split up the CSS, meant we had to ship a large render-blocking CSS blob to our users, complete with styles for components they might never load.&lt;/p&gt;
&lt;p&gt;But by far, the biggest issue was that our React app was merely a wrapper around our static content.
To make the React app aware of the HTML content that our build tool generated would have required expensive reparsing of the HTML and an extraordinary amount of logic which we'd have to ship to users in our client-side JavaScript.
We didn't want to do this, so the React app boundary essentially ended where our documentation began – we used React's &lt;code&gt;dangerouslySetInnerHTML&lt;/code&gt; to insert the content.&lt;/p&gt;
&lt;p&gt;Our content is &lt;em&gt;mostly&lt;/em&gt; static (prose and code examples), but there were a number of places within this static content where we needed to add interactivity (think things like the "Copy" button on code blocks).
For these interactive parts, we ended up using regular DOM APIs, which wasn't very elegant, particularly when the rest of the site was written in React.
We couldn't use &lt;a href="https://react.dev/learn/writing-markup-with-jsx" target="_blank"&gt;JSX&lt;/a&gt; (React's HTML-like syntax), which limited the maintainability of more complex pieces of interactivity, and we occasionally faced the worst-case scenario of maintaining duplicate implementations - one using React and another using DOM APIs.&lt;/p&gt;
  &lt;/section&gt;&lt;section&gt;&lt;p&gt;As a possible solution to this problem, in 2024, we started experimenting with &lt;a href="https://lit.dev/" target="_blank"&gt;Lit&lt;/a&gt; and &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_components"&gt;web components&lt;/a&gt; to see whether they could improve the developer experience when working on this kind of interactivity within content.
Our first proper prototype, and eventual production implementation, came out of our work on the &lt;a href="https://developer.mozilla.org/en-US/curriculum/"&gt;MDN Curriculum&lt;/a&gt; when we partnered with &lt;a href="https://scrimba.com" target="_blank"&gt;Scrimba&lt;/a&gt;.&lt;/p&gt;
  &lt;/section&gt;&lt;section&gt;&lt;p&gt;Scrimba has a feature called "Scrims" - an interactive learning environment we embed on MDN via an &lt;code&gt;&amp;lt;iframe&amp;gt;&lt;/code&gt;. Scrims let learners watch a short coding tutorial and then edit the code themselves, all within the same view — think of them as interactive screencasts.&lt;/p&gt;
&lt;p&gt;On our pages, we didn't want to send any user data to Scrimba until a user chose to interact with their content; so we didn't load the &lt;code&gt;&amp;lt;iframe&amp;gt;&lt;/code&gt; until after a user clicks to open it.
We also wanted to be able to expand the Scrim to fullscreen, without a user leaving MDN, so we used the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/dialog"&gt;&lt;code&gt;&amp;lt;dialog&amp;gt;&lt;/code&gt;&lt;/a&gt; element.&lt;/p&gt;
&lt;p&gt;We figured that building a web component would allow us to use a custom element to insert these Scrims directly into our content, thereby skipping a number of rendering steps and avoiding the tricky-to-maintain DOM API implementation.&lt;/p&gt;
&lt;p&gt;Our component starts life by extending &lt;code&gt;LitElement&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;Within that, we need to define some state.
In Lit, we can do this through a static properties attribute:&lt;/p&gt;

&lt;p&gt;And we set defaults in our class constructor:&lt;/p&gt;

&lt;p&gt;We want to manipulate the URL which has been provided as an attribute to our custom element.
Lit provides us with &lt;a href="https://lit.dev/docs/components/lifecycle/" target="_blank"&gt;lifecycle methods&lt;/a&gt; to do this. We want to compute a value once we know an update to the component will be rendered:&lt;/p&gt;

&lt;p&gt;We can then use this state to render our component:&lt;/p&gt;

&lt;p&gt;Lit's &lt;code&gt;html&lt;/code&gt; template literal is just as convenient as JSX in allowing us to write HTML-ish syntax in JavaScript.
The huge advantage over JSX is it doesn't require any compilation to use: it's native JavaScript.&lt;/p&gt;
&lt;p&gt;I say "HTML-ish" because you'll notice a few annotations before certain attributes in the template above, namely &lt;code&gt;@close&lt;/code&gt; and &lt;code&gt;@click&lt;/code&gt;.
This is Lit syntax that allows us to bind event listeners to these elements: the close event on the &lt;code&gt;&amp;lt;dialog&amp;gt;&lt;/code&gt; and click events on a couple of buttons.
We define these in the class too:&lt;/p&gt;

&lt;p&gt;When a user clicks to open the Scrim, the &lt;code&gt;#open&lt;/code&gt; method fires, which updates the values of &lt;code&gt;_scrimLoaded&lt;/code&gt; and &lt;code&gt;_fullscreen&lt;/code&gt;.
Lit notices the changes to these properties, because we'd defined them in &lt;code&gt;static properties&lt;/code&gt;, and automatically re-renders the component, loading the &lt;code&gt;&amp;lt;iframe&amp;gt;&lt;/code&gt; and the Scrim inside.&lt;/p&gt;
&lt;p&gt;I've simplified the component a little for brevity, you can see the &lt;a href="https://github.com/mdn/fred/blob/main/components/scrim-inline/element.js" target="_blank"&gt;&lt;code&gt;MDNScrimInline&lt;/code&gt; source on GitHub&lt;/a&gt;.
There's a number of additions in there like telemetry, and a dynamically-rendered thumbnail (it was fewer bytes and a simpler implementation than pre-rendering a bunch of images).
As you can imagine, this was very straightforward to develop, thanks to the convenience functions we get with Lit; this would've been a massive headache to implement directly with traditional DOM APIs.&lt;/p&gt;
&lt;p&gt;In many ways, I found that the implementation in Lit was simpler than in React: you may notice the state we're dealing with isn't particularly complex, and doesn't require a complex component architecture to reflect it.
But more importantly, it gives us that custom element we can insert into our curriculum content wherever we need to add a Scrim:&lt;/p&gt;

  &lt;/section&gt;&lt;section&gt;&lt;p&gt;The Scrimba implementation was a good introduction for the team to writing a small component, but what about something more complex?
Interactive examples are the components that appear under "Try it" sections at the top of many CSS, JavaScript, and HTML pages.&lt;/p&gt;
&lt;p&gt;Improving the infrastructure for these had been on the engineering team backlog for a while; they were difficult for our technical writers and community to maintain and author.
The existing implementation was split across four git repositories, and authoring or debugging an example could require synchronizing changes across them all.
Worse still, examples had to be written in isolation from the content they'd be included in, so it wasn't possible to show a live preview containing both changes to an example and the content of the MDN page it would be included on.&lt;/p&gt;
&lt;p&gt;This complexity came about for good reasons: these interactive examples were too complex to easily engineer and maintain with DOM APIs directly.
Instead, we had a separate build system and examples repository which rendered these examples into separate HTML pages which we could load in an &lt;code&gt;&amp;lt;iframe&amp;gt;&lt;/code&gt; directly.&lt;/p&gt;
&lt;p&gt;We wanted to simplify this architecture, to make writing interactive examples easier for our authors, so again: we reached for Lit to build a web component we could include directly in our content.
This was a much more technically-complex implementation than Scrims.
Firstly, we needed a number of templates for the different ways interactive examples are displayed:&lt;/p&gt;
&lt;ol&gt;&lt;li&gt;A code editor and console for JavaScript examples, with &lt;a href="https://developer.mozilla.org/en-US/docs/WebAssembly/Reference/Memory/load"&gt;the addition of tabs for WASM&lt;/a&gt; examples.&lt;/li&gt;
&lt;li&gt;A tabbed code editor with rendered output for HTML examples (usually &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/p#try_it"&gt;tabs for HTML and CSS&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;A table of code editors that can be selected with rendered output for CSS examples (see the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/background-clip#try_it"&gt;CSS &lt;code&gt;background-clip&lt;/code&gt; property&lt;/a&gt;, for example).&lt;/li&gt;
&lt;/ol&gt;&lt;p&gt;Secondly, we needed a way to render the examples, and the edits users made to them.
We had already written that logic to create our interactive &lt;a href="https://developer.mozilla.org/en-US/blog/introducing-the-mdn-playground/"&gt;Playground&lt;/a&gt;, but it was in React: so we needed to port that too.&lt;/p&gt;
&lt;p&gt;So we set about doing all that.
What made things a lot simpler was that &lt;a href="https://lit.dev/docs/frameworks/react/" target="_blank"&gt;Lit's React integration&lt;/a&gt; allowed us to render these web components in our existing React app.
So we could port the elements of the Playground we needed piece-by-piece to web components, without having to port the entire thing all at once, and without having to maintain dual implementations.&lt;/p&gt;
&lt;p&gt;At a high level, we split our single entangled Playground React component into a series of custom elements:&lt;/p&gt;
&lt;ul&gt;&lt;li&gt;&lt;code&gt;&amp;lt;play-editor&amp;gt;&lt;/code&gt;: A &lt;a href="https://codemirror.net/" target="_blank"&gt;CodeMirror&lt;/a&gt;-powered editor.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;lt;play-console&amp;gt;&lt;/code&gt;: An element to format and render console messages.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;lt;play-runner&amp;gt;&lt;/code&gt;: An element responsible for rendering the current state of each editor.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;lt;play-controller&amp;gt;&lt;/code&gt;: An element responsible for passing events and state between each of the above elements.&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;This made the logic in the Playground simpler and decoupled, which allowed us to reuse these elements in the &lt;code&gt;&amp;lt;interactive-example&amp;gt;&lt;/code&gt; element we created.
This included logic to search the page for &lt;code&gt;&amp;lt;code&amp;gt;&lt;/code&gt; elements the interactive example component should ingest, sending their contents to a &lt;code&gt;&amp;lt;play-controller&amp;gt;&lt;/code&gt;, and working out which of the above templates it needed to render: using some combination of the &lt;code&gt;&amp;lt;play-*&amp;gt;&lt;/code&gt; elements to do this.&lt;/p&gt;
&lt;p&gt;This meant that authors could now add a macro to content - which renders our &lt;code&gt;&amp;lt;interactive-example&amp;gt;&lt;/code&gt; custom element behind the scenes - followed by the code blocks the example should use:&lt;/p&gt;

&lt;p&gt;You can see the full source for this example on the &lt;a href="https://github.com/mdn/content/blob/616b1da6696a833451891ad8c767ff15474b08f7/files/en-us/web/css/background-repeat/index.md?plain=1#L11-L50" target="_blank"&gt;CSS background-repeat page GitHub&lt;/a&gt;.
We're not quite sure where we stand on putting custom elements directly in our non-curriculum Markdown content, which could make our architecture even simpler; that's a discussion for another day.&lt;/p&gt;
  &lt;/section&gt;&lt;section&gt;&lt;p&gt;So this is all well and good, web components seem pretty cool and solve some of our problems around adding interactivity within our static content. But wasn't this blog post supposed to be about how we rewrote our entire frontend stack: what happened to that? To answer that, let's get into another problem we had with our old frontend:&lt;/p&gt;
  &lt;/section&gt;&lt;section&gt;&lt;p&gt;I've mentioned our problem with our React app being a "wrapper" and not being able to interact with our content.
The fundamental problem is that (at least classically) React apps are Single Page Applications (SPAs), which you figure out how to render on the server, then attempt to figure out how to avoid shipping an absolutely enormous JavaScript bundle to your users.&lt;/p&gt;
&lt;p&gt;That last part is necessary. That's because everything you render in an SPA, even if it could be rendered statically on a server or in a compilation step, has to be shipped in your client-side JavaScript bundle and re-rendered in the client – just to verify nothing has changed.
The React documentation itself summarises it better than I could:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;This pattern means users need to download and parse an additional 75K (gzipped) of libraries, and wait for a second request to fetch the data after the page loads, just to render static content that will not change for the lifetime of the page. &lt;br&gt;&lt;a href="https://react.dev/reference/rsc/server-components" target="_blank"&gt;Server Components&lt;/a&gt; on react.dev&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That's a quote from the documentation for React Server Components (RSC): so the project recognizes that this is a problem, and is working hard on solving it.
Unfortunately, using RSC effectively requires using a framework that we aren't already using.
Migrating to that would require rewriting a lot of the frontend anyway.&lt;/p&gt;
&lt;p&gt;So since a large rewrite was required to address this fundamental issue, we could also re-evaluate what kind of a site MDN is, and how complex it needed to be.
Really, MDN isn't a particularly complex site, at least from a "things that require interactivity" standpoint.
The vast majority of content on an MDN documentation page is HTML and CSS: we don't need a complex app powering the majority of the site.
We essentially have islands of interactivity, which could easily all be implemented as web components.&lt;/p&gt;
&lt;p&gt;And if we're implementing all our functionality in isolated web components, it doesn't really matter how they're assembled: we just need to template HTML together, and that can happen multiple times, in multiple places in our overall build system.
There's no higher-level "app" that needs to understand the state of the entire page, so there never could be a "wrapper" problem – our markdown to HTML build tool is just as first class a citizen as whatever templating we need to do in our frontend.&lt;/p&gt;
&lt;p&gt;This approach solved all three problems at once: there's no SPA shipping redundant JavaScript to re-render static content, there's no "wrapper" that can't reach into our documentation HTML, and each piece of interactivity is a self-contained web component that only loads when it's needed. What remained was deciding how to do the static templating that assembles everything else.&lt;/p&gt;
  &lt;/section&gt;&lt;section&gt;&lt;p&gt;We considered using a dedicated templating language, such as EJS, to do templating in our frontend, but realized there's a lot of benefits to a component-based architecture.
While doing static HTML templating on the server avoids shipping logic to users in a client-side JavaScript bundle, this HTML still requires styling.
And as you may recall, the CSS in our old frontend was a mess, and we wanted to avoid shipping unnecessary CSS if it wasn't necessary to render the current page.&lt;/p&gt;
&lt;p&gt;We first built our own concept of Server Components, using Lit's HTML template literal, which we were already comfortable with.
Here's an example of our top navigation bar component:&lt;/p&gt;

&lt;p&gt;You'll see the logic is handled in a &lt;code&gt;render&lt;/code&gt; method, much like it would be in a Lit component.
We don't need any lifecycle methods because this only ever runs once.
This component can render other server components like &lt;code&gt;Logo&lt;/code&gt; and &lt;code&gt;Menu&lt;/code&gt;, as well as web components like &lt;code&gt;&amp;lt;mdn-search-button&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;mdn-search-modal&amp;gt;&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;We render this to HTML in NodeJS, using a convenient function Lit provides.
This also renders these Lit web components to &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_shadow_DOM"&gt;Declarative Shadow DOM&lt;/a&gt; so, in compatible browsers, the Shadow DOM and CSS of our custom elements is rendered before the JavaScript gets loaded.&lt;/p&gt;
  &lt;/section&gt;&lt;section&gt;&lt;p&gt;As I mentioned before, one big problem with an SPA-approach to building websites is everything necessary to render the page needs to be included in the client-side JavaScript bundle.
Another similar problem is that it's very easy, and therefore very common, to ship a whole load of code &lt;em&gt;not&lt;/em&gt; necessary for rendering the current page, and only necessary for rendering other pages, in one huge client-side bundle.
We fell into this trap with our old frontend, both with our JavaScript and our CSS.&lt;/p&gt;
&lt;p&gt;Over time we did partition off certain routes into separate chunks, but this was only possible with our JavaScript. Our CSS was too entangled to do this, and our build tool wasn't configured to do it either.&lt;/p&gt;
&lt;p&gt;And, it was only possible to do this &lt;em&gt;per route&lt;/em&gt;, not for components within the same page. If there was a chance a certain route might load a component, it needed to be included in the bundle if it was to be server-side rendered, even if it wasn't, and that JavaScript was never executed client side.&lt;/p&gt;
&lt;p&gt;We wanted to avoid all this in our new frontend: only loading the most minimal CSS and JavaScript bundles required to render the page, and making it interactive; and I wanted to achieve this on an architectural level, so it was nearly impossible to not do. We achieved this in a few ways, but the key to unlocking them all was a flat name-based component structure.&lt;/p&gt;
&lt;p&gt;Every component lives in a flat hierarchy under the &lt;code&gt;./components/&lt;/code&gt; directory, with the following file names reserved for certain pieces of the component:&lt;/p&gt;
&lt;pre&gt;components/example-component
├── element.css
├── element.js
├── global.css
├── server.css
└── server.js
&lt;/pre&gt;
&lt;ul&gt;&lt;li&gt;&lt;code&gt;element.js&lt;/code&gt; - A web component, exporting a &lt;code&gt;MDNExampleComponent&lt;/code&gt; class and defining a &lt;code&gt;&amp;lt;mdn-example-component&amp;gt;&lt;/code&gt; element.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;server.js&lt;/code&gt; - A server component, extending &lt;code&gt;ServerComponent&lt;/code&gt; from &lt;a href="https://github.com/mdn/fred/blob/main/components/server/index.js" target="_blank"&gt;&lt;code&gt;components/server/index.js&lt;/code&gt;&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;server.css&lt;/code&gt; - CSS for a server component that will be automatically loaded for this component from the server.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;global.css&lt;/code&gt; - CSS for the component that gets loaded everywhere all the time.&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;We enforce parts of this via linting, and some of this by throwing errors if you don't adhere to the naming requirements.&lt;/p&gt;
  &lt;/section&gt;&lt;section&gt;&lt;p&gt;Because we know where each web component lives based on name, we can do some clever things.
When our page loads, we run logic like this client-side:&lt;/p&gt;

&lt;p&gt;This results in lazy-loading every custom element present in the DOM at load time, entirely async and in parallel.
The advantages here are numerous:&lt;/p&gt;
&lt;ul&gt;&lt;li&gt;Engineers don't have to remember to import web components in server components; they can use them as if they are normal HTML elements.&lt;/li&gt;
&lt;li&gt;We can add custom elements to our content markdown (either directly or through a macro), without having to wire up an export elsewhere.&lt;/li&gt;
&lt;li&gt;We only ever load the JavaScript for each web component if it's present on the page, automatically.
It's not necessary for engineers to think about whether their new component increases the bundle size - if it's not present on the page the user is viewing, that code won't end up being loaded by the users' browser.&lt;/li&gt;
&lt;li&gt;Changes to one component should only have minimal impact on the rest of the bundle: a bugfix in one component will require that component's JavaScript to be reloaded, but other components should be cached by the browser and will become interactive almost immediately, as they load in parallel asynchronously.&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;We also automatically load every web component in our SSR bundle, where Lit renders them into a Declarative Shadow DOM (unless the component opts out because it doesn't make sense to render them on the server).
This helps ensure we don't have layout shifts when the JavaScript loads.
The result is that the slight delay to interactivity when we load its JavaScript after initial render is imperceptible because the component is already on the page, just not interactive yet.&lt;/p&gt;
&lt;p&gt;At least, not entirely interactive: this architecture is flexible enough for us to be very clever with certain components.
One of these is &lt;code&gt;&amp;lt;mdn-dropdown&amp;gt;&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;This is a component which, as the name might suggest, implements a dropdown.
The render method is rather simple:&lt;/p&gt;

&lt;p&gt;We render two slots, which can be used like so:&lt;/p&gt;

&lt;p&gt;Since we use slots here, &lt;code&gt;&amp;lt;mdn-dropdown&amp;gt;&lt;/code&gt;'s shadow DOM is almost irrelevant, and any children of it can be styled entirely as normal: the element only adds interactivity.
It &lt;em&gt;does&lt;/em&gt; have some styles attached, but those aren't for styling: they're for interactivity too.
See, we also have a lifecycle method:&lt;/p&gt;

&lt;p&gt;And define our loaded property like so:&lt;/p&gt;

&lt;p&gt;If you trace the logic through, you'll see that the dropdown slot isn't hidden by default, and therefore, is visible when we render to DSD on the server.
And once the component is &lt;code&gt;loaded=true&lt;/code&gt;, we reflect that attribute into the DOM, so it appears like:&lt;/p&gt;

&lt;p&gt;The reason we want this is because we also attach the following CSS to the element:&lt;/p&gt;

&lt;p&gt;The logic here is a little hard to parse - and I say that as the person who wrote it - but it effectively translates to:&lt;/p&gt;
&lt;ul&gt;&lt;li&gt;If JavaScript for the &lt;code&gt;mdn-dropdown&lt;/code&gt; has loaded, do no styling.&lt;/li&gt;
&lt;li&gt;If the JavaScript for the &lt;code&gt;mdn-dropdown&lt;/code&gt; hasn't loaded, then:
&lt;ul&gt;&lt;li&gt;If the focus is outside the element, hide the dropdown slot.&lt;/li&gt;
&lt;li&gt;If the focus is within the element, show the dropdown slot.
And what does this mean? Well, we have a CSS native dropdown as soon as the page is rendered, which progressively enhances to a JavaScript dropdown, once that code has loaded; other engineers need not know any of this is going on, just that the dropdown component works.&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;This is hugely important in our top navigation menu, where most of our links are behind dropdowns: those are completely usable as soon as they're rendered to the page.
In other components, such as in our theme switcher, while choosing between themes isn't possible until its JavaScript loads, the dropdown being interactive already gives us a few seconds longer load time for that JavaScript before the user clicks on anything requiring it.&lt;/p&gt;
&lt;h4 id="what_if_we_dont_have_dsd"&gt;What if we don't have DSD&lt;/h4&gt;
&lt;p&gt;Now, Declarative Shadow DOM isn't widely available yet, so we have to ensure things also work on slightly older browsers.
This is where the &lt;code&gt;global.css&lt;/code&gt; file comes in: any CSS written in one of these is included on all pages, all the time.&lt;/p&gt;
&lt;p&gt;This is obviously necessary for setting things like global CSS variables, global reset styles, and the like.
But for components, when DSD isn't available, before its JavaScript has loaded, they'll appear to the browser as an empty inline element with no styling attached.
This isn't always optimal, and can cause layout shifts when the JavaScript gets loaded, so we set global styles for certain elements, for example, for our button component:&lt;/p&gt;

&lt;p&gt;This is just enough to ensure buttons don't shift the layout before being loaded.
There's a small optimisation to be had by only loading this style when an &lt;code&gt;mdn-button&lt;/code&gt; is present on the page, rather than on every page: but it's so minimal it's probably not worth the added complexity.
It's also important for our aforementioned dropdown component: we still want this to be interactive if DSD hasn't loaded, so we include a similar style in a &lt;code&gt;global.css&lt;/code&gt; file.&lt;/p&gt;
  &lt;/section&gt;&lt;section&gt;&lt;p&gt;For server components, the considerations are a little different. The JavaScript &lt;em&gt;itself&lt;/em&gt; doesn't need to be lazy loaded or cut down, since it's only being used to SSR our HTML. But what does require careful loading is the CSS used in each server component. We only want to load this if the component gets rendered to the page. But how do we know this?&lt;/p&gt;
&lt;p&gt;Our server components extend our &lt;code&gt;ServerComponent&lt;/code&gt; class, so we place some tracking logic in its static render method, which runs before and after instantiating each server component:&lt;/p&gt;

&lt;p&gt;This gives us a &lt;code&gt;Set&lt;/code&gt;, &lt;code&gt;componentsUsed&lt;/code&gt;, which only contains the components that rendered anything. We then use this in our &lt;code&gt;OuterLayout&lt;/code&gt; component:&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;compilationStats&lt;/code&gt; object here comes from our build tool, Rspack (more on that later), and this code block gives us a list of &lt;code&gt;&amp;lt;link&amp;gt;&lt;/code&gt; tags to include in our &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt; containing only the CSS that's necessary to render the page. We also load a CSS file with all the &lt;code&gt;global.css&lt;/code&gt; files mentioned before, bundled into one.&lt;/p&gt;
&lt;p&gt;Again, this is a simplified version of our &lt;a href="https://github.com/mdn/fred/blob/main/components/server/index.js" target="_blank"&gt;&lt;code&gt;ServerComponent&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://github.com/mdn/fred/blob/main/components/outer-layout/server.js" target="_blank"&gt;&lt;code&gt;OuterLayout&lt;/code&gt;&lt;/a&gt; classes, which you can see in their entirety in our repository if you're interested.&lt;/p&gt;
  &lt;/section&gt;&lt;section&gt;&lt;p&gt;You'll note that what I've suggested here results in a number of quite small CSS and JavaScript files being loaded on the page. This goes against the classical wisdom that there's an optimal bundle size where you combine multiple assets into one to reduce the number of round trips a browser needs to make to load them all.&lt;/p&gt;
&lt;p&gt;I can't claim to be a performance expert here, but &lt;a href="https://developer.mozilla.org/en-US/docs/Glossary/HTTP_2"&gt;HTTP/2&lt;/a&gt; and &lt;a href="https://developer.mozilla.org/en-US/docs/Glossary/HTTP_3"&gt;HTTP/3&lt;/a&gt; do a lot to change that wisdom. As we can now download assets in parallel, and reuse connections, multiple small assets don't have the overhead they did before, and can be advantageous, particularly given how our web components load.&lt;/p&gt;
&lt;p&gt;As I described before, since we load our web components asynchronously and independently after the page has rendered, it's faster to fire these down the wire component by component - so the browser can act on adding interactivity as soon as the code has loaded - rather than all in one blob where the browser has to parse the code for multiple components, perhaps to only add interactivity for one.&lt;/p&gt;
&lt;p&gt;Once you throw caching into the mix, things get even faster, and this extends to our CSS too: an update to one component in many cases won't touch the bundled code of the others. So for a user re-visiting MDN, they'll get interactivity for components that have been cached near-instantly, and for any that have changed - or for any server component CSS that has changed - they'll only be waiting for that changed component to download.&lt;/p&gt;
&lt;p&gt;Benchmarking these things is always required, and we could always do more, but what we've done so far showed that bundling things together was only as good or slower with a cold cache. We do also have a few levers in our build config to pull to easily bundle smaller components together if future benchmarking shows that that would be better.&lt;/p&gt;
  &lt;/section&gt;&lt;section&gt;&lt;p&gt;We're using quite a number of more modern web technologies here, and we needed an easy way to determine if we could use something, and if we could do without polyfills or progressive enhancement. Luckily enough, we've spent the last few years working on the Baseline project with a cross-vendor range of partners in the &lt;a href="https://www.w3.org/community/webdx/" target="_blank"&gt;WebDX group&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;This gave us a fantastically easy way to determine whether to use an API or not. The advice I gave the other engineers was: if it's "Baseline Widely Available", just use it; if it's "Baseline Newly Available", come talk to me first, and we'll figure out a polyfill or if it can be used as a progressive enhancement; and if it's "Baseline Limited Availability" or you need to do something there's no API for yet, think some more about if you really need to do it, then come talk to me.&lt;/p&gt;
&lt;p&gt;We ended up using a range of technologies, across all these statuses:&lt;/p&gt;
&lt;p&gt;Things like Custom Elements and Shadow DOM have been supported cross-browser for a surprisingly long time these days, and are solidly Baseline Widely Available.&lt;/p&gt;
&lt;p&gt;Declarative Shadow DOM, as I mentioned earlier, is supported cross browser but hasn't been in the web platform long enough to be Widely Available, so we use it as a progressive enhancement, with fallbacks in place for older browsers which don't support it yet, like our use of &lt;code&gt;global.css&lt;/code&gt; stylesheets.
There's also a few things we wanted which are at the very bleeding edge: one of those was extending &lt;code&gt;light-dark&lt;/code&gt; to images, where we used PostCSS to define a custom mixin, allowing us to use syntax like this:&lt;/p&gt;

&lt;p&gt;Using Baseline allows us to build confidently - knowing the vast majority of users can use a feature once it's reached Widely Available - and also keep our set of polyfills (and their overhead) small as we automatically remove them when features move from Newly Available to Widely Available.&lt;/p&gt;
  &lt;/section&gt;&lt;section&gt;&lt;p&gt;I've saved what I think our best improvement is until last: but I'm a little biased as an engineer working on MDN. Our old frontend development environment pained me every time I had to use it.&lt;/p&gt;
&lt;p&gt;The big problems were:&lt;/p&gt;
&lt;ul&gt;&lt;li&gt;It was slow: the default start command took about two minutes to present you with a functional locally running SPA, not including the time to download npm packages and so on.&lt;/li&gt;
&lt;li&gt;It was complex: there were an enormous number of commands in our &lt;code&gt;package.json&lt;/code&gt;, some of which gave you a development environment faster by skipping elements of the build, but that required an intricate knowledge of what exactly these complex commands were doing to know if you needed them or not.&lt;/li&gt;
&lt;li&gt;It didn't reliably restart: frequently changes, even simple ones like adding a new image, would require restarting the development server - and waiting another two minutes - to see the change.&lt;/li&gt;
&lt;li&gt;By default, we only rendered the SPA, with no SSR: that required running a separate command, which only created a production bundle and took even longer to run, which made debugging certain issues with SSR exceptionally difficult to do.&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;We were very keen to improve this - obviously for our own sake - but also to make the contribution process easier, and we have: the new frontend takes 2 &lt;em&gt;seconds&lt;/em&gt; to start, and there's really only one command you need:&lt;/p&gt;

&lt;p&gt;An enormous amount of this speed comes from using &lt;a href="https://rspack.rs/" target="_blank"&gt;Rspack&lt;/a&gt; as our build tool.
Webpack was what the old frontend used, and to its credit is fantastically configurable - something we needed given the approaches we took with our architecture.
Rspack has a webpack-compatible API, but it's written in Rust and is incredibly fast.&lt;/p&gt;
&lt;p&gt;Though &lt;a href="https://github.com/mdn/fred/blob/main/rspack.config.js" target="_blank"&gt;our Rspack config&lt;/a&gt;, currently standing at 650 LOC isn't necessarily &lt;em&gt;simple&lt;/em&gt;, I would argue it's &lt;em&gt;straightforward&lt;/em&gt;. There's very little logic hidden away, magically happening in our build tool. This config does a lot for us, including all the bundling, polyfilling, mixins, and optimizations I described before.&lt;/p&gt;
&lt;p&gt;Our architecture is simple enough to rely on a single command that does almost everything. Unlike before, there aren't really separate things to build independently. There's no SPA that we can render without SSR, as server components are fundamental to our architecture. We don't need multiple commands because there's only one way our website is assembled.&lt;/p&gt;
&lt;p&gt;This gives us an environment far more similar to our production environment than before, with the main difference being whether we reload the page on every change, and reload and re-render our server components on each request. This means we almost never have to restart our development environment, unless we're making changes to our Rspack config itself or applying various production-level optimizations. We have another command to build a production build with those optimizations and without the dynamic loading, which we can easily run if we need.&lt;/p&gt;
&lt;p&gt;Developing in this new environment has been an absolute joy for me, and I'm so glad we managed to make these improvements.&lt;/p&gt;
  &lt;/section&gt;&lt;section&gt;&lt;p&gt;I think you'll agree that this blog post is long enough, and I hear the Slack pings coming in from our content team telling me to finish this post already, but I don't feel like I've even told you half the story of our new frontend architecture!&lt;/p&gt;
&lt;p&gt;If you're interested in learning more, or have any questions about anything we've done, please feel free to come chat with us in the &lt;a href="https://discord.com/channels/1009925603572600863/1170042997212184576" target="_blank"&gt;#platform channel&lt;/a&gt; on our Discord.&lt;/p&gt;
&lt;p&gt;If you spot any issues, please raise them in &lt;a href="https://github.com/mdn/fred/issues" target="_blank"&gt;the fred GitHub repository&lt;/a&gt;. And if anything there looks like something you'd like to fix, have a go and submit a PR.&lt;/p&gt;
&lt;p&gt;Building this new frontend was a complete pleasure: it's been a privilege to be able to use new web technologies to build &lt;em&gt;the&lt;/em&gt; website that documents them.&lt;/p&gt;
  &lt;/section&gt;&lt;/div&gt;&lt;/div&gt;&lt;/body&gt;&lt;/html&gt;</content>
        <author>
            <name/>
        </author>
        <media:content medium="image" url="https://transcend-cdn.com/cm/d556c3a1-e57c-4bdf-a490-390a1aebf6dd/airgap.js"/>
        <source>
            <id>tag:feedly.com,2013:cloud/feed/https://developer.mozilla.org/en-US/blog/rss.xml</id>
            <title type="html">developer.mozilla.org</title>
            <link href="https://developer.mozilla.org" rel="alternate" type="text/html"/>
            <updated>2026-04-11T15:22:17Z</updated>
        </source>
    </entry>
    <entry>
        <id>tag:feedly.com,2013:cloud/entry/c1+r4TpK8uaTr2UOrMQDK0/1GSCBZDCx6Gq/d2cjVgo=_19cf035cda2:e7ddef:7d8a2c4</id>
        <title type="html">Tech's empiricism problem</title>
        <published>2026-03-15T06:36:23Z</published>
        <updated>2026-03-16T06:12:02Z</updated>
        <link href="https://deadsimpletech.com/blog/tech_empiricism_problem" rel="alternate" type="text/html"/>
        <summary type="html">The tech industry has extreme difficulty integrating information that doesn't have its source in an overtly rationalist process. In practice, this means that we tend to think that if you can't give a logical chain of deductions that proves that something is the case, your information is worthless. The issue with this is that day-to-day, in the tech world and outside of it, the vast bulk of the information we use to make decisions isn't this kind of information.</summary>
        <content type="html">The tech industry has extreme difficulty integrating information that doesn't have its source in an overtly rationalist process. In practice, this means that we tend to think that if you can't give a logical chain of deductions that proves that something is the case, your information is worthless. The issue with this is that day-to-day, in the tech world and outside of it, the vast bulk of the information we use to make decisions isn't this kind of information.</content>
        <author>
            <name/>
        </author>
        <media:content medium="image" url="https://lh3.googleusercontent.com/eqRlcrCaF4bxVodhZi4F3nmLLAQ4PAJS3x5X2kVPbRSVbmxCVwfyytkxvETlDirbESYi_roSdJv5jonaH3fuOGCvjFj8HMk-Bqn2n0o"/>
        <source>
            <id>tag:feedly.com,2013:cloud/feed/https://deadsimpletech.com/rss</id>
            <title type="html">deadSimpleTech blog feed</title>
            <link href="https://deadsimpletech.com" rel="alternate" type="text/html"/>
            <updated>2026-03-16T06:12:02Z</updated>
        </source>
    </entry>
    <entry>
        <id>tag:feedly.com,2013:cloud/entry/cOXRjoksGIQzU1pdqbdybn+OTWYeI0riptNneGckdsI=_19cc6dd7b70:3f8aa9:8275c42b</id>
        <title type="html">Nobody Gets Promoted for Simplicity</title>
        <published>2026-03-07T05:55:29Z</published>
        <updated>2026-03-07T05:55:34Z</updated>
        <link href="https://terriblesoftware.org/2026/03/03/nobody-gets-promoted-for-simplicity/" rel="alternate" type="text/html"/>
        <summary type="html">We reward complexity and ignore simplicity. In interviews, design reviews, and promotions. Here’s how to fix it.</summary>
        <content type="html">&lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"&gt;
&lt;html&gt;&lt;body&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;
&lt;blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow"&gt;
&lt;p class="wp-block-paragraph"&gt;&lt;em&gt;“Simplicity is a great virtue, but it requires hard work to achieve and education to appreciate. And to make matters worse, complexity sells better.”&lt;/em&gt; — Edsger Dijkstra&lt;/p&gt;
&lt;/blockquote&gt;



&lt;p class="wp-block-paragraph"&gt;I think there’s something quietly screwing up a lot of engineering teams. In interviews, in promotion packets, in design reviews: the engineer who overbuilds gets a compelling narrative, but the one who ships the simplest thing that works gets… nothing.&lt;/p&gt;



&lt;p class="wp-block-paragraph"&gt;This isn’t intentional, of course. Nobody sits down and says, &lt;em&gt;“let’s make sure the people who over-engineer things get promoted!”&lt;/em&gt; But that’s what can happen (and it has been, over and over again) when companies evaluate work incorrectly.&lt;/p&gt;



&lt;hr class="wp-block-separator has-alpha-channel-opacity"&gt;&lt;p class="wp-block-paragraph"&gt;Picture two engineers on the same team. Engineer A gets assigned a feature. She looks at the problem, considers a few options, and picks the simplest. A straightforward implementation, maybe 50 lines of code. Easy to read, easy to test, easy for the next person to pick up. It works. She ships it in a couple of days and moves on.&lt;/p&gt;



&lt;p class="wp-block-paragraph"&gt;Engineer B gets a similar feature. He also looks at the problem, but he sees an opportunity to build something more “robust.” He introduces a new abstraction layer, creates a pub/sub system for communication between components, adds a configuration framework so the feature is “extensible” for future use cases. It takes three weeks. There are multiple PRs. Lots of excited emojis when he shares the document explaining all of this.&lt;/p&gt;



&lt;p class="wp-block-paragraph"&gt;Now, promotion time comes around. Engineer B’s work practically writes itself into a promotion packet: &lt;em&gt;“Designed and implemented a scalable event-driven architecture, introduced a reusable abstraction layer adopted by multiple teams, and built a configuration framework enabling future extensibility.”&lt;/em&gt; That practically screams Staff+.&lt;/p&gt;



&lt;p class="wp-block-paragraph"&gt;But for Engineer A’s work, there’s almost nothing to say. &lt;em&gt;“Implemented feature X.”&lt;/em&gt; Three words. Her work was better. But it’s invisible because of how simple she made it look. You can’t write a compelling narrative about the thing you &lt;em&gt;didn’t&lt;/em&gt; build. &lt;strong&gt;Nobody gets promoted for the complexity they avoided&lt;/strong&gt;.&lt;/p&gt;



&lt;p class="wp-block-paragraph"&gt;Complexity looks smart. Not because it is, but because our systems are set up to reward it. And the incentive problem doesn’t start at promotion time. It starts before you even get the job.&lt;/p&gt;



&lt;p class="wp-block-paragraph"&gt;Think about interviews. You’re in a system design round, and you propose a simple solution. A single database, a straightforward API, maybe a caching layer. The interviewer is like: &lt;em&gt;“What about scalability? What if you have ten million users?”&lt;/em&gt; So you add services. You add queues. You add sharding. You draw more boxes on the whiteboard. The interviewer finally seems satisfied now.&lt;/p&gt;



&lt;p class="wp-block-paragraph"&gt;What you just learned is that complexity impresses people. The simple answer wasn’t wrong. It just wasn’t interesting enough. And you might carry that lesson with you into your career. To be fair, interviewers sometimes have good reasons to push on scale; they want to see how you think under pressure and whether you understand distributed systems. But when the takeaway for the candidate is “simple wasn’t enough,” something’s off.&lt;/p&gt;



&lt;p class="wp-block-paragraph"&gt;It also shows up in design reviews. An engineer proposes a clean, simple approach and gets hit with &lt;em&gt;“shouldn’t we future-proof this?”&lt;/em&gt; So they go back and add layers they don’t need yet, abstractions for problems that might never materialize, flexibility for requirements nobody has asked for. Not because the problem demanded it, but because the room expected it.&lt;/p&gt;



&lt;p class="wp-block-paragraph"&gt;I’ve &lt;a href="https://terriblesoftware.org/2025/05/28/duplication-is-not-the-enemy/"&gt;seen engineers&lt;/a&gt; (and have been one myself) create abstractions to avoid duplicating a few lines of code, only to end up with something far harder to understand and maintain than the duplication ever was. Every time, it felt like the right thing to do. The code looked more “professional.” More engineered. But the users didn’t get their feature any faster, and the next engineer to touch it had to spend half a day understanding the abstraction before they could make any changes.&lt;/p&gt;



&lt;p class="wp-block-paragraph"&gt;Now, let me be clear: complexity is sometimes the right call. If you’re processing millions of transactions, you might need distributed systems. If you have 10 teams working on the same product, you probably need service boundaries. When the problem is complex, the solution (probably) should be too!&lt;/p&gt;



&lt;p class="wp-block-paragraph"&gt;The issue isn’t complexity itself. It’s unearned complexity. There’s a difference between &lt;em&gt;“we’re hitting database limits and need to shard”&lt;/em&gt; and &lt;em&gt;“we might hit database limits in three years, so let’s shard now.”&lt;/em&gt;&lt;/p&gt;



&lt;p class="wp-block-paragraph"&gt;Some engineers understand this. And when you look at their code (and architecture), you think &lt;em&gt;“well, yeah, of course.”&lt;/em&gt; There’s no magic, no cleverness, nothing that makes you feel stupid for not understanding it. And that’s exactly the point.&lt;/p&gt;



&lt;p class="wp-block-paragraph"&gt;The &lt;a href="https://terriblesoftware.org/2025/11/25/what-actually-makes-you-senior/"&gt;actual path to seniority&lt;/a&gt; isn’t learning more tools and patterns, but learning when not to use them. &lt;strong&gt;Anyone can add complexity. It takes experience and confidence to leave it out&lt;/strong&gt;.&lt;/p&gt;



&lt;hr class="wp-block-separator has-alpha-channel-opacity"&gt;&lt;p class="wp-block-paragraph"&gt;So what do we actually do about this? Because saying “keep it simple” is easy. Changing incentive structures is harder.&lt;/p&gt;



&lt;p class="wp-block-paragraph"&gt;&lt;strong&gt;If you’re an engineer&lt;/strong&gt;, learn that simplicity needs to be made visible. The work doesn’t speak for itself; not because it’s not good, but because most systems aren’t designed to hear it.&lt;/p&gt;



&lt;p class="wp-block-paragraph"&gt;Start with how you talk about your own work. “Implemented feature X” doesn’t mean much. But &lt;em&gt;“evaluated three approaches including an event-driven architecture and a custom abstraction layer, determined that a straightforward implementation met all current and projected requirements, and shipped in two days with zero incidents over six months”&lt;/em&gt;, that’s the same simple work, just described in a way that captures the judgment behind it. The decision &lt;em&gt;not&lt;/em&gt; to build something is a decision, an important one! Document it accordingly.&lt;/p&gt;



&lt;p class="wp-block-paragraph"&gt;In design reviews, when someone asks “shouldn’t we future-proof this?”, don’t just cave and go add layers. Try: &lt;em&gt;“Here’s what it would take to add that later if we need it, and here’s what it costs us to add it now. I think we wait.”&lt;/em&gt; You’re not pushing back, but showing you’ve done your homework. You considered the complexity and chose not to take it on.&lt;/p&gt;



&lt;p class="wp-block-paragraph"&gt;And yes, bring this up with your manager. Something like: &lt;em&gt;“I want to make sure the way I document my work reflects the decisions I’m making, not just the code I’m writing. Can we talk about how to frame that for my next review?”&lt;/em&gt; Most managers will appreciate this because you’re making their job easier. You’re giving them language they can use to advocate for you.&lt;/p&gt;



&lt;p class="wp-block-paragraph"&gt;Now, if you do all of this and your team still only promotes the person who builds the most elaborate system… that’s useful information too. It tells you something about where you work. Some cultures genuinely value simplicity. Others say they do, but reward the opposite. If you’re in the second kind, you can either play the game or find a place where good judgment is actually recognized. But at least you’ll know which one you’re in.&lt;/p&gt;



&lt;p class="wp-block-paragraph"&gt;&lt;strong&gt;If you’re an engineering leader&lt;/strong&gt;, this one’s on you more than anyone else. You set the incentives, whether you realize it or not. And the problem is that most promotion criteria are basically designed to reward complexity, even when they don’t intend to. “Impact” gets measured by the size and scope of what someone built, which more often than not matters! But what they avoided should also matter.&lt;/p&gt;



&lt;p class="wp-block-paragraph"&gt;So start by changing the questions you ask. In design reviews, instead of “have we thought about scale?”, try &lt;em&gt;“what’s the simplest version we could ship, and what specific signals would tell us we need something more complex?”&lt;/em&gt; That one question changes the game: it makes simplicity the default and puts the burden of proof on complexity, not the other way around!&lt;/p&gt;



&lt;p class="wp-block-paragraph"&gt;In promotion discussions, push back when someone’s packet is basically a list of impressive-sounding systems. Ask: &lt;em&gt;“Was all of that necessary? Did we actually need a pub/sub system here, or did it just look good on paper?”&lt;/em&gt; And when an engineer on your team ships something clean and simple, help them write the narrative. “Evaluated multiple approaches and chose the simplest one that solved the problem” &lt;em&gt;is&lt;/em&gt; a compelling promotion case, but only if you actually treat it like one.&lt;/p&gt;



&lt;p class="wp-block-paragraph"&gt;One more thing: pay attention to what you celebrate publicly. If every shout-out in your team channel is for the big, complex project, that’s what people will optimize for. Start recognizing the engineer who deleted code. The one who said “we don’t need this yet” and was right.&lt;/p&gt;



&lt;p class="wp-block-paragraph"&gt;At the end of the day, if we keep rewarding complexity and ignoring simplicity, we shouldn’t be surprised when that’s exactly what we get. But the fix isn’t complicated. Which, I guess, is kind of the point.&lt;/p&gt;
&lt;/div&gt;


&lt;/div&gt;&lt;/div&gt;&lt;/body&gt;&lt;/html&gt;</content>
        <author>
            <name/>
        </author>
        <media:content medium="image" url="https://i0.wp.com/terriblesoftware.org/wp-content/uploads/2026/03/chjpdmf0zs9sci9pbwfnzxmvd2vic2l0zs8ymdiylta1l25zmtexndetaw1hz2uta3d2d3b1c3kuanbn.webp?fit=1024%2C680&amp;ssl=1&amp;w=640"/>
        <link href="https://i0.wp.com/terriblesoftware.org/wp-content/uploads/2026/03/chjpdmf0zs9sci9pbwfnzxmvd2vic2l0zs8ymdiylta1l25zmtexndetaw1hz2uta3d2d3b1c3kuanbn.webp?fit=1024%2C680&amp;ssl=1" rel="enclosure" type="image"/>
        <source>
            <id>tag:feedly.com,2013:cloud/feed/https://terriblesoftware.org/feed/</id>
            <title type="html">Terrible Software</title>
            <link href="https://terriblesoftware.org" rel="alternate" type="text/html"/>
            <updated>2026-03-07T05:55:34Z</updated>
        </source>
    </entry>
    <entry>
        <id>tag:feedly.com,2013:cloud/entry/cOXRjoksGIQzU1pdqbdybn+OTWYeI0riptNneGckdsI=_19cbf7aa19d:105d1:68bfe759</id>
        <title type="html">The Frontend Treadmill</title>
        <published>2026-03-05T19:30:10Z</published>
        <updated>2026-03-05T19:30:14Z</updated>
        <link href="https://polotek.net/posts/the-frontend-treadmill/" rel="alternate" type="text/html"/>
        <summary type="html">A lot of frontend teams are very convinced that rewriting their frontend will lead to the promised land. And I am the bearer of bad tidings.
If you are building a product that you hope has longevity, your frontend framework is the least interesting technical decision for you to make. And all of the time you spend arguing about it is wasted energy.
I will die on this hill.</summary>
        <content type="html">&lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"&gt;
&lt;html&gt;&lt;body&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;p&gt;A lot of frontend teams are very convinced that rewriting their frontend will lead to the promised land. And I am the bearer of bad tidings.&lt;/p&gt;&lt;p&gt;If you are building a product that you hope has longevity, your frontend framework is the least interesting technical decision for you to make. And all of the time you spend arguing about it is wasted energy.&lt;/p&gt;&lt;p&gt;I will die on this hill.&lt;/p&gt;&lt;p&gt;If your product is still around in 5 years, you’re doing great and you should feel successful. But guess what? Whatever framework you choose will be obsolete in 5 years. That’s just how the frontend community has been operating, and I don’t expect it to change soon. Even the popular frameworks that are still around are completely different. Because change is the name of the game. So they’re gonna rewrite their shit too and just give it a new version number.&lt;/p&gt;&lt;p&gt;Product teams that are smart are getting off the treadmill. Whatever framework you currently have, start investing in getting to know it deeply. Learn the tools until they are not an impediment to your progress. That’s the only option. Replacing it with a shiny new tool is a trap.&lt;/p&gt;&lt;p&gt;I also wanna give a piece of candid advice to engineers who are searching for jobs. If you feel strongly about what framework you want to use, please make that a criteria for your job search. Please stop walking into teams and derailing everything by trying to convince them to switch from framework X to your framework of choice. It’s really annoying and tremendously costly.&lt;/p&gt;&lt;p&gt;I always have to start with the cynical take. It’s just how I am. But I do want to talk about what I think should be happening instead.&lt;/p&gt;&lt;p&gt;Companies that want to reduce the cost of their frontend tech becoming obsoleted so often should be looking to get back to fundamentals. Your teams should be working closer to the web platform with a lot less complex abstractions. We need to relearn what the web is capable of and go back to that.&lt;/p&gt;&lt;p&gt;Let’s be clear, I’m not suggesting this is strictly better and the answer to all of your problems. I’m suggesting this as an intentional business tradeoff that I think provides more value and is less costly in the long run. I believe if you stick closer to core web technologies, you’ll be better able to hire capable engineers in the future without them convincing you they can’t do work without rewriting millions of lines of code.&lt;/p&gt;&lt;p&gt;And if you’re an engineer, you will be able to retain much higher market value over time if you dig into and understand core web technologies. I was here before react, and I’ll be here after it dies. You may trade some job marketability today. But it does a lot more for career longevity than trying to learn every new thing that gets popular. And you see how quickly they discarded us when the market turned anyway. Knowing certain tech won’t save you from those realities.&lt;/p&gt;&lt;p&gt;I couldn’t speak this candidly about this stuff when I held a management role. People can’t help but question my motivations and whatever agenda I may be pushing. Either that or I get into a lot of trouble with my internal team because they think I’m talking about them. But this is just what I’ve seen play out after doing this for 20+ years. And I feel like we need to be able to speak plainly.&lt;/p&gt;&lt;p&gt;This has been brewing in my head for a long time. The frontend ecosystem is kind of broken right now. And it’s frustrating to me for a few different reasons. New developers are having an extremely hard time learning enough skills to be gainfully employed. They are drowning in this complex garbage and feeling really disheartened. As a result, companies are finding it more difficult to do basic hiring. The bar is so high just to get a regular dev job. And everybody loses.&lt;/p&gt;&lt;p&gt;What’s even worse is that I believe a lot of this energy is wasted. People that are learning the current tech ecosystem are absolutely not learning web fundamentals. They are too abstracted away. And when the stack changes again, these folks are going to be at a serious disadvantage when they have to adapt away from what they learned. It’s a deep disservice to people’s professional careers, and it’s going to cause a lot of heartache later.&lt;/p&gt;&lt;p&gt;On a more personal note, this is frustrating to me because I think it’s a big part of why we’re seeing the web stagnate so much. I still run into lots of devs who are creative and enthusiastic about building cool things. They just can’t. They are trying and failing because the tools being recommended to them are just not approachable enough. And at the same time, they’re being convinced that learning fundamentals is a waste of time because it’s so different from what everybody is talking about.&lt;/p&gt;&lt;p&gt;I guess I want to close by stating my biases. I’m a web guy. I’ve been bullish on the web for 20+ years, and I will continue to be. I think it is an extremely capable and unique platform for delivering software. And it has only gotten better over time while retaining an incredible level of backwards compatibility. The underlying tools we have are dope now. But our current framework layer is working against the grain instead of embracing the platform.&lt;/p&gt;&lt;blockquote&gt;&lt;p&gt;This is from &lt;a href="https://social.polotek.net/@polotek/112617458589147547"&gt;a recent thread I wrote on mastodon&lt;/a&gt;. Reproduced with only light editing.&lt;/p&gt;&lt;/blockquote&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/body&gt;&lt;/html&gt;</content>
        <author>
            <name>Marco Rogers (polotek)</name>
        </author>
        <media:content medium="image" url="https://polotek.net/js/script.min.74bf1a3fcf1af396efa4acf3e660e876b61a2153ab9cbe1893ac24ea6d4f94ee.js"/>
    </entry>
    <entry>
        <id>tag:feedly.com,2013:cloud/entry/cOXRjoksGIQzU1pdqbdybn+OTWYeI0riptNneGckdsI=_19cbf6d2731:80d01:4b6775c</id>
        <title type="html">A GitHub Issue Title Compromised 4,000 Developer Machines</title>
        <published>2026-03-05T19:15:27Z</published>
        <updated>2026-03-05T19:15:36Z</updated>
        <link href="https://grith.ai/blog/clinejection-when-your-ai-tool-installs-another" rel="alternate" type="text/html"/>
        <summary type="html">A prompt injection in a GitHub issue triggered a chain reaction that ended with 4,000 developers getting OpenClaw installed without consent. The attack composes well-understood vulnerabilities into something new: one AI tool bootstrapping another.</summary>
        <content type="html">&lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"&gt;
&lt;html&gt;&lt;body&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;&lt;figure&gt;&lt;img src="https://grith.ai/blog/clinejection-when-your-ai-tool-installs-another/hero-clinejection-chain-1600x900.png" alt="The Clinejection attack chain: a prompt injection in a GitHub issue title cascades through AI triage, cache poisoning, and credential theft to silently install OpenClaw on 4,000 developer machines" tabindex="0"&gt;&lt;div&gt;&lt;p&gt;&lt;/p&gt;&lt;img src="https://grith.ai/blog/clinejection-when-your-ai-tool-installs-another/hero-clinejection-chain-1600x900.png" alt="The Clinejection attack chain: a prompt injection in a GitHub issue title cascades through AI triage, cache poisoning, and credential theft to silently install OpenClaw on 4,000 developer machines"&gt;&lt;span&gt;esc to close&lt;/span&gt;&lt;/div&gt;&lt;figcaption&gt;Five steps from a GitHub issue title to 4,000 compromised developer machines. The entry point was natural language.&lt;/figcaption&gt;&lt;/figure&gt;&lt;p&gt;On February 17, 2026, someone published &lt;code&gt;cline@2.3.0&lt;/code&gt; to npm. The CLI binary was byte-identical to the previous version. The only change was one line in &lt;code&gt;package.json&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;"postinstall": "npm install -g openclaw@latest"
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For the next eight hours, every developer who installed or updated Cline got OpenClaw - a separate AI agent with full system access - installed globally on their machine without consent. Approximately 4,000 downloads occurred before the package was pulled&lt;sup&gt;&lt;a href="https://grith.ai/blog/clinejection-when-your-ai-tool-installs-another#user-content-fn-1" id="user-content-fnref-1"&gt;1&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;The interesting part is not the payload. It is how the attacker got the npm token in the first place: by injecting a prompt into a GitHub issue title, which an AI triage bot read, interpreted as an instruction, and executed.&lt;/p&gt;
&lt;h2&gt;The full chain&lt;/h2&gt;
&lt;p&gt;The attack - which Snyk named "Clinejection"&lt;sup&gt;&lt;a href="https://grith.ai/blog/clinejection-when-your-ai-tool-installs-another#user-content-fn-2" id="user-content-fnref-2"&gt;2&lt;/a&gt;&lt;/sup&gt; - composes five well-understood vulnerabilities into a single exploit that requires nothing more than opening a GitHub issue.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 1: Prompt injection via issue title.&lt;/strong&gt; Cline had deployed an AI-powered issue triage workflow using Anthropic's &lt;code&gt;claude-code-action&lt;/code&gt;. The workflow was configured with &lt;code&gt;allowed_non_write_users: "*"&lt;/code&gt;, meaning any GitHub user could trigger it by opening an issue. The issue title was interpolated directly into Claude's prompt via &lt;code&gt;${{ github.event.issue.title }}&lt;/code&gt; without sanitisation.&lt;/p&gt;
&lt;p&gt;On January 28, an attacker created Issue #8904 with a title crafted to look like a performance report but containing an embedded instruction: install a package from a specific GitHub repository&lt;sup&gt;&lt;a href="https://grith.ai/blog/clinejection-when-your-ai-tool-installs-another#user-content-fn-3" id="user-content-fnref-3"&gt;3&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 2: The AI bot executes arbitrary code.&lt;/strong&gt; Claude interpreted the injected instruction as legitimate and ran &lt;code&gt;npm install&lt;/code&gt; pointing to the attacker's fork - a typosquatted repository (&lt;code&gt;glthub-actions/cline&lt;/code&gt;, note the missing 'i' in 'github'). The fork's &lt;code&gt;package.json&lt;/code&gt; contained a preinstall script that fetched and executed a remote shell script.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 3: Cache poisoning.&lt;/strong&gt; The shell script deployed Cacheract, a GitHub Actions cache poisoning tool. It flooded the cache with over 10GB of junk data, triggering GitHub's LRU eviction policy and evicting legitimate cache entries. The poisoned entries were crafted to match the cache key pattern used by Cline's nightly release workflow.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 4: Credential theft.&lt;/strong&gt; When the nightly release workflow ran and restored &lt;code&gt;node_modules&lt;/code&gt; from cache, it got the compromised version. The release workflow held the &lt;code&gt;NPM_RELEASE_TOKEN&lt;/code&gt;, &lt;code&gt;VSCE_PAT&lt;/code&gt; (VS Code Marketplace), and &lt;code&gt;OVSX_PAT&lt;/code&gt; (OpenVSX). All three were exfiltrated&lt;sup&gt;&lt;a href="https://grith.ai/blog/clinejection-when-your-ai-tool-installs-another#user-content-fn-3" id="user-content-fnref-3-2"&gt;3&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 5: Malicious publish.&lt;/strong&gt; Using the stolen npm token, the attacker published &lt;code&gt;cline@2.3.0&lt;/code&gt; with the OpenClaw postinstall hook. The compromised version was live for eight hours before StepSecurity's automated monitoring flagged it - approximately 14 minutes after publication&lt;sup&gt;&lt;a href="https://grith.ai/blog/clinejection-when-your-ai-tool-installs-another#user-content-fn-1" id="user-content-fnref-1-2"&gt;1&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;h2&gt;A botched rotation made it worse&lt;/h2&gt;
&lt;p&gt;Security researcher Adnan Khan had actually discovered the vulnerability chain in late December 2025 and reported it via a GitHub Security Advisory on January 1, 2026. He sent multiple follow-ups over five weeks. None received a response&lt;sup&gt;&lt;a href="https://grith.ai/blog/clinejection-when-your-ai-tool-installs-another#user-content-fn-3" id="user-content-fnref-3-3"&gt;3&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;When Khan publicly disclosed on February 9, Cline patched within 30 minutes by removing the AI triage workflows. They began credential rotation the next day.&lt;/p&gt;
&lt;p&gt;But the rotation was incomplete. The team deleted the wrong token, leaving the exposed one active&lt;sup&gt;&lt;a href="https://grith.ai/blog/clinejection-when-your-ai-tool-installs-another#user-content-fn-4" id="user-content-fnref-4"&gt;4&lt;/a&gt;&lt;/sup&gt;. They discovered the error on February 11 and re-rotated. But the attacker had already exfiltrated the credentials, and the npm token remained valid long enough to publish the compromised package six days later.&lt;/p&gt;
&lt;p&gt;Khan was not the attacker. A separate, unknown actor found Khan's proof-of-concept on his test repository and weaponised it against Cline directly&lt;sup&gt;&lt;a href="https://grith.ai/blog/clinejection-when-your-ai-tool-installs-another#user-content-fn-3" id="user-content-fnref-3-4"&gt;3&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;h2&gt;The new pattern: AI installs AI&lt;/h2&gt;
&lt;p&gt;The specific vulnerability chain is interesting but not unprecedented. Prompt injection, cache poisoning, and credential theft are all documented attack classes. What makes Clinejection distinct is the outcome: one AI tool silently bootstrapping a second AI agent on developer machines.&lt;/p&gt;
&lt;p&gt;This creates a recursion problem in the supply chain. The developer trusts Tool A (Cline). Tool A is compromised to install Tool B (OpenClaw). Tool B has its own capabilities - shell execution, credential access, persistent daemon installation - that are independent of Tool A and invisible to the developer's original trust decision.&lt;/p&gt;
&lt;p&gt;OpenClaw as installed could read credentials from &lt;code&gt;~/.openclaw/&lt;/code&gt;, execute shell commands via its Gateway API, and install itself as a persistent system daemon surviving reboots&lt;sup&gt;&lt;a href="https://grith.ai/blog/clinejection-when-your-ai-tool-installs-another#user-content-fn-1" id="user-content-fnref-1-3"&gt;1&lt;/a&gt;&lt;/sup&gt;. The severity was debated - Endor Labs characterised the payload as closer to a proof-of-concept than a weaponised attack&lt;sup&gt;&lt;a href="https://grith.ai/blog/clinejection-when-your-ai-tool-installs-another#user-content-fn-5" id="user-content-fnref-5"&gt;5&lt;/a&gt;&lt;/sup&gt; - but the mechanism is what matters. The next payload will not be a proof-of-concept.&lt;/p&gt;
&lt;p&gt;This is the supply chain equivalent of &lt;a href="https://en.wikipedia.org/wiki/Confused_deputy_problem"&gt;confused deputy&lt;/a&gt;: the developer authorises Cline to act on their behalf, and Cline (via compromise) delegates that authority to an entirely separate agent the developer never evaluated, never configured, and never consented to.&lt;/p&gt;
&lt;h2&gt;Why existing controls did not catch it&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;npm audit&lt;/strong&gt;: The postinstall script installs a legitimate, non-malicious package (OpenClaw). There is no malware to detect.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Code review&lt;/strong&gt;: The CLI binary was byte-identical to the previous version. Only &lt;code&gt;package.json&lt;/code&gt; changed, and only by one line. Automated diff checks that focus on binary changes would miss it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Provenance attestations&lt;/strong&gt;: Cline was not using OIDC-based npm provenance at the time. The compromised token could publish without provenance metadata, which StepSecurity flagged as anomalous&lt;sup&gt;&lt;a href="https://grith.ai/blog/clinejection-when-your-ai-tool-installs-another#user-content-fn-1" id="user-content-fnref-1-4"&gt;1&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Permission prompts&lt;/strong&gt;: The installation happens in a postinstall hook during &lt;code&gt;npm install&lt;/code&gt;. No AI coding tool prompts the user before a dependency's lifecycle script runs. The operation is invisible.&lt;/p&gt;
&lt;p&gt;The attack exploited the gap between what developers think they are installing (a specific version of Cline) and what actually executes (arbitrary lifecycle scripts from the package and everything it transitively installs).&lt;/p&gt;
&lt;h2&gt;What Cline changed afterward&lt;/h2&gt;
&lt;p&gt;Cline's post-mortem&lt;sup&gt;&lt;a href="https://grith.ai/blog/clinejection-when-your-ai-tool-installs-another#user-content-fn-4" id="user-content-fnref-4-2"&gt;4&lt;/a&gt;&lt;/sup&gt; outlines several remediation steps:&lt;/p&gt;
&lt;ul&gt;&lt;li&gt;Eliminated GitHub Actions cache usage from credential-handling workflows&lt;/li&gt;
&lt;li&gt;Adopted OIDC provenance attestations for npm publishing, eliminating long-lived tokens&lt;/li&gt;
&lt;li&gt;Added verification requirements for credential rotation&lt;/li&gt;
&lt;li&gt;Began working on a formal vulnerability disclosure process with SLAs&lt;/li&gt;
&lt;li&gt;Commissioned third-party security audits of CI/CD infrastructure&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;These are meaningful improvements. The OIDC migration alone would have prevented the attack - a stolen token cannot publish packages when provenance requires a cryptographic attestation from a specific GitHub Actions workflow.&lt;/p&gt;
&lt;h2&gt;The architectural question&lt;/h2&gt;
&lt;p&gt;Clinejection is a supply chain attack, but it is also an agent security problem. The entry point was natural language in a GitHub issue title. The first link in the chain was an AI bot that interpreted untrusted text as an instruction and executed it with the privileges of the CI environment.&lt;/p&gt;
&lt;p&gt;This is the same structural pattern we have written about in the context of &lt;a href="https://grith.ai/blog/mcp-servers-new-npm-packages"&gt;MCP tool poisoning&lt;/a&gt; and &lt;a href="https://grith.ai/blog/agent-skills-supply-chain"&gt;agent skill registries&lt;/a&gt; - untrusted input reaches an agent, the agent acts on it, and nothing evaluates the resulting operations before they execute.&lt;/p&gt;
&lt;p&gt;The difference here is that the agent was not a developer's local coding assistant. It was an automated CI workflow that ran on every new issue, with shell access and cached credentials. The blast radius was not one developer's machine - it was the entire project's publication pipeline.&lt;/p&gt;
&lt;p&gt;Every team deploying AI agents in CI/CD - for issue triage, code review, automated testing, or any other workflow - has this same exposure. The agent processes untrusted input (issues, PRs, comments) and has access to secrets (tokens, keys, credentials). The question is whether anything evaluates what the agent does with that access.&lt;/p&gt;
&lt;p&gt;Per-syscall interception catches this class of attack at the operation layer. When the AI triage bot attempts to run &lt;code&gt;npm install&lt;/code&gt; from an unexpected repository, the operation is evaluated against policy before it executes - regardless of what the issue title said. When a lifecycle script attempts to exfiltrate credentials to an external host, the egress is blocked.&lt;/p&gt;
&lt;p&gt;The entry point changes. The operations do not. &lt;a href="https://grith.ai/"&gt;grith&lt;/a&gt; was built to catch exactly this class of problem - evaluating every operation at the syscall layer, regardless of which agent triggered it or why.&lt;/p&gt;
&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/body&gt;&lt;/html&gt;</content>
        <author>
            <name/>
        </author>
        <media:content medium="image" url="https://grith.ai/blog/clinejection-when-your-ai-tool-installs-another/hero-clinejection-chain-1600x900.png"/>
    </entry>
    <entry>
        <id>tag:feedly.com,2013:cloud/entry/cOXRjoksGIQzU1pdqbdybn+OTWYeI0riptNneGckdsI=_19cbc9c1a2c:32fe9b:98d097d6</id>
        <title type="html">Container queries and units in action</title>
        <published>2026-03-05T06:07:52Z</published>
        <updated>2026-03-05T06:07:56Z</updated>
        <link href="https://web.dev/articles/baseline-in-action-container-queries" rel="alternate" type="text/html"/>
        <summary type="html">Learn how to use CSS container queries for adding both responsive typography and styles based on container dimensions in this guide.</summary>
        <content type="html">&lt;!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd"&gt;
&lt;html&gt;&lt;body&gt;&lt;div&gt;&lt;div&gt;&lt;div&gt;

  
    
    




&lt;p&gt;

&lt;/p&gt;

&lt;p&gt;
  Published: October 23, 2025
&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;
   
&lt;/p&gt;

&lt;p&gt;One of the goals when writing CSS is to build component parts that will adapt well to different (and unexpected) contexts. Ideally, a component can be placed inside any "container" element without it feeling broken or out of place. How can you accomplish this in a complex layout like a store where the primary component—the "product"—has to fit into a variety of list layouts, including the sidebar?&lt;/p&gt;

&lt;h2 id="responsive_typography_with_cqi_units" tabindex="-1"&gt;&lt;span&gt;Responsive typography with &lt;code dir="ltr"&gt;cqi&lt;/code&gt; units&lt;/span&gt;&lt;/h2&gt;

&lt;p&gt;The first step is to define some basic sizing variables that could be reused across the project—starting with whitespace sizes. But before creating any custom properties, the browser provides some useful named values as CSS units:&lt;/p&gt;

&lt;ul&gt;&lt;li&gt;&lt;code dir="ltr"&gt;1em&lt;/code&gt;: the current font size.&lt;/li&gt;
&lt;li&gt;&lt;code dir="ltr"&gt;1rem&lt;/code&gt;: the font size on the &lt;code dir="ltr"&gt;:root (html)&lt;/code&gt; element.&lt;/li&gt;
&lt;li&gt;&lt;code dir="ltr"&gt;1lh&lt;/code&gt; / &lt;code dir="ltr"&gt;1rlh&lt;/code&gt;: the current and root line heights.&lt;/li&gt;
&lt;li&gt;&lt;code dir="ltr"&gt;1vw&lt;/code&gt;: the viewport width.&lt;/li&gt;
&lt;li&gt;&lt;code dir="ltr"&gt;1vi&lt;/code&gt;: the viewport "inline" size (for English, this is the same as &lt;code dir="ltr"&gt;vw&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;&lt;code dir="ltr"&gt;1cqi&lt;/code&gt;: the inline size of the nearest "container" (defaulting to the viewport).&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;You can think of these units as common variables provided by the browser, with a shorthand syntax for multiplication. If you wanted half of a &lt;code dir="ltr"&gt;--line-height&lt;/code&gt; custom property in CSS, you would need to write the entire calculation &lt;code dir="ltr"&gt;calc(0.5 * var(--line-height))&lt;/code&gt;, but with the &lt;code dir="ltr"&gt;lh&lt;/code&gt; unit, you can ask for &lt;code dir="ltr"&gt;0.5lh&lt;/code&gt; instead.&lt;/p&gt;

&lt;p&gt;Custom properties like &lt;code dir="ltr"&gt;--brand-color&lt;/code&gt; and &lt;code dir="ltr"&gt;--button-background&lt;/code&gt; have different meanings and serve a different purpose, even when they result in the same &lt;code dir="ltr"&gt;deepPink&lt;/code&gt; color (another browser-provided variable). Similarly, &lt;code dir="ltr"&gt;1em&lt;/code&gt; might sometimes be equal to &lt;code dir="ltr"&gt;16px&lt;/code&gt;, but that's not a stable relationship. Like any other variable, units should be used to express a relationship, rather than an expected value.&lt;/p&gt;

&lt;p&gt;Both &lt;code dir="ltr"&gt;1em&lt;/code&gt; and &lt;code dir="ltr"&gt;1lh&lt;/code&gt; are font-relative units that could be used for spacing, but only one of them has a reliable relationship to the current line height. If this page involved a lot of elements with prose in them—paragraphs and lists, for example—the &lt;code dir="ltr"&gt;lh&lt;/code&gt; unit would work well for spacing between them:&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;pre dir="ltr"&gt;&lt;code dir="ltr"&gt;&lt;span&gt;p&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ul&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;ol&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;{&lt;/span&gt;
&lt;span&gt;  &lt;/span&gt;&lt;span&gt;margin-block&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;lh&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That will maintain a consistent baseline rhythm, without any extra work. But this page has almost no prose. Instead of spacing within a flow of text, this layout requires spacing to push text away from the edge of a card, and spacing between columns and rows in a grid or stacked layout. In this case there are several things to consider:&lt;/p&gt;

&lt;ul&gt;&lt;li&gt;Multiples (or fractions) of &lt;code dir="ltr"&gt;1lh&lt;/code&gt; may still be useful for maintaining vertical rhythm across the page.&lt;/li&gt;
&lt;li&gt;The &lt;code dir="ltr"&gt;cqi&lt;/code&gt; unit would account for the amount of &lt;em&gt;available space&lt;/em&gt; in a given context.&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;By combining these two units in a &lt;code dir="ltr"&gt;round()&lt;/code&gt; function, the &lt;code dir="ltr"&gt;--gap&lt;/code&gt; variable is based primarily on the container size, but rounded up to a multiple of quarter-lines:&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;pre dir="ltr"&gt;&lt;code dir="ltr"&gt;&lt;span&gt;html&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;{&lt;/span&gt;
&lt;span&gt;  &lt;/span&gt;&lt;span&gt;--gap&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;round&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;up&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;2&lt;/span&gt;&lt;span&gt;cqi&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;0.25&lt;/span&gt;&lt;span&gt;lh&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;/p&gt;

&lt;p&gt;If the &lt;code dir="ltr"&gt;line-height&lt;/code&gt; is &lt;code dir="ltr"&gt;20px&lt;/code&gt;, then the &lt;code dir="ltr"&gt;--&lt;/code&gt;gap will be multiples of &lt;code dir="ltr"&gt;5px&lt;/code&gt;—but the exact multiple will depend on available space. If either the &lt;code dir="ltr"&gt;line-height&lt;/code&gt; or available space change, the &lt;code dir="ltr"&gt;--gap&lt;/code&gt; variable will adapt to its new context. To make the &lt;code dir="ltr"&gt;--gap&lt;/code&gt; consistent across the entire design, you could replace the container-relative &lt;code dir="ltr"&gt;cqi&lt;/code&gt; units with viewport-relative &lt;code dir="ltr"&gt;vi&lt;/code&gt; units.&lt;/p&gt;

&lt;p&gt;The same approach is useful when establishing "fluid" font sizes that respond to available space. This &lt;code dir="ltr"&gt;--body-text&lt;/code&gt; variable is based on the user-provided font preference, with some range to adapt based on the container. In this case, &lt;code dir="ltr"&gt;clamp()&lt;/code&gt; ensures the font will be at least as large as the user's preference, can grow some as the container size increases, but will stop growing at some point:&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;pre dir="ltr"&gt;&lt;code dir="ltr"&gt;&lt;span&gt;html&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;{&lt;/span&gt;
&lt;span&gt;  &lt;/span&gt;&lt;span&gt;--body-text&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;clamp&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;rem&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;0.875&lt;/span&gt;&lt;span&gt;rem&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;+&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;0.5&lt;/span&gt;&lt;span&gt;cqi&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;1.25&lt;/span&gt;&lt;span&gt;rem&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;&lt;li&gt;The range is clamped between &lt;code dir="ltr"&gt;1rem&lt;/code&gt; and &lt;code dir="ltr"&gt;1.25rem&lt;/code&gt;, to stay near the user-selected font size.&lt;/li&gt;
&lt;li&gt;The &lt;code dir="ltr"&gt;cqi&lt;/code&gt; value determines how &lt;em&gt;fast&lt;/em&gt; the font will grow or shrink in relation to available space, which is added to a &lt;code dir="ltr"&gt;rem&lt;/code&gt; value to offset that growth based on the user-selected size.&lt;/li&gt;
&lt;/ul&gt;&lt;p&gt;Keeping the central &lt;code dir="ltr"&gt;rem&lt;/code&gt; value near &lt;code dir="ltr"&gt;1&lt;/code&gt; and the added &lt;code dir="ltr"&gt;cqi&lt;/code&gt; value low ensures that users still have significant control when zooming in or out. You can adjust those two values, and then use browser zoom to see how they interact. The closer you get to &lt;code dir="ltr"&gt;100cqi&lt;/code&gt;, and the farther you fall below &lt;code dir="ltr"&gt;1rem&lt;/code&gt;, the less influence user font preferences will have—and the less font sizes will respond to zooming in or out.&lt;/p&gt;

&lt;p&gt;The &lt;code dir="ltr"&gt;--item-title&lt;/code&gt; variable is slightly larger and more responsive, and the &lt;code dir="ltr"&gt;--list-title&lt;/code&gt; size responds to the viewport size (using &lt;code dir="ltr"&gt;vi&lt;/code&gt;) rather than the immediate container size. That way item headings respond to their context, but the main list headings all match in size no matter where they show up.&lt;/p&gt;
&lt;aside&gt;&lt;strong&gt;Note:&lt;/strong&gt;&lt;span&gt; Product titles in the cart sidebar might be slightly different from product titles in the main list—but the "cart" heading is always identical to the "products" list heading.&lt;/span&gt;&lt;/aside&gt;&lt;h2 id="defining_containers_to_measure_in_context" tabindex="-1"&gt;&lt;span&gt;Defining containers to measure in context&lt;/span&gt;&lt;/h2&gt;

&lt;p&gt;At this point, container query units have been used to create adaptive typography on a web page, but new containers haven't been defined.&lt;/p&gt;

&lt;p&gt;By default, &lt;code dir="ltr"&gt;1cqi&lt;/code&gt; (&lt;code dir="ltr"&gt;1/100&lt;/code&gt; container query inline size) is the same as &lt;code dir="ltr"&gt;1svi&lt;/code&gt; (&lt;code dir="ltr"&gt;1/100&lt;/code&gt; small viewport inline size) because the &lt;a href="https://web.dev/learn/css/sizing#alternative_viewport-relative_units"&gt;"small" viewport&lt;/a&gt; acts as the initial container for any web page. In order to take full advantage of the &lt;code dir="ltr"&gt;cqi&lt;/code&gt; unit, you need to define additional "containers" within the page. The primary layout containers on this page are the &lt;code dir="ltr"&gt;product-list&lt;/code&gt; and &lt;code dir="ltr"&gt;shopping-cart&lt;/code&gt;—so they are set to expose their &lt;code dir="ltr"&gt;inline-size&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;pre dir="ltr"&gt;&lt;code dir="ltr"&gt;&lt;span&gt;product-list&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;
&lt;span&gt;shopping-cart&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;{&lt;/span&gt;
&lt;span&gt;  &lt;/span&gt;&lt;span&gt;container-type&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;inline&lt;/span&gt;&lt;span&gt;-&lt;/span&gt;&lt;span&gt;size&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;aside&gt;&lt;strong&gt;Note:&lt;/strong&gt;&lt;span&gt; &lt;code dir="ltr"&gt;&amp;lt;div&amp;gt;&lt;/code&gt; or &lt;code dir="ltr"&gt;&amp;lt;section&amp;gt;&lt;/code&gt; elements with &lt;code dir="ltr"&gt;.product-list&lt;/code&gt; and &lt;code dir="ltr"&gt;.shopping-cart&lt;/code&gt; classes would also work here. Since those have no semantic meaning or functionality in HTML, it also works to define unregistered custom elements with those names. This doesn't impact the CSS, except that custom elements use a &lt;code dir="ltr"&gt;display&lt;/code&gt; of &lt;code dir="ltr"&gt;inline&lt;/code&gt; by default.&lt;/span&gt;&lt;/aside&gt;&lt;p&gt;Container query units—including &lt;code dir="ltr"&gt;cqi&lt;/code&gt;—aren't able to measure the element that they are used on. If you set the &lt;code dir="ltr"&gt;width&lt;/code&gt; of &lt;code dir="ltr"&gt;shopping-cart&lt;/code&gt; to &lt;code dir="ltr"&gt;25cqi&lt;/code&gt;, it would be a paradox to determine the container's width based on its own width! Instead, the result will be based on the next ancestor container in the tree hierarchy &lt;em&gt;that contains&lt;/em&gt; &lt;code dir="ltr"&gt;shopping-cart&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;code dir="ltr"&gt;product-detail&lt;/code&gt; cards are part of a &lt;code dir="ltr"&gt;product-list&lt;/code&gt; grid that can be changed by the user. The &lt;code dir="ltr"&gt;list&lt;/code&gt; layout option displays each card at full width. In that case, referring to the parent container size is useful, but both the &lt;code dir="ltr"&gt;small-grid&lt;/code&gt; and &lt;code dir="ltr"&gt;large-grid&lt;/code&gt; options cause the card to grow and shrink depending on how many columns fit into the container. Even when the container is quite large, the cards can remain tightly packed into smaller grid cells.&lt;/p&gt;

&lt;p&gt;There's currently no way to declare those grid cells as "containers" directly. Instead, an extra element is needed to measure within each cell. That's why each &lt;code dir="ltr"&gt;product-detail&lt;/code&gt; instance has an &lt;code dir="ltr"&gt;&amp;lt;article&amp;gt;&lt;/code&gt; element nested directly inside. &lt;code dir="ltr"&gt;product-detail &amp;gt; article&lt;/code&gt; is the card to be styled, while &lt;code dir="ltr"&gt;product-detail&lt;/code&gt; itself is used only as a container to measure. That allows the &lt;code dir="ltr"&gt;cqi&lt;/code&gt;-based text and spacing calculations previously defined to be recalculated for the space available to each card.&lt;/p&gt;

&lt;h2 id="explicit_container_queries" tabindex="-1"&gt;&lt;span&gt;Explicit container queries&lt;/span&gt;&lt;/h2&gt;

&lt;p&gt;Container units are powerful, but sometimes it's useful to make more dramatic changes in a component layout when the available size crosses a threshold. These are often called &lt;em&gt;breakpoints&lt;/em&gt;—since the fix is applied at the point when a given layout begins to break. You may already be familiar with using &lt;code dir="ltr"&gt;@media&lt;/code&gt; to add breakpoints based on the viewport size:&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;pre dir="ltr"&gt;&lt;code dir="ltr"&gt;&lt;span&gt;main&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;{&lt;/span&gt;
&lt;span&gt;  &lt;/span&gt;&lt;span&gt;display&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;grid&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;  &lt;/span&gt;&lt;span&gt;grid-template&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;
&lt;span&gt;    &lt;/span&gt;&lt;span&gt;'controls'&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;auto&lt;/span&gt;
&lt;span&gt;    &lt;/span&gt;&lt;span&gt;'cart'&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;fr&lt;/span&gt;
&lt;span&gt;    &lt;/span&gt;&lt;span&gt;'list'&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;auto&lt;/span&gt;
&lt;span&gt;    &lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;minmax&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;min&lt;/span&gt;&lt;span&gt;-content&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;fr&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;

&lt;span&gt;  &lt;/span&gt;&lt;span&gt;@media&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;(width&lt;/span&gt;&lt;span&gt; &amp;gt; &lt;/span&gt;&lt;span&gt;30em)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;{&lt;/span&gt;
&lt;span&gt;    &lt;/span&gt;&lt;span&gt;grid-template&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;
&lt;span&gt;      &lt;/span&gt;&lt;span&gt;'controls controls'&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;auto&lt;/span&gt;
&lt;span&gt;      &lt;/span&gt;&lt;span&gt;'list cart'&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;fr&lt;/span&gt;
&lt;span&gt;      &lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;2&lt;/span&gt;&lt;span&gt;fr&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;fr&lt;/span&gt;
&lt;span&gt;    &lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;  &lt;/span&gt;&lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here, &lt;code dir="ltr"&gt;shopping-cart&lt;/code&gt; appears above the main &lt;code dir="ltr"&gt;product-list&lt;/code&gt; on small screens—but at a certain point, there's more horizontal space, and a sidebar makes more sense. Since that shift depends on the overall viewport, a media query is used to handle the change. However, the &lt;code dir="ltr"&gt;product-list&lt;/code&gt; and &lt;code dir="ltr"&gt;product-detail&lt;/code&gt; components might appear in different contexts, somewhat independent of the viewport size. When the viewport grows wider than the sidebar breakpoint, the &lt;code dir="ltr"&gt;shopping-cart&lt;/code&gt; component suddenly becomes &lt;em&gt;smaller&lt;/em&gt;, providing less space for products inside. This is where container queries become necessary. The syntax is nearly identical to a media query, but uses &lt;code dir="ltr"&gt;@container&lt;/code&gt; rather than &lt;code dir="ltr"&gt;@media&lt;/code&gt; at the start of the rule:&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;pre dir="ltr"&gt;&lt;code dir="ltr"&gt;&lt;span&gt;article&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;{&lt;/span&gt;
&lt;span&gt;  &lt;/span&gt;&lt;span&gt;display&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;grid&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;  &lt;/span&gt;&lt;span&gt;grid-template&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;
&lt;span&gt;    &lt;/span&gt;&lt;span&gt;'image'&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;auto&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;'title'&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;auto&lt;/span&gt;
&lt;span&gt;    &lt;/span&gt;&lt;span&gt;'summary'&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;auto&lt;/span&gt;
&lt;span&gt;    &lt;/span&gt;&lt;span&gt;'button'&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;auto&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;auto&lt;/span&gt;
&lt;span&gt;  &lt;/span&gt;&lt;span&gt;;&lt;/span&gt;

&lt;span&gt;  &lt;/span&gt;&lt;span&gt;@container&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;(inline-size&lt;/span&gt;&lt;span&gt; &amp;gt; &lt;/span&gt;&lt;span&gt;40ch)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;{&lt;/span&gt;
&lt;span&gt;    &lt;/span&gt;&lt;span&gt;--image-ratio&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;    &lt;/span&gt;&lt;span&gt;grid-template&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;
&lt;span&gt;      &lt;/span&gt;&lt;span&gt;'image title'&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;auto&lt;/span&gt;
&lt;span&gt;      &lt;/span&gt;&lt;span&gt;'image summary'&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;fr&lt;/span&gt;
&lt;span&gt;      &lt;/span&gt;&lt;span&gt;'image button'&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;auto&lt;/span&gt;
&lt;span&gt;      &lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;minmax&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;50&lt;/span&gt;&lt;span&gt;px&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;min&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;20&lt;/span&gt;&lt;span&gt;%&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;500&lt;/span&gt;&lt;span&gt;px&lt;/span&gt;&lt;span&gt;))&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;1&lt;/span&gt;&lt;span&gt;fr&lt;/span&gt;
&lt;span&gt;    &lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;  &lt;/span&gt;&lt;span&gt;}&lt;/span&gt;

&lt;span&gt;  &lt;/span&gt;&lt;span&gt;@&lt;/span&gt;&lt;span&gt;container&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;inline-size&lt;/span&gt;&lt;span&gt; &amp;gt; &lt;/span&gt;&lt;span&gt;50ch&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;{&lt;/span&gt;
&lt;span&gt;    &lt;/span&gt;&lt;span&gt;grid-template&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;
&lt;span&gt;      &lt;/span&gt;&lt;span&gt;'image title title'&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;auto&lt;/span&gt;
&lt;span&gt;      &lt;/span&gt;&lt;span&gt;'image summary button'&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;1fr&lt;/span&gt;
&lt;span&gt;      &lt;/span&gt;&lt;span&gt;/&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;minmax&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;50px&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;min&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;20&lt;/span&gt;&lt;span&gt;%,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;500px&lt;/span&gt;&lt;span&gt;))&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;1fr&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;fit-content&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span&gt;20&lt;/span&gt;&lt;span&gt;%);&lt;/span&gt;
&lt;span&gt;  &lt;/span&gt;&lt;span&gt;}&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The initial goal of this post is that components should be able to respond to any context. To make that work, each component defines its own internal behavior, without explicit knowledge of the surrounding components. Container queries help us accomplish that. The &lt;code dir="ltr"&gt;product-list&lt;/code&gt; and &lt;code dir="ltr"&gt;product-detail&lt;/code&gt; components don't need to be aware of why they have more or less space in a given context, they only need to know &lt;em&gt;how much space&lt;/em&gt; is currently available.&lt;/p&gt;

&lt;h2 id="transition_grid_templates_and_visibility" tabindex="-1"&gt;&lt;span&gt;Transition grid templates and visibility&lt;/span&gt;&lt;/h2&gt;

&lt;p&gt;With a layout that's changing often based on container queries, how do we smooth out all of the transitions? When using grids for layout, it's possible to animate the size of a column or row, as well as the gap between columns and rows. In this case, the cart area and gap are expanded from &lt;code dir="ltr"&gt;0&lt;/code&gt; width when the cart is opened. In order to animate grid templates like this, two things are required:&lt;/p&gt;

&lt;ol&gt;&lt;li&gt;The initial and end states must have the same number of tracks (columns or rows).&lt;/li&gt;
&lt;li&gt;The animated tracks must use comparable units.&lt;/li&gt;
&lt;/ol&gt;&lt;p&gt;While it would be possible to change from a one-column grid to a two-column grid when the sidebar is hidden, the empty sidebar column is instead resized to &lt;code dir="ltr"&gt;0&lt;/code&gt;, and the column-gap is also set to &lt;code dir="ltr"&gt;0&lt;/code&gt;. When the cart is open in the sidebar, the &lt;code dir="ltr"&gt;0&lt;/code&gt;-width column transitions to &lt;code dir="ltr"&gt;calc(15em + 1cqi)&lt;/code&gt;. Since the calculation results in a normal length value, the transition can be animated from length &lt;code dir="ltr"&gt;0&lt;/code&gt;. The gap also animates from one length to another—from &lt;code dir="ltr"&gt;0&lt;/code&gt; to &lt;code dir="ltr"&gt;var(--gap)&lt;/code&gt;—which was defined earlier:&lt;/p&gt;
&lt;p&gt;&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;pre dir="ltr"&gt;&lt;code dir="ltr"&gt;&lt;span&gt;main&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;{&lt;/span&gt;
&lt;span&gt;  &lt;/span&gt;&lt;span&gt;transition&lt;/span&gt;&lt;span&gt;:&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;grid-template&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;250&lt;/span&gt;&lt;span&gt;ms&lt;/span&gt;&lt;span&gt;,&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;gap&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;250&lt;/span&gt;&lt;span&gt;ms&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;
&lt;span&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="non-baseline_animation_enhancements" tabindex="-1"&gt;&lt;span&gt;Non-Baseline animation enhancements&lt;/span&gt;&lt;/h3&gt;

&lt;p&gt;The &lt;code dir="ltr"&gt;shopping-cart&lt;/code&gt; and &lt;code dir="ltr"&gt;product-detail&lt;/code&gt; components are also animated when hidden or shown, and this is done using two recent features that are not yet Baseline, but work as progressive enhancements for browsers that do have support:&lt;/p&gt;

&lt;ol&gt;&lt;li&gt;Applying &lt;code dir="ltr"&gt;interpolate-size: allow-keywords&lt;/code&gt; allows &lt;a href="https://developer.chrome.com/docs/css-ui/animate-to-height-auto#animate_to_and_from_intrinsic_sizing_keywords_with_interpolate-size"&gt;transitioning element dimensions from &lt;code dir="ltr"&gt;0&lt;/code&gt; to &lt;code dir="ltr"&gt;auto&lt;/code&gt;&lt;/a&gt;. This is used for transitioning the products to and from a &lt;code dir="ltr"&gt;block-size&lt;/code&gt; of &lt;code dir="ltr"&gt;0&lt;/code&gt; when they are added or removed from the cart. Since the &lt;code dir="ltr"&gt;interpolate-size&lt;/code&gt; property inherits, that only needs to be defined once on the &lt;code dir="ltr"&gt;&amp;lt;html&amp;gt;&lt;/code&gt; element, and it is available to every other element on the page. This is only supported in Chrome-based browsers (Chrome, Edge, and others), but can be used as a progressive enhancement. The fallback works as expected, just without the animated transition.&lt;br&gt;&lt;/li&gt;
&lt;li&gt;"Discrete" properties like &lt;code dir="ltr"&gt;display&lt;/code&gt; can now be transitioned as well, even though there are no intermediate values between &lt;code dir="ltr"&gt;grid&lt;/code&gt; and &lt;code dir="ltr"&gt;none&lt;/code&gt;. Instead the transition is applied at the start or end of the duration provided. To achieve that, &lt;code dir="ltr"&gt;allow-discrete&lt;/code&gt; is added to the &lt;code dir="ltr"&gt;transition-behavior&lt;/code&gt; property. While &lt;code dir="ltr"&gt;transition-behavior&lt;/code&gt; is otherwise Baseline, animating the &lt;code dir="ltr"&gt;display&lt;/code&gt; property is not yet supported in Firefox. But again, this works well as a progressive enhancement.&lt;br&gt;&lt;/li&gt;
&lt;/ol&gt;&lt;p&gt;There are many situations where container queries and units can be used to replace media queries and viewport units, and that's great when it helps you express the intent of a design more clearly. But one is not meant to replace the other, and there's not a &lt;em&gt;better&lt;/em&gt; query or unit that will work in every situation. CSS works best when the relationships established in code match the goals and purpose of the design. When you want text and spacing that is relative to immediate context, container queries provide that functionality, but when you want consistent sizing relative to the overall viewport, media queries and viewport units are still an excellent option. Most websites will likely involve a mix of both.&lt;/p&gt;
  

  
&lt;/div&gt;

  
    
    
      
    &lt;/div&gt;&lt;/div&gt;&lt;/body&gt;&lt;/html&gt;</content>
        <author>
            <name/>
        </author>
        <media:content medium="image" url="https://web.dev/static/articles/baseline-in-action-container-queries/image/thumbnail.png"/>
    </entry>
</feed>