[{"data":1,"prerenderedAt":2137},["ShallowReactive",2],{"project:\u002Ffui-content:en-us":3,"project:prev:\u002Ffui-content:en-us":2131,"project:next:\u002Ffui-content:en-us":2134},{"id":4,"title":5,"body":6,"category":2099,"company":2102,"cover":2103,"date":2104,"description":2105,"extension":2106,"featured":403,"image":2107,"isPublic":403,"links":2108,"meta":2112,"navigation":403,"order":911,"path":2113,"period":2114,"role":2115,"seo":2118,"stack":2119,"status":2122,"stem":2123,"tags":2124,"__hash__":2130},"projects_enUS\u002Fen-us\u002Fprojects\u002Ffui-content.md","fui-content",{"type":7,"value":8,"toc":2077},"minimark",[9,30,33,38,176,203,207,218,266,269,333,359,363,376,468,521,524,574,578,589,592,964,982,1167,1174,1178,1193,1196,1264,1291,1321,1333,1342,1349,1353,1367,1378,1383,1394,1410,1443,1447,1454,1464,1468,1471,1481,1485,1499,1509,1525,1529,1536,1543,1554,1557,1620,1623,1627,1700,1704,1707,1779,1819,1822,1839,1874,1880,1907,1910,2021,2034,2038,2041,2044,2048,2066,2073],[10,11,12,13,17,18,21,22,24,25,29],"p",{},"Everything else in this portfolio — every ",[14,15,16],"code",{},"fui-btn",", every ",[14,19,20],{},"fui-card",", every theme switch in the morphism picker up top — comes from here. ",[14,23,5],{}," is the SCSS design system I started building in April 2026: a monorepo with tokens, utilities, and five complete visual morphisms, published as independent packages on npm. This is both the most technical and the most personal project in this portfolio — no client, no agency deadline, just the question that started it: can you have five completely different visual identities — glass, clay, extruded plastic, physical hardware, pure flat — by swapping ",[26,27,28],"strong",{},"a single import",", without duplicating HTML, without coupling to a framework, and without sacrificing WCAG contrast in any of them?",[10,31,32],{},"The answer, after more than 170 commits, is yes. And the process of getting there taught me more about modern CSS than any client project I've ever touched.",[34,35,37],"h2",{"id":36},"under-the-hood","Under the hood",[39,40,41,54],"table",{},[42,43,44],"thead",{},[45,46,47,51],"tr",{},[48,49,50],"th",{},"Layer",[48,52,53],{},"Technology",[55,56,57,82,95,108,121,139,152,163],"tbody",{},[45,58,59,63],{},[60,61,62],"td",{},"Language",[60,64,65,73,74,77,78,81],{},[66,67,72],"a",{"href":68,"rel":69,"target":71},"https:\u002F\u002Fsass-lang.com\u002Fdocumentation\u002F",[70],"nofollow","_blank","SCSS\u002FSass"," — native ",[14,75,76],{},"@use",", ",[14,79,80],{},"@forward",", functions and mixins",[45,83,84,87],{},[60,85,86],{},"Monorepo",[60,88,89,94],{},[66,90,93],{"href":91,"rel":92,"target":71},"https:\u002F\u002Fdocs.npmjs.com\u002Fcli\u002Fv10\u002Fusing-npm\u002Fworkspaces",[70],"npm workspaces"," — 5 independently published packages",[45,96,97,100],{},[60,98,99],{},"Distribution",[60,101,102,107],{},[66,103,106],{"href":104,"rel":105,"target":71},"https:\u002F\u002Fdocs.github.com\u002Fen\u002Fpackages",[70],"GitHub Packages"," — per-package private npm registry",[45,109,110,113],{},[60,111,112],{},"Tests",[60,114,115,120],{},[66,116,119],{"href":117,"rel":118,"target":71},"https:\u002F\u002Fwww.oddbird.net\u002Ftrue\u002F",[70],"sass-true"," via Jest — 307 tests, including computed WCAG contrast",[45,122,123,126],{},[60,124,125],{},"Quality",[60,127,128,133,134],{},[66,129,132],{"href":130,"rel":131,"target":71},"https:\u002F\u002Fstylelint.io\u002F",[70],"Stylelint"," + ",[66,135,138],{"href":136,"rel":137,"target":71},"https:\u002F\u002Fprettier.io\u002F",[70],"Prettier",[45,140,141,144],{},[60,142,143],{},"CI\u002FCD",[60,145,146,151],{},[66,147,150],{"href":148,"rel":149,"target":71},"https:\u002F\u002Fdocs.github.com\u002Fen\u002Factions",[70],"GitHub Actions"," — build\u002Flint\u002Ftest on every push, publish per tag",[45,153,154,157],{},[60,155,156],{},"Local preview",[60,158,159,160],{},"Docker + Traefik, served at ",[14,161,162],{},"fui.localhost",[45,164,165,168],{},[60,166,167],{},"Commits",[60,169,170,175],{},[66,171,174],{"href":172,"rel":173,"target":71},"https:\u002F\u002Fwww.conventionalcommits.org\u002F",[70],"Conventional Commits"," via Husky + commitlint",[10,177,178,179,182,183,186,187,190,191,194,195,77,197,77,199,202],{},"The core idea is simple to state and hard to sustain: ",[26,180,181],{},"one theme equals one morphism",". No component ever carries a morphism class (",[14,184,185],{},".fui-glass .fui-btn"," doesn't exist) — installing ",[14,188,189],{},"@allangboliveira\u002Fui-theme-glass"," and toggling ",[14,192,193],{},"data-theme"," is enough for the entire component vocabulary (",[14,196,16],{},[14,198,20],{},[14,200,201],{},"fui-input","...) to become glass. Switching morphism in production means swapping a package, not rewriting HTML.",[34,204,206],{"id":205},"six-layers-an-order-that-cant-break","Six layers, an order that can't break",[10,208,209,210,217],{},"The system's entire CSS is organized under ",[66,211,214],{"href":212,"rel":213,"target":71},"https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FCSS\u002F@layer",[70],[14,215,216],{},"@layer",", declared exactly once, in the very first file any consumer loads:",[219,220,225],"pre",{"className":221,"code":222,"language":223,"meta":224,"style":224},"language-scss shiki shiki-themes github-light-high-contrast github-dark-high-contrast","\u002F\u002F packages\u002Fbase\u002Flayout\u002F_grid.scss\n@layer fui.layout, fui.base, fui.components, fui.utilities;\n","scss","",[14,226,227,236],{"__ignoreMap":224},[228,229,232],"span",{"class":230,"line":231},"line",1,[228,233,235],{"class":234},"sLBg1","\u002F\u002F packages\u002Fbase\u002Flayout\u002F_grid.scss\n",[228,237,239,243,247,250,253,255,258,260,263],{"class":230,"line":238},2,[228,240,242],{"class":241},"suds8","@layer fui",[228,244,246],{"class":245},"s-5SL",".layout",[228,248,249],{"class":241},", fui",[228,251,252],{"class":245},".base",[228,254,249],{"class":241},[228,256,257],{"class":245},".components",[228,259,249],{"class":241},[228,261,262],{"class":245},".utilities",[228,264,265],{"class":241},";\n",[10,267,268],{},"The order between layers is the single most important rule in the whole project — and the easiest one to break by accident:",[270,271,272,279,295,301,314,320],"ol",{},[273,274,275,278],"li",{},[26,276,277],{},"tokens"," — compile-time Sass variables (palette, spacing, typography)",[273,280,281,284,285,77,288,77,291,294],{},[26,282,283],{},"utilities"," — atomic classes (",[14,286,287],{},"fui-bg-primary",[14,289,290],{},"fui-p-4",[14,292,293],{},"fui-shadow-md",")",[273,296,297,300],{},[26,298,299],{},"layout"," — grid, breakpoints, container",[273,302,303,306,307,310,311,294],{},[26,304,305],{},"themes"," — theme generation (",[14,308,309],{},"[data-theme=\"light\"]"," \u002F ",[14,312,313],{},"[data-theme=\"dark\"]",[273,315,316,319],{},[26,317,318],{},"base"," — reset and plain HTML element styles",[273,321,322,325,326,77,328,77,330,332],{},[26,323,324],{},"components"," — ",[14,327,16],{},[14,329,20],{},[14,331,201],{},"...",[10,334,335,337,338,342,343,345,346,349,350,352,353,358],{},[14,336,216],{}," resolves in ",[339,340,341],"em",{},"declaration"," order, not specificity — so once fixed, this list is a contract: if a theme package declared its own ",[14,344,216],{}," order different from the base's, the browser would resolve the cascade wrong depending on which ",[14,347,348],{},"\u003Clink>"," the parser reached first. (This exact category of bug — non-deterministic ",[14,351,216],{}," order — is the same class of problem I later solved on the consumer side, here on ao.dev, documented in the ",[66,354,357],{"href":355,"rel":356,"target":71},"https:\u002F\u002Fgithub.com\u002FAllanGBOliveira\u002Fao.dev",[70],"project's own CLAUDE.md"," as a \"real bleed bug on cold reload.\" A lesson from one project became structural prevention in the other.)",[34,360,362],{"id":361},"two-layer-tokens-sass-at-compile-time-css-custom-properties-at-runtime","Two-layer tokens — Sass at compile time, CSS custom properties at runtime",[10,364,365,366,371,372,375],{},"Every visual value in the system is born as a Sass variable and emitted as a ",[66,367,370],{"href":368,"rel":369,"target":71},"https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FCSS\u002F--*",[70],"CSS custom property",". The duality is deliberate: Sass resolves functions, map deep-merges, and contrast calculations ",[26,373,374],{},"before"," a single byte reaches the browser; the CSS var exists so a consumer can override at runtime without recompiling Sass.",[219,377,379],{"className":221,"code":378,"language":223,"meta":224,"style":224},"\u002F\u002F packages\u002Fbase\u002Ftokens\u002F_colors.scss — consumer customization\n$palette-overrides: () !default;\n\n\u002F\u002F ao.dev customizes it this way, in fui.scss:\n@use '@allangboliveira\u002Fui-themes-base' with (\n  $palette-overrides: (\"brand\": (500: #ff6b35, 600: #e55a2b))\n);\n",[14,380,381,386,398,405,411,426,462],{"__ignoreMap":224},[228,382,383],{"class":230,"line":231},[228,384,385],{"class":234},"\u002F\u002F packages\u002Fbase\u002Ftokens\u002F_colors.scss — consumer customization\n",[228,387,388,392,396],{"class":230,"line":238},[228,389,391],{"class":390},"soyes","$palette-overrides: () ",[228,393,395],{"class":394},"sHUrx","!default",[228,397,265],{"class":241},[228,399,401],{"class":230,"line":400},3,[228,402,404],{"emptyLinePlaceholder":403},true,"\n",[228,406,408],{"class":230,"line":407},4,[228,409,410],{"class":234},"\u002F\u002F ao.dev customizes it this way, in fui.scss:\n",[228,412,414,416,420,423],{"class":230,"line":413},5,[228,415,76],{"class":394},[228,417,419],{"class":418},"sT6z2"," '@allangboliveira\u002Fui-themes-base'",[228,421,422],{"class":394}," with",[228,424,425],{"class":241}," (\n",[228,427,429,432,435,438,440,443,446,449,451,454,456,459],{"class":230,"line":428},6,[228,430,431],{"class":390},"  $palette-overrides",[228,433,434],{"class":241},": (",[228,436,437],{"class":418},"\"brand\"",[228,439,434],{"class":241},[228,441,442],{"class":245},"500",[228,444,445],{"class":241},": ",[228,447,448],{"class":245},"#ff6b35",[228,450,77],{"class":241},[228,452,453],{"class":245},"600",[228,455,445],{"class":241},[228,457,458],{"class":245},"#e55a2b",[228,460,461],{"class":241},"))\n",[228,463,465],{"class":230,"line":464},7,[228,466,467],{"class":241},");\n",[219,469,473],{"className":470,"code":471,"language":472,"meta":224,"style":224},"language-css shiki shiki-themes github-light-high-contrast github-dark-high-contrast","\u002F* what reaches the browser, already resolved *\u002F\n:root {\n  --fui-color-primary: #7c3aed;\n  --fui-color-on-primary: #fafafa; \u002F* computed, not hardcoded — see next section *\u002F\n}\n","css",[14,474,475,480,488,500,516],{"__ignoreMap":224},[228,476,477],{"class":230,"line":231},[228,478,479],{"class":234},"\u002F* what reaches the browser, already resolved *\u002F\n",[228,481,482,485],{"class":230,"line":238},[228,483,484],{"class":245},":root",[228,486,487],{"class":241}," {\n",[228,489,490,493,495,498],{"class":230,"line":400},[228,491,492],{"class":390},"  --fui-color-primary",[228,494,445],{"class":241},[228,496,497],{"class":245},"#7c3aed",[228,499,265],{"class":241},[228,501,502,505,507,510,513],{"class":230,"line":407},[228,503,504],{"class":390},"  --fui-color-on-primary",[228,506,445],{"class":241},[228,508,509],{"class":245},"#fafafa",[228,511,512],{"class":241},"; ",[228,514,515],{"class":234},"\u002F* computed, not hardcoded — see next section *\u002F\n",[228,517,518],{"class":230,"line":413},[228,519,520],{"class":241},"}\n",[10,522,523],{},"Two customization levels coexist by design:",[525,526,527,544],"ul",{},[273,528,529,325,532,535,536,539,540,543],{},[26,530,531],{},"Sass compile-time",[14,533,534],{},"$palette-overrides"," and functions like ",[14,537,538],{},"shadow-color-for()","\u002F",[14,541,542],{},"text-color-for()",": automatic derivation, full recalculation span (text color, shadow color, everything readjusts).",[273,545,546,549,550,553,554,557,558,561,562,565,566,569,570,573],{},[26,547,548],{},"CSS runtime"," — overriding ",[14,551,552],{},"--fui-color-surface"," directly via ",[14,555,556],{},"\u003Cstyle>",": the cascade always wins, but only what has a ",[14,559,560],{},"color-mix()"," fallback (like shadows) readapts itself. Text does ",[26,563,564],{},"not"," — CSS still has no real supported ",[14,567,568],{},"color-contrast()",", so overriding surface color at runtime requires manually updating ",[14,571,572],{},"--fui-color-on-surface",". I documented this asymmetry explicitly so it wouldn't become a surprise for anyone consuming the library.",[34,575,577],{"id":576},"a-palette-with-guaranteed-wcag-contrast-computed-not-guessed","A palette with guaranteed WCAG contrast — computed, not guessed",[10,579,580,581,584,585,588],{},"This was the single most interesting problem in the whole project. Every design system palette carries the same risk: someone picks ",[14,582,583],{},"blue-500"," as text color over a ",[14,586,587],{},"blue-600"," background, it looks great in Figma, and it silently fails WCAG AA (4.5:1) until an accessibility audit catches it.",[10,590,591],{},"The fix here was to eliminate the guesswork entirely: a Sass function computes relative luminance and contrast ratio at compile time, and decides on its own whether text should be light or dark.",[219,593,595],{"className":221,"code":594,"language":223,"meta":224,"style":224},"\u002F\u002F packages\u002Fbase\u002Futilities\u002F_a11y.scss\n@function luminance($color) {\n  \u002F\u002F relative luminance formula (WCAG 2.1)\n  $r: red($color) \u002F 255;\n  $g: green($color) \u002F 255;\n  $b: blue($color) \u002F 255;\n  \u002F\u002F ... per-channel gamma correction\n  @return 0.2126 * $r + 0.7152 * $g + 0.0722 * $b;\n}\n\n@function contrast-ratio($fg, $bg) {\n  $l1: luminance($fg) + 0.05;\n  $l2: luminance($bg) + 0.05;\n  @return math.max($l1, $l2) \u002F math.min($l1, $l2);\n}\n\n@function text-color-for($bg) {\n  \u002F\u002F returns neutral-50 or neutral-950 — whichever has HIGHER contrast against $bg\n  $contrast-light: contrast-ratio($neutral-50, $bg);\n  $contrast-dark: contrast-ratio($neutral-950, $bg);\n  @return if($contrast-light > $contrast-dark, $neutral-50, $neutral-950);\n}\n",[14,596,597,602,619,624,648,670,692,697,735,740,745,765,790,812,857,862,867,881,887,909,930,959],{"__ignoreMap":224},[228,598,599],{"class":230,"line":231},[228,600,601],{"class":234},"\u002F\u002F packages\u002Fbase\u002Futilities\u002F_a11y.scss\n",[228,603,604,607,610,613,616],{"class":230,"line":238},[228,605,606],{"class":394},"@function",[228,608,609],{"class":245}," luminance",[228,611,612],{"class":241},"(",[228,614,615],{"class":390},"$color",[228,617,618],{"class":241},") {\n",[228,620,621],{"class":230,"line":400},[228,622,623],{"class":234},"  \u002F\u002F relative luminance formula (WCAG 2.1)\n",[228,625,626,629,631,634,636,638,641,643,646],{"class":230,"line":407},[228,627,628],{"class":390},"  $r",[228,630,445],{"class":241},[228,632,633],{"class":245},"red",[228,635,612],{"class":241},[228,637,615],{"class":390},[228,639,640],{"class":241},") ",[228,642,539],{"class":394},[228,644,645],{"class":245}," 255",[228,647,265],{"class":241},[228,649,650,653,655,658,660,662,664,666,668],{"class":230,"line":413},[228,651,652],{"class":390},"  $g",[228,654,445],{"class":241},[228,656,657],{"class":245},"green",[228,659,612],{"class":241},[228,661,615],{"class":390},[228,663,640],{"class":241},[228,665,539],{"class":394},[228,667,645],{"class":245},[228,669,265],{"class":241},[228,671,672,675,677,680,682,684,686,688,690],{"class":230,"line":428},[228,673,674],{"class":390},"  $b",[228,676,445],{"class":241},[228,678,679],{"class":245},"blue",[228,681,612],{"class":241},[228,683,615],{"class":390},[228,685,640],{"class":241},[228,687,539],{"class":394},[228,689,645],{"class":245},[228,691,265],{"class":241},[228,693,694],{"class":230,"line":464},[228,695,696],{"class":234},"  \u002F\u002F ... per-channel gamma correction\n",[228,698,700,703,706,709,712,715,718,720,723,725,728,730,733],{"class":230,"line":699},8,[228,701,702],{"class":394},"  @return",[228,704,705],{"class":245}," 0.2126",[228,707,708],{"class":394}," *",[228,710,711],{"class":390}," $r",[228,713,714],{"class":394}," +",[228,716,717],{"class":245}," 0.7152",[228,719,708],{"class":394},[228,721,722],{"class":390}," $g",[228,724,714],{"class":394},[228,726,727],{"class":245}," 0.0722",[228,729,708],{"class":394},[228,731,732],{"class":390}," $b",[228,734,265],{"class":241},[228,736,738],{"class":230,"line":737},9,[228,739,520],{"class":241},[228,741,743],{"class":230,"line":742},10,[228,744,404],{"emptyLinePlaceholder":403},[228,746,748,750,753,755,758,760,763],{"class":230,"line":747},11,[228,749,606],{"class":394},[228,751,752],{"class":245}," contrast-ratio",[228,754,612],{"class":241},[228,756,757],{"class":390},"$fg",[228,759,77],{"class":241},[228,761,762],{"class":390},"$bg",[228,764,618],{"class":241},[228,766,768,771,773,776,778,780,782,785,788],{"class":230,"line":767},12,[228,769,770],{"class":390},"  $l1",[228,772,445],{"class":241},[228,774,775],{"class":245},"luminance",[228,777,612],{"class":241},[228,779,757],{"class":390},[228,781,640],{"class":241},[228,783,784],{"class":394},"+",[228,786,787],{"class":245}," 0.05",[228,789,265],{"class":241},[228,791,793,796,798,800,802,804,806,808,810],{"class":230,"line":792},13,[228,794,795],{"class":390},"  $l2",[228,797,445],{"class":241},[228,799,775],{"class":245},[228,801,612],{"class":241},[228,803,762],{"class":390},[228,805,640],{"class":241},[228,807,784],{"class":394},[228,809,787],{"class":245},[228,811,265],{"class":241},[228,813,815,817,820,823,826,828,831,833,836,838,840,842,844,847,849,851,853,855],{"class":230,"line":814},14,[228,816,702],{"class":394},[228,818,819],{"class":390}," math",[228,821,822],{"class":241},".",[228,824,825],{"class":245},"max",[228,827,612],{"class":241},[228,829,830],{"class":390},"$l1",[228,832,77],{"class":241},[228,834,835],{"class":390},"$l2",[228,837,640],{"class":241},[228,839,539],{"class":394},[228,841,819],{"class":390},[228,843,822],{"class":241},[228,845,846],{"class":245},"min",[228,848,612],{"class":241},[228,850,830],{"class":390},[228,852,77],{"class":241},[228,854,835],{"class":390},[228,856,467],{"class":241},[228,858,860],{"class":230,"line":859},15,[228,861,520],{"class":241},[228,863,865],{"class":230,"line":864},16,[228,866,404],{"emptyLinePlaceholder":403},[228,868,870,872,875,877,879],{"class":230,"line":869},17,[228,871,606],{"class":394},[228,873,874],{"class":245}," text-color-for",[228,876,612],{"class":241},[228,878,762],{"class":390},[228,880,618],{"class":241},[228,882,884],{"class":230,"line":883},18,[228,885,886],{"class":234},"  \u002F\u002F returns neutral-50 or neutral-950 — whichever has HIGHER contrast against $bg\n",[228,888,890,893,895,898,900,903,905,907],{"class":230,"line":889},19,[228,891,892],{"class":390},"  $contrast-light",[228,894,445],{"class":241},[228,896,897],{"class":245},"contrast-ratio",[228,899,612],{"class":241},[228,901,902],{"class":390},"$neutral-50",[228,904,77],{"class":241},[228,906,762],{"class":390},[228,908,467],{"class":241},[228,910,912,915,917,919,921,924,926,928],{"class":230,"line":911},20,[228,913,914],{"class":390},"  $contrast-dark",[228,916,445],{"class":241},[228,918,897],{"class":245},[228,920,612],{"class":241},[228,922,923],{"class":390},"$neutral-950",[228,925,77],{"class":241},[228,927,762],{"class":390},[228,929,467],{"class":241},[228,931,933,935,938,940,943,946,949,951,953,955,957],{"class":230,"line":932},21,[228,934,702],{"class":394},[228,936,937],{"class":245}," if",[228,939,612],{"class":241},[228,941,942],{"class":390},"$contrast-light",[228,944,945],{"class":390}," >",[228,947,948],{"class":390}," $contrast-dark",[228,950,77],{"class":241},[228,952,902],{"class":390},[228,954,77],{"class":241},[228,956,923],{"class":390},[228,958,467],{"class":241},[228,960,962],{"class":230,"line":961},22,[228,963,520],{"class":241},[10,965,966,967,970,971,974,975,978,979,981],{},"The practical result: ",[26,968,969],{},"88 button variants with guaranteed contrast"," — 8 color scales × 11 steps (",[14,972,973],{},"50"," through ",[14,976,977],{},"950",") — every single one tested. The test suite (",[14,980,119],{}," via Jest) runs this check against every real combination:",[219,983,987],{"className":984,"code":985,"language":986,"meta":224,"style":224},"language-bash shiki shiki-themes github-light-high-contrast github-dark-high-contrast","$ npm test\n\n  A11y — button palette WCAG AA\n    Button palette — WCAG AA contrast (4.5:1)\n      neutral\n        ✓ neutral-50 has a compliant text choice\n        ✓ neutral-100 has a compliant text choice\n        ...\n      blue \u002F green \u002F red \u002F yellow \u002F purple \u002F ...\n        ✓ (11 steps each, all compliant)\n\nTests:       307 passed, 307 total\n","bash",[14,988,989,1000,1004,1024,1044,1049,1072,1089,1094,1125,1146,1150],{"__ignoreMap":224},[228,990,991,994,997],{"class":230,"line":231},[228,992,993],{"class":390},"$",[228,995,996],{"class":418}," npm",[228,998,999],{"class":418}," test\n",[228,1001,1002],{"class":230,"line":238},[228,1003,404],{"emptyLinePlaceholder":403},[228,1005,1006,1009,1012,1015,1018,1021],{"class":230,"line":400},[228,1007,1008],{"class":390},"  A11y",[228,1010,1011],{"class":418}," —",[228,1013,1014],{"class":418}," button",[228,1016,1017],{"class":418}," palette",[228,1019,1020],{"class":418}," WCAG",[228,1022,1023],{"class":418}," AA\n",[228,1025,1026,1029,1031,1033,1035,1038,1041],{"class":230,"line":407},[228,1027,1028],{"class":390},"    Button",[228,1030,1017],{"class":418},[228,1032,1011],{"class":418},[228,1034,1020],{"class":418},[228,1036,1037],{"class":418}," AA",[228,1039,1040],{"class":418}," contrast",[228,1042,1043],{"class":241}," (4.5:1)\n",[228,1045,1046],{"class":230,"line":413},[228,1047,1048],{"class":390},"      neutral\n",[228,1050,1051,1054,1057,1060,1063,1066,1069],{"class":230,"line":428},[228,1052,1053],{"class":390},"        ✓",[228,1055,1056],{"class":418}," neutral-50",[228,1058,1059],{"class":418}," has",[228,1061,1062],{"class":418}," a",[228,1064,1065],{"class":418}," compliant",[228,1067,1068],{"class":418}," text",[228,1070,1071],{"class":418}," choice\n",[228,1073,1074,1076,1079,1081,1083,1085,1087],{"class":230,"line":464},[228,1075,1053],{"class":390},[228,1077,1078],{"class":418}," neutral-100",[228,1080,1059],{"class":418},[228,1082,1062],{"class":418},[228,1084,1065],{"class":418},[228,1086,1068],{"class":418},[228,1088,1071],{"class":418},[228,1090,1091],{"class":230,"line":699},[228,1092,1093],{"class":245},"        ...\n",[228,1095,1096,1099,1102,1105,1107,1110,1112,1115,1117,1120,1122],{"class":230,"line":737},[228,1097,1098],{"class":390},"      blue",[228,1100,1101],{"class":418}," \u002F",[228,1103,1104],{"class":418}," green",[228,1106,1101],{"class":418},[228,1108,1109],{"class":418}," red",[228,1111,1101],{"class":418},[228,1113,1114],{"class":418}," yellow",[228,1116,1101],{"class":418},[228,1118,1119],{"class":418}," purple",[228,1121,1101],{"class":418},[228,1123,1124],{"class":418}," ...\n",[228,1126,1127,1129,1132,1135,1138,1141,1143],{"class":230,"line":742},[228,1128,1053],{"class":390},[228,1130,1131],{"class":241}," (11 ",[228,1133,1134],{"class":418},"steps",[228,1136,1137],{"class":418}," each,",[228,1139,1140],{"class":418}," all",[228,1142,1065],{"class":418},[228,1144,1145],{"class":241},")\n",[228,1147,1148],{"class":230,"line":747},[228,1149,404],{"emptyLinePlaceholder":403},[228,1151,1152,1155,1158,1161,1164],{"class":230,"line":767},[228,1153,1154],{"class":390},"Tests:",[228,1156,1157],{"class":245},"       307",[228,1159,1160],{"class":418}," passed,",[228,1162,1163],{"class":245}," 307",[228,1165,1166],{"class":418}," total\n",[10,1168,1169,1170,1173],{},"This isn't a visual snapshot test — it's a mathematical test running the exact contrast formula WCAG defines, against every real cell of the palette, every time someone runs ",[14,1171,1172],{},"npm test",". If I add a new color to the palette someday and it fails contrast at some step, CI catches it before I'd even notice visually.",[34,1175,1177],{"id":1176},"shadow-and-surface-the-problem-pure-black-was-hiding","Shadow and surface — the problem pure black was hiding",[10,1179,1180,1181,1184,1185,1188,1189,1192],{},"One of the project's first real bugs: in dark mode, ",[14,1182,1183],{},"box-shadow"," over ",[14,1186,1187],{},"neutral-900"," (near-pure black) is ",[26,1190,1191],{},"invisible"," — black on black, even with generous blur. Cards and buttons lost all visual elevation the moment a user switched to dark.",[10,1194,1195],{},"The fix wasn't blindly cranking up blur or opacity — it was recalibrating the surface steps themselves:",[39,1197,1198,1211],{},[42,1199,1200],{},[45,1201,1202,1205,1208],{},[48,1203,1204],{},"Role",[48,1206,1207],{},"Light",[48,1209,1210],{},"Dark",[55,1212,1213,1230,1247],{},[45,1214,1215,1220,1225],{},[60,1216,1217],{},[14,1218,1219],{},"surface",[60,1221,1222],{},[14,1223,1224],{},"neutral-50",[60,1226,1227],{},[14,1228,1229],{},"neutral-800",[45,1231,1232,1237,1242],{},[60,1233,1234],{},[14,1235,1236],{},"surface-alt",[60,1238,1239],{},[14,1240,1241],{},"neutral-100",[60,1243,1244],{},[14,1245,1246],{},"neutral-700",[45,1248,1249,1254,1259],{},[60,1250,1251],{},[14,1252,1253],{},"surface-raised",[60,1255,1256],{},[14,1257,1258],{},"#fff",[60,1260,1261],{},[14,1262,1263],{},"neutral-600",[10,1265,1266,1267,539,1269,1271,1272,1274,1275,1278,1279,1282,1283,1286,1287,1290],{},"Moving dark mode's base from ",[14,1268,1187],{},[14,1270,977],{}," to ",[14,1273,1229],{}," (a gray, not a near-black) opened up room for shadows to stand out against. In parallel, I created a semantic ",[14,1276,1277],{},"color-shadow"," token — light uses ",[14,1280,1281],{},"rgba(0,0,0,0.15)",", dark uses ",[14,1284,1285],{},"rgba(0,0,0,0.55)"," — because the same shadow opacity that's subtle over white becomes imperceptible over dark gray. A function (",[14,1288,1289],{},"shadow-color-for($bg)",") derives that opacity automatically from the background's luminance, with specific thresholds:",[219,1292,1294],{"className":221,"code":1293,"language":223,"meta":224,"style":224},"\u002F\u002F luminance \u003C 0.03 → opacity 0.55\n\u002F\u002F luminance \u003C 0.10 → opacity 0.45\n\u002F\u002F luminance \u003C 0.30 → opacity 0.30\n\u002F\u002F luminance \u003C 0.65 → opacity 0.20\n\u002F\u002F otherwise         → opacity 0.15\n",[14,1295,1296,1301,1306,1311,1316],{"__ignoreMap":224},[228,1297,1298],{"class":230,"line":231},[228,1299,1300],{"class":234},"\u002F\u002F luminance \u003C 0.03 → opacity 0.55\n",[228,1302,1303],{"class":230,"line":238},[228,1304,1305],{"class":234},"\u002F\u002F luminance \u003C 0.10 → opacity 0.45\n",[228,1307,1308],{"class":230,"line":400},[228,1309,1310],{"class":234},"\u002F\u002F luminance \u003C 0.30 → opacity 0.30\n",[228,1312,1313],{"class":230,"line":407},[228,1314,1315],{"class":234},"\u002F\u002F luminance \u003C 0.65 → opacity 0.20\n",[228,1317,1318],{"class":230,"line":413},[228,1319,1320],{"class":234},"\u002F\u002F otherwise         → opacity 0.15\n",[10,1322,1323,1324,1326,1327,1329,1330,1332],{},"And as a safety net for whenever a consumer overrides ",[14,1325,552],{}," via plain CSS without knowing about the Sass function, every ",[14,1328,1183],{}," in the system carries a ",[14,1331,560],{}," fallback:",[219,1334,1336],{"className":470,"code":1335,"language":472,"meta":224,"style":224},"--fui-shadow-md: 0 4px 6px -1px var(--fui-color-shadow, color-mix(in srgb, var(--fui-color-surface) 75%, black));\n",[14,1337,1338],{"__ignoreMap":224},[228,1339,1340],{"class":230,"line":231},[228,1341,1335],{"class":241},[10,1343,1344,1345,1348],{},"If ",[14,1346,1347],{},"--fui-color-shadow"," doesn't exist, the browser derives a plausible shadow straight from the surface color itself — never black-on-black, even under last-minute customization.",[34,1350,1352],{"id":1351},"five-morphisms-the-same-component-light-and-dark-at-once","Five morphisms, the same component, light and dark at once",[1354,1355,1356],"blockquote",{},[10,1357,1358,1359,1362,1363,1366],{},"Installing and importing the package already applies the morphism to every component. No ",[14,1360,1361],{},".fui-glass",", no ",[14,1364,1365],{},".fui-skeu"," in the HTML — the theme is the package, not a class.",[10,1368,1369,1370,1373,1374,1377],{},"Each morphism is its own package (",[14,1371,1372],{},"@allangboliveira\u002Fui-theme-{glass,clay,neu,skeu}","), with full component parity: button, card, input, checkbox, radio, select, tabs, navbar, pagination, tag, content. The exact same HTML markup changes its entire identity depending on which package is installed. Every screenshot below is the same section of the system's test page — the button's states and semantic-colors tables — with ",[26,1375,1376],{},"light and dark side by side in the same image",", because comparing both modes at once is the most honest visual test a multi-theme design system can get.",[1379,1380,1382],"h3",{"id":1381},"skeuomorphism-physical-hardware","Skeuomorphism — physical hardware",[10,1384,1385,1386,1389,1390,1393],{},"Side-lit perspective, like a physical keyboard key: convex gradient, four shadow layers for smooth transitions between states, and a real \"pressed\" signal — ",[14,1387,1388],{},"translateY(1px)"," combined with ",[14,1391,1392],{},"border-style: inset"," and a flipped gradient, not just a smaller shadow.",[1395,1396,1399,1407],"figure",{"className":1397},[1398],"rehype-figure",[1400,1401],"img",{"alt":1402,"height":1403,"src":1404,"title":1405,"width":1406},"Skeuomorphic button with convex gradient, captured in light and dark side by side on the same test screen",675,"\u002Fimages\u002Fpages\u002Fprojects\u002Ffui-content\u002Ffui-content-skeu.png","Skeuomorphism — light and dark",1200,[1408,1409,1402],"figcaption",{},[10,1411,1412,1413,1416,1417,1420,1421,77,1424,77,1427,77,1430,77,1433,77,1436,1438,1439,1442],{},"The skeu card goes further than the button: it simulates a physical 3×5 inch index card, with ",[26,1414,1415],{},"horizontal ruled lines on by default"," (",[14,1418,1419],{},"fui-card-lined",") and paper-color variants (",[14,1422,1423],{},"new",[14,1425,1426],{},"cream",[14,1428,1429],{},"aged",[14,1431,1432],{},"dirty",[14,1434,1435],{},"yellow",[14,1437,679],{},") — including an optional vertical margin line (",[14,1440,1441],{},"fui-card-margin",", the \"college-ruled notebook\" effect).",[1379,1444,1446],{"id":1445},"glassmorphism-glass-and-translucency","Glassmorphism — glass and translucency",[10,1448,1449,1450,1453],{},"Real ",[14,1451,1452],{},"backdrop-filter: blur()",", opacity calibrated per component — 15% on the card versus 50% on the button, because a card's reading area needs more background transparency without losing legibility of overlaid text.",[1395,1455,1457,1462],{"className":1456},[1398],[1400,1458],{"alt":1459,"height":1403,"src":1460,"title":1461,"width":1406},"Translucent glassmorphic button with backdrop-filter, captured in light and dark side by side","\u002Fimages\u002Fpages\u002Fprojects\u002Ffui-content\u002Ffui-content-glass.png","Glassmorphism — light and dark",[1408,1463,1459],{},[1379,1465,1467],{"id":1466},"claymorphism-inflated-clay","Claymorphism — inflated clay",[10,1469,1470],{},"Colored HSL shadows (not gray) derived from the component's own color, generous border radii (26–50px), and a progressive depth scale — the \"inflated\" effect that gives the morphism its name.",[1395,1472,1474,1479],{"className":1473},[1398],[1400,1475],{"alt":1476,"height":1403,"src":1477,"title":1478,"width":1406},"Claymorphic button with colored shadow and generously rounded corners, captured in light and dark side by side","\u002Fimages\u002Fpages\u002Fprojects\u002Ffui-content\u002Ffui-content-clay.png","Claymorphism — light and dark",[1408,1480,1476],{},[1379,1482,1484],{"id":1483},"neumorphism-extruded-plastic","Neumorphism — extruded plastic",[10,1486,1487,1488,1491,1492,1495,1496,1498],{},"Top-down perspective (camera from above, not from the side — which is why the active state has ",[26,1489,1490],{},"no"," ",[14,1493,1494],{},"translateY",", unlike skeu). Background must equal the surface color, with a dual shadow via ",[14,1497,560],{}," simulating light from a single angle.",[1395,1500,1502,1507],{"className":1501},[1398],[1400,1503],{"alt":1504,"height":1403,"src":1505,"title":1506,"width":1406},"Neumorphic button extruded from its own background, captured in light and dark side by side","\u002Fimages\u002Fpages\u002Fprojects\u002Ffui-content\u002Ffui-content-neu.png","Neumorphism — light and dark",[1408,1508,1504],{},[10,1510,1511,1512,1515,1516,1518,1519,1521,1522,1524],{},"The test page itself documents a real limitation of this morphism, right in a visible warning above the semantic-colors table: in the ",[14,1513,1514],{},"soft"," and ",[14,1517,26],{}," variants, the background is always transparent and the role's own color becomes the text color — and not every ",[14,1520,1514],{}," (step 400) or ",[14,1523,26],{}," (desaturated) combination passes WCAG AA. The documented recommendation is to use the standard tier when guaranteed contrast matters more than visual variation. I'd rather leave that limitation exposed than hide it behind a component that looks perfect and isn't.",[1379,1526,1528],{"id":1527},"flat-the-base-without-morphism","Flat — the base without morphism",[10,1530,1531,1532,1535],{},"With no theme package installed, ",[14,1533,1534],{},"@allangboliveira\u002Fui-themes-base"," alone is already a complete, usable design system — flat, no decorative shadow, just tokens and the base. It's the morphism almost nobody consciously \"picks,\" but it needs to work flawlessly on its own, because it's the floor all four others are built on. Its screenshot is the one that opens this case study, over in this page's sidebar.",[34,1537,1539,1542],{"id":1538},"border-aware-padding-when-the-border-steals-space-from-the-padding",[14,1540,1541],{},"border-aware-padding"," — when the border steals space from the padding",[10,1544,1545,1546,1549,1550,1553],{},"A subtle consistency bug: two buttons with the exact same declared ",[14,1547,1548],{},"padding",", one with no border and one with ",[14,1551,1552],{},"border: 2px solid",", render with visibly different inner areas — the border \"eats\" padding space, so the thicker-bordered button's text looks more cramped.",[10,1555,1556],{},"The fix became a mixin used in every component with a border that varies between morphisms:",[219,1558,1560],{"className":221,"code":1559,"language":223,"meta":224,"style":224},"@mixin border-aware-padding($padding-base, $border-ref, $border-actual) {\n  padding: calc(#{$padding-base} + #{$border-ref} - #{$border-actual});\n}\n",[14,1561,1562,1588,1616],{"__ignoreMap":224},[228,1563,1564,1567,1571,1573,1576,1578,1581,1583,1586],{"class":230,"line":231},[228,1565,1566],{"class":394},"@mixin",[228,1568,1570],{"class":1569},"sKwhi"," border-aware-padding",[228,1572,612],{"class":241},[228,1574,1575],{"class":390},"$padding-base",[228,1577,77],{"class":241},[228,1579,1580],{"class":390},"$border-ref",[228,1582,77],{"class":241},[228,1584,1585],{"class":390},"$border-actual",[228,1587,618],{"class":241},[228,1589,1590,1593,1595,1598,1600,1603,1605,1608,1611,1614],{"class":230,"line":238},[228,1591,1592],{"class":245},"  padding",[228,1594,445],{"class":241},[228,1596,1597],{"class":245},"calc",[228,1599,612],{"class":241},[228,1601,1602],{"class":390},"#{$padding-base}",[228,1604,714],{"class":394},[228,1606,1607],{"class":390}," #{$border-ref}",[228,1609,1610],{"class":394}," -",[228,1612,1613],{"class":390}," #{$border-actual}",[228,1615,467],{"class":241},[228,1617,1618],{"class":230,"line":400},[228,1619,520],{"class":241},[10,1621,1622],{},"The formula compensates for the difference between a reference border (the one the design was calibrated for) and the current morphism's actual border — skeu and clay tend to use thicker borders for the relief effect, glass barely uses a border at all. Without this mixin, every morphism would need a manually recalibrated padding value; with it, the same spacing token produces the same perceived inner area across any morphism.",[34,1624,1626],{"id":1625},"actually-tested-not-just-compiled","Actually tested, not just compiled",[525,1628,1629,1657,1663],{},[273,1630,1631,1634,1635,1638,1639,77,1642,77,1645,1648,1649,1652,1653,1656],{},[26,1632,1633],{},"307 tests"," via ",[66,1636,119],{"href":117,"rel":1637,"target":71},[70]," running inside Jest — pure functions (",[14,1640,1641],{},"px-to-rem",[14,1643,1644],{},"map-range",[14,1646,1647],{},"fluid","), mixins (",[14,1650,1651],{},"transition"," respecting ",[14,1654,1655],{},"prefers-reduced-motion","), tokens (palette, opacity, radius, typography), and the WCAG contrast tests already described.",[273,1658,1659,1662],{},[26,1660,1661],{},"Stylelint + Prettier"," enforced in CI — SCSS formatting and conventions don't depend on manual review.",[273,1664,1665,1668,1669,1672,1673,1676,1677,1679,1680,1683,1684,1687,1688,1691,1692,1695,1696,1699],{},[26,1666,1667],{},"IBM Equal Access"," drove real accessibility fixes in the library itself, not just in the consumer: indicators that relied solely on ",[14,1670,1671],{},"background-image"," gained a ",[14,1674,1675],{},"forced-colors: active"," fallback (Windows High Contrast Mode doesn't render decorative ",[14,1678,1671],{},"), and ",[14,1681,1682],{},"_content.scss"," never declares ",[14,1685,1686],{},"background-color"," or ",[14,1689,1690],{},"color"," on ",[14,1693,1694],{},"\u003Cpre>"," — that responsibility stays ",[26,1697,1698],{},"exclusively"," with Shiki on the consumer side, the exact same division-of-responsibility rule documented here on ao.dev.",[34,1701,1703],{"id":1702},"cicd-deploy-order-is-a-rule-not-a-suggestion","CI\u002FCD — deploy order is a rule, not a suggestion",[10,1705,1706],{},"The monorepo publishes five independent packages, each with its own version tag:",[39,1708,1709,1719],{},[42,1710,1711],{},[45,1712,1713,1716],{},[48,1714,1715],{},"Tag",[48,1717,1718],{},"Publishes",[55,1720,1721,1732,1743,1755,1767],{},[45,1722,1723,1728],{},[60,1724,1725],{},[14,1726,1727],{},"base\u002Fv0.3.0-canary.36",[60,1729,1730],{},[14,1731,1534],{},[45,1733,1734,1739],{},[60,1735,1736],{},[14,1737,1738],{},"theme-glass\u002Fv0.2.0-canary.17",[60,1740,1741],{},[14,1742,189],{},[45,1744,1745,1750],{},[60,1746,1747],{},[14,1748,1749],{},"theme-clay\u002Fv0.2.0-canary.18",[60,1751,1752],{},[14,1753,1754],{},"@allangboliveira\u002Fui-theme-clay",[45,1756,1757,1762],{},[60,1758,1759],{},[14,1760,1761],{},"theme-neu\u002Fv0.2.0-canary.18",[60,1763,1764],{},[14,1765,1766],{},"@allangboliveira\u002Fui-theme-neu",[45,1768,1769,1774],{},[60,1770,1771],{},[14,1772,1773],{},"theme-skeu\u002Fv0.2.0-canary.17",[60,1775,1776],{},[14,1777,1778],{},"@allangboliveira\u002Fui-theme-skeu",[219,1780,1782],{"className":984,"code":1781,"language":986,"meta":224,"style":224},"git tag base\u002Fv0.3.0 && git push origin base\u002Fv0.3.0\n# triggers the \"CD — Publish\" workflow, which runs lint + test\n# + a compile check before any `npm publish`\n",[14,1783,1784,1809,1814],{"__ignoreMap":224},[228,1785,1786,1789,1792,1795,1798,1800,1803,1806],{"class":230,"line":231},[228,1787,1788],{"class":390},"git",[228,1790,1791],{"class":418}," tag",[228,1793,1794],{"class":418}," base\u002Fv0.3.0",[228,1796,1797],{"class":241}," && ",[228,1799,1788],{"class":390},[228,1801,1802],{"class":418}," push",[228,1804,1805],{"class":418}," origin",[228,1807,1808],{"class":418}," base\u002Fv0.3.0\n",[228,1810,1811],{"class":230,"line":238},[228,1812,1813],{"class":234},"# triggers the \"CD — Publish\" workflow, which runs lint + test\n",[228,1815,1816],{"class":230,"line":400},[228,1817,1818],{"class":234},"# + a compile check before any `npm publish`\n",[10,1820,1821],{},"Two discipline rules I learned the hard way, not from a manual:",[270,1823,1824,1833],{},[273,1825,1826,1829,1830,1832],{},[26,1827,1828],{},"A single commit for any version bump across packages that depend on each other"," — avoids a published theme pointing at a ",[14,1831,318],{}," version that doesn't exist in the registry yet.",[273,1834,1835,1838],{},[26,1836,1837],{},"No more than two morphisms published in parallel"," — publishing all four at once multiplies the surface area for something to go wrong in a single window, with no real speed gain.",[10,1840,1841,1842,1845,1846,1849,1850,1853,1854,77,1857,77,1860,77,1863,1866,1867,1870,1871,1873],{},"Every push (not just to ",[14,1843,1844],{},"main",") already runs the ",[14,1847,1848],{},"Compile, Lint & Test"," job — compiling all five packages with ",[14,1851,1852],{},"sass",", checking formatting, lint, and the 307 tests. No ",[14,1855,1856],{},"feat\u002F",[14,1858,1859],{},"fix\u002F",[14,1861,1862],{},"chore\u002F",[14,1864,1865],{},"refactor\u002F",", or ",[14,1868,1869],{},"docs\u002F"," branch escapes it. And since April, an active ruleset on the repository blocks direct pushes to ",[14,1872,1844],{}," — even for me: any change, even a documentation one, has to go through a Pull Request with a green check. It's the same discipline I ask for on any client project, just automated and non-negotiable here.",[34,1875,1877,1879],{"id":1876},"fui-content-the-wrapper-the-project-is-named-after",[14,1878,5],{}," — the wrapper the project is named after",[10,1881,1882,1883,1886,1887,77,1889,77,1891,77,1893,77,1895,77,1898,77,1901,1903,1904,822],{},"The entire design system's name comes from a single class: ",[14,1884,1885],{},".fui-content",", the reading-context wrapper that styles any HTML generated by a CMS, rich editor, or Markdown pipeline — ",[14,1888,34],{},[14,1890,10],{},[14,1892,219],{},[14,1894,1354],{},[14,1896,1897],{},"kbd",[14,1899,1900],{},"mark",[14,1902,1395],{},", all of it, with no dependency on any framework name. It's literally what renders this very paragraph you're reading right now, inside ao.dev's ",[14,1905,1906],{},"\u003Carticle class=\"fui-content\">",[10,1908,1909],{},"Every visual property of the wrapper is a CSS custom property, and morphisms only ever override the variables — never a selector, never a property directly:",[219,1911,1913],{"className":221,"code":1912,"language":223,"meta":224,"style":224},".fui-content {\n  --fui-content-code-bg:        var(--fui-color-surface-alt);\n  --fui-content-code-shadow:    none;   \u002F\u002F morphism hook\n  --fui-content-code-backdrop:  none;   \u002F\u002F morphism hook (glass)\n  --fui-content-quote-accent:   var(--fui-color-primary);\n  --fui-content-mark-bg-image:  none;   \u002F\u002F morphism hook (\"ink\" gradient)\n  --fui-content-hr-shadow:      none;   \u002F\u002F morphism hook (groove effect)\n}\n",[14,1914,1915,1921,1939,1956,1971,1988,2002,2017],{"__ignoreMap":224},[228,1916,1917,1919],{"class":230,"line":231},[228,1918,1885],{"class":245},[228,1920,487],{"class":241},[228,1922,1923,1926,1929,1932,1934,1937],{"class":230,"line":238},[228,1924,1925],{"class":390},"  --fui-content-code-bg",[228,1927,1928],{"class":241},":        ",[228,1930,1931],{"class":245},"var",[228,1933,612],{"class":241},[228,1935,1936],{"class":390},"--fui-color-surface-alt",[228,1938,467],{"class":241},[228,1940,1941,1944,1947,1950,1953],{"class":230,"line":400},[228,1942,1943],{"class":390},"  --fui-content-code-shadow",[228,1945,1946],{"class":241},":    ",[228,1948,1949],{"class":245},"none",[228,1951,1952],{"class":241},";   ",[228,1954,1955],{"class":234},"\u002F\u002F morphism hook\n",[228,1957,1958,1961,1964,1966,1968],{"class":230,"line":407},[228,1959,1960],{"class":390},"  --fui-content-code-backdrop",[228,1962,1963],{"class":241},":  ",[228,1965,1949],{"class":245},[228,1967,1952],{"class":241},[228,1969,1970],{"class":234},"\u002F\u002F morphism hook (glass)\n",[228,1972,1973,1976,1979,1981,1983,1986],{"class":230,"line":413},[228,1974,1975],{"class":390},"  --fui-content-quote-accent",[228,1977,1978],{"class":241},":   ",[228,1980,1931],{"class":245},[228,1982,612],{"class":241},[228,1984,1985],{"class":390},"--fui-color-primary",[228,1987,467],{"class":241},[228,1989,1990,1993,1995,1997,1999],{"class":230,"line":428},[228,1991,1992],{"class":390},"  --fui-content-mark-bg-image",[228,1994,1963],{"class":241},[228,1996,1949],{"class":245},[228,1998,1952],{"class":241},[228,2000,2001],{"class":234},"\u002F\u002F morphism hook (\"ink\" gradient)\n",[228,2003,2004,2007,2010,2012,2014],{"class":230,"line":464},[228,2005,2006],{"class":390},"  --fui-content-hr-shadow",[228,2008,2009],{"class":241},":      ",[228,2011,1949],{"class":245},[228,2013,1952],{"class":241},[228,2015,2016],{"class":234},"\u002F\u002F morphism hook (groove effect)\n",[228,2018,2019],{"class":230,"line":699},[228,2020,520],{"class":241},[10,2022,2023,2024,2027,2028,2030,2031,2033],{},"Every ",[14,2025,2026],{},"\u002F\u002F morphism hook"," is a var that only gets a real value inside each theme's specific ",[14,2029,1682],{}," file — the base declares the variable empty, the morphism fills it in. ",[14,2032,1885],{}," never needs to know which morphism is active; it only exposes the extension points.",[34,2035,2037],{"id":2036},"what-it-became-aodev","What it became: ao.dev",[10,2039,2040],{},"This project and the portfolio you're browsing were, in practice, the same project from the start — every component I finish here unlocks a real page on ao.dev. Priority order was never \"what's most interesting to build,\" it was always \"what does the next portfolio page need\": Button first (used everywhere), Card next (every listing), Badge\u002FTag (inside every card), Navbar, form fields, Timeline (the About page), Meta block and Breadcrumb (case study), Code block (technical blog), Avatar.",[10,2042,2043],{},"The morphism switcher at the top of this very site — the gear icon that swaps between skeu, glass, neu, clay, and flat in real time — is the most honest acceptance test the library could have: if switching theme here, live, with no reload, no flash, and no broken contrast in any of the five, the design system is doing exactly what it promised.",[34,2045,2047],{"id":2046},"whats-still-missing","What's still missing",[10,2049,2050,2051,2054,2055,539,2058,2061,2062,2065],{},"The documented roadmap includes an optional \"pro\" layer — procedural textures via ",[14,2052,2053],{},"filter"," + SVG (",[14,2056,2057],{},"feTurbulence",[14,2059,2060],{},"feDisplacementMap",") applied on a pseudo-element: frosted grain for glass, plastic\u002Frubber texture for neu, metal\u002Fleather\u002Fwood for skeu, fine grain for clay. None of it is implemented yet — the deliberate call was to finish component parity across all five morphisms first, and only then consider the texture layer as an optional add-on package (",[14,2063,2064],{},".fui-texture","), without touching base behavior.",[10,2067,2068,2069,2072],{},"This repository was marked private since its very first commit — part of the process of publishing this portfolio was pulling this library out of the closet: dropping ",[14,2070,2071],{},"private",", rotating the tokens that only made sense while nobody else could see the code, and open-sourcing the design system that literally powers the page you're reading right now.",[2074,2075,2076],"style",{},"html pre.shiki code .sLBg1, html code.shiki .sLBg1{--shiki-default:#66707B;--shiki-dark:#BDC4CC}html pre.shiki code .suds8, html code.shiki .suds8{--shiki-default:#0E1116;--shiki-dark:#F0F3F6}html pre.shiki code .s-5SL, html code.shiki .s-5SL{--shiki-default:#023B95;--shiki-dark:#91CBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .soyes, html code.shiki .soyes{--shiki-default:#702C00;--shiki-dark:#FFB757}html pre.shiki code .sHUrx, html code.shiki .sHUrx{--shiki-default:#A0111F;--shiki-dark:#FF9492}html pre.shiki code .sT6z2, html code.shiki .sT6z2{--shiki-default:#032563;--shiki-dark:#ADDCFF}html pre.shiki code .sKwhi, html code.shiki .sKwhi{--shiki-default:#622CBC;--shiki-dark:#DBB7FF}",{"title":224,"searchDepth":238,"depth":238,"links":2078},[2079,2080,2081,2082,2083,2084,2091,2093,2094,2095,2097,2098],{"id":36,"depth":238,"text":37},{"id":205,"depth":238,"text":206},{"id":361,"depth":238,"text":362},{"id":576,"depth":238,"text":577},{"id":1176,"depth":238,"text":1177},{"id":1351,"depth":238,"text":1352,"children":2085},[2086,2087,2088,2089,2090],{"id":1381,"depth":400,"text":1382},{"id":1445,"depth":400,"text":1446},{"id":1466,"depth":400,"text":1467},{"id":1483,"depth":400,"text":1484},{"id":1527,"depth":400,"text":1528},{"id":1538,"depth":238,"text":2092},"border-aware-padding — when the border steals space from the padding",{"id":1625,"depth":238,"text":1626},{"id":1702,"depth":238,"text":1703},{"id":1876,"depth":238,"text":2096},"fui-content — the wrapper the project is named after",{"id":2036,"depth":238,"text":2037},{"id":2046,"depth":238,"text":2047},[2100,2101],"open-source","design-system","Personal","\u002Fimages\u002Fpages\u002Fprojects\u002Ffui-content\u002Ffui-content-cover.png","2026-07-12","SCSS design system that styles native HTML with five interchangeable morphisms — glass, clay, neu, skeu and flat — WCAG AA contrast computed at compile time, and zero extra classes in the HTML. It's the library that dresses this very portfolio.","md","\u002Fimages\u002Fpages\u002Fprojects\u002Ffui-content\u002Ffui-content-detail.png",[2109],{"label":2110,"url":2111},"github.com\u002FAllanGBOliveira\u002Ffront-ui-scss-themes","https:\u002F\u002Fgithub.com\u002FAllanGBOliveira\u002Ffront-ui-scss-themes",{},"\u002Fen-us\u002Fprojects\u002Ffui-content","2026 – present",[2116,2117],"Creator","Maintainer",{"title":5,"description":2105},[72,93,106,2120,132,138,2121,150],"sass-true (Jest)","Docker","in-development","en-us\u002Fprojects\u002Ffui-content",[2125,2126,2127,86,2128,2129],"SCSS","Design System","Sass","Accessibility","Open Source","XltYH2W22rjXSM474Y_6XsIdeJ5vMyyvFQXQH_CfRD8",{"path":2132,"title":2133,"order":889},"\u002Fen-us\u002Fprojects\u002F3c-disparos-em-massa","Bulk Messaging · Grupo 3C",{"path":2135,"title":2136,"order":932},"\u002Fen-us\u002Fprojects\u002Fao-dev","ao.dev",1784312184503]