Commit 41dbece4 authored by Mahmoud Aglan's avatar Mahmoud Aglan

docs(design): DESIGN.md for Night Arcade; flat faces, no gloss rims

DESIGN.md (and .impeccable/design.json) replace the June v3 spec with the
shipped Night Arcade system, derived from tokens.css and core.css: the
stage, ink tiles, white cards, keys on a lip, colour rationing, the board
palettes, Rubik with the pinned display face, motion and feel, RTL, and
the theming contract.

The documenter's audit found the code breaking its own rules: a white rim
on top of every key and the play key read as gloss (removed), the green
and orange faces had a lighter top that dropped white text under 3:1
(both are one flat colour now, orange deepened to 3.4:1), and small green
keys carried 13-14px text at 3.1:1 (they use a deeper green, 5:1). The
CTA glint stays, recorded as motion.
Co-Authored-By: 's avatarClaude Opus 5.5 <noreply@anthropic.com>
parent 3be7e45a
{
"schemaVersion": 2,
"generatedAt": "2026-10-19T06:31:24.800Z",
"title": "Design System: EL3AB",
"extensions": {
"colorMeta": {
"stage": {
"role": "neutral",
"displayName": "Stage Ink",
"canonical": "#14121A",
"tonalRamp": [
"#060508",
"#0A090D",
"#0E0D12",
"#14121A",
"#4C4B51",
"#858488",
"#BDBDBF",
"#E7E7E8"
]
},
"back-800": {
"role": "neutral",
"displayName": "Ink Tile",
"canonical": "#1D1A25",
"tonalRamp": [
"#09080B",
"#0F0D13",
"#14121A",
"#1D1A25",
"#535159",
"#89888E",
"#C0BFC2",
"#E8E8E9"
]
},
"back-700": {
"role": "neutral",
"displayName": "Raised Ink",
"canonical": "#262230",
"tonalRamp": [
"#0B0A0E",
"#131118",
"#1B1822",
"#262230",
"#5A5762",
"#8E8C93",
"#C2C1C5",
"#E9E9EA"
]
},
"back-950": {
"role": "neutral",
"displayName": "Deep Ink Lip",
"canonical": "#0D0C12",
"tonalRamp": [
"#040405",
"#070609",
"#09080D",
"#0D0C12",
"#47464B",
"#818184",
"#BBBBBD",
"#E7E7E7"
]
},
"card": {
"role": "neutral",
"displayName": "Card White",
"canonical": "#FFFFFF",
"tonalRamp": [
"#4D4D4D",
"#808080",
"#B3B3B3",
"#FFFFFF",
"#FFFFFF",
"#FFFFFF",
"#FFFFFF",
"#FFFFFF"
]
},
"card-edge": {
"role": "neutral",
"displayName": "Card Lip",
"canonical": "#CFC9DA",
"tonalRamp": [
"#3E3C41",
"#68656D",
"#918D99",
"#CFC9DA",
"#DBD6E3",
"#E6E3EC",
"#F2F0F5",
"#FAFAFB"
]
},
"ink": {
"role": "neutral",
"displayName": "Printed Ink",
"canonical": "#1A1722",
"tonalRamp": [
"#08070A",
"#0D0C11",
"#121018",
"#1A1722",
"#514F57",
"#88868C",
"#BFBEC1",
"#E8E8E9"
]
},
"text-secondary": {
"role": "neutral",
"displayName": "Stage Secondary Text",
"canonical": "#C8C3D4",
"tonalRamp": [
"#3C3B40",
"#64626A",
"#8C8994",
"#C8C3D4",
"#D5D1DE",
"#E2E0E9",
"#F0EEF3",
"#FAF9FB"
]
},
"text-muted": {
"role": "neutral",
"displayName": "Stage Muted Text",
"canonical": "#8E889E",
"tonalRamp": [
"#2B292F",
"#47444F",
"#635F6F",
"#8E889E",
"#A9A5B5",
"#C4C1CD",
"#DFDEE4",
"#F4F3F5"
]
},
"btn-green-2": {
"role": "primary",
"displayName": "Play Green",
"canonical": "#20A85D",
"tonalRamp": [
"#0A321C",
"#10542F",
"#167641",
"#20A85D",
"#56BD84",
"#8BD2AB",
"#C1E7D2",
"#E9F6EF"
]
},
"gold": {
"role": "primary",
"displayName": "Coin Gold",
"canonical": "#FFC531",
"tonalRamp": [
"#4D3B0F",
"#806319",
"#B38A22",
"#FFC531",
"#FFD362",
"#FFE194",
"#FFEFC5",
"#FFF9EA"
]
},
"gold-ink": {
"role": "primary",
"displayName": "Gold Ink",
"canonical": "#7A4E00",
"tonalRamp": [
"#251700",
"#3D2700",
"#553700",
"#7A4E00",
"#9A783D",
"#BAA37A",
"#DACDB8",
"#F2EDE5"
]
},
"chess-primary": {
"role": "secondary",
"displayName": "Chess Green",
"canonical": "#20A85D",
"tonalRamp": [
"#0A321C",
"#10542F",
"#167641",
"#20A85D",
"#56BD84",
"#8BD2AB",
"#C1E7D2",
"#E9F6EF"
]
},
"ludo-primary": {
"role": "secondary",
"displayName": "Ludo Orange",
"canonical": "#E06A16",
"tonalRamp": [
"#47230A",
"#763A10",
"#A55116",
"#E06A16",
"#F19556",
"#F5B78B",
"#FAD8C1",
"#FDF1E9"
]
},
"domino-primary": {
"role": "secondary",
"displayName": "Domino Violet",
"canonical": "#8C5CFA",
"tonalRamp": [
"#2A1C4B",
"#462E7D",
"#6240AF",
"#8C5CFA",
"#A883FB",
"#C3AAFC",
"#DFD1FE",
"#F4EFFF"
]
},
"backgammon-primary": {
"role": "secondary",
"displayName": "Backgammon Red",
"canonical": "#FF4458",
"tonalRamp": [
"#4D141A",
"#80222C",
"#B3303E",
"#FF4458",
"#FF7180",
"#FF9EA8",
"#FFCBD0",
"#FFECEE"
]
},
"red": {
"role": "tertiary",
"displayName": "Alert Red",
"canonical": "#FF4D5E",
"tonalRamp": [
"#4D171C",
"#80272F",
"#B33642",
"#FF4D5E",
"#FF7885",
"#FFA2AB",
"#FFCDD2",
"#FFEDEF"
]
},
"win": {
"role": "tertiary",
"displayName": "Win Green",
"canonical": "#2ED47A",
"tonalRamp": [
"#0E4025",
"#176A3D",
"#209455",
"#2ED47A",
"#60DE9A",
"#92E9BA",
"#C4F3DA",
"#EAFBF2"
]
},
"sky": {
"role": "tertiary",
"displayName": "Sky",
"canonical": "#4DA3FF",
"tonalRamp": [
"#17314D",
"#275280",
"#3672B3",
"#4DA3FF",
"#78B9FF",
"#A2CFFF",
"#CDE5FF",
"#EDF6FF"
]
},
"board-dark": {
"role": "tertiary",
"displayName": "Chess Slate",
"canonical": "#8B80AC",
"tonalRamp": [
"#2A2634",
"#464056",
"#615A78",
"#8B80AC",
"#A79EC0",
"#C3BDD4",
"#DFDBE8",
"#F3F2F7"
]
},
"bg-board-light": {
"role": "tertiary",
"displayName": "Backgammon Teal Field",
"canonical": "#22A57E",
"tonalRamp": [
"#0A3226",
"#11533F",
"#187358",
"#22A57E",
"#57BB9D",
"#8CD0BC",
"#C1E6DB",
"#E9F6F2"
]
}
},
"typographyMeta": {
"display": {
"displayName": "Display",
"purpose": "Big moments: result headlines, large counters. El3ab Display = Rubik pinned to 800."
},
"headline": {
"displayName": "Headline",
"purpose": "Screen titles (screenHead) and .btn-xl."
},
"title": {
"displayName": "Title",
"purpose": "Mode-button titles, empty-state titles, ribbons, .btn-lg, room-hero names."
},
"button": {
"displayName": "Button / Section",
"purpose": "Default buttons and section titles."
},
"body": {
"displayName": "Body",
"purpose": "Reading text, Rubik 500, tabular numerals."
},
"body-strong": {
"displayName": "Body Strong",
"purpose": "Row titles and toast text, Rubik 700."
},
"label": {
"displayName": "Label",
"purpose": "Subtitles, row subs, chips, .btn-sm."
},
"caption": {
"displayName": "Caption",
"purpose": "Mode-button sub-lines, HUD level; 12px is the floor."
}
},
"shadows": [
{
"name": "btn-lip",
"value": "0 var(--btn-lip) 0 var(--be)",
"purpose": "The solid lip under every .btn; 5px default, 3px sm, 6px xl. Pressed: translate 0 4px, lip 1px."
},
{
"name": "ink-tile-lip",
"value": "0 3px 0 var(--back-950)",
"purpose": "Rows, chips, HUD capsules; 4px on panels and icon keys."
},
{
"name": "shadow-card",
"value": "0 5px 0 var(--card-edge), 0 14px 28px -10px rgba(0, 0, 0, 0.55)",
"purpose": "White game cards."
},
{
"name": "shadow-sm",
"value": "0 2px 6px rgba(0, 0, 0, 0.25)",
"purpose": "Small floating marks."
},
{
"name": "shadow-md",
"value": "0 8px 20px -6px rgba(0, 0, 0, 0.45)",
"purpose": "Floating panels and popovers."
},
{
"name": "shadow-lg",
"value": "0 18px 40px -12px rgba(0, 0, 0, 0.6)",
"purpose": "A pushed scene over the previous one."
},
{
"name": "stage-ring",
"value": "0 0 0 3px var(--stage)",
"purpose": "Badges and the level badge, punched out of the stage."
},
{
"name": "text-outline",
"value": "0 2px 0 rgba(0, 0, 0, 0.16)",
"purpose": "The one soft drop under white text on a coloured key. Not an outline."
}
],
"motion": [
{
"name": "ease-pop",
"value": "cubic-bezier(0.34, 1.56, 0.64, 1)",
"purpose": "Overshoot: release spring, deal-in, toasts, tab pop, badges."
},
{
"name": "ease-spring",
"value": "cubic-bezier(0.34, 1.56, 0.64, 1)",
"purpose": "Alias of ease-pop."
},
{
"name": "ease-snap",
"value": "cubic-bezier(0.5, 1.8, 0.6, 1)",
"purpose": "Hard snaps."
},
{
"name": "ease-deal",
"value": "cubic-bezier(0.2, 0.9, 0.25, 1)",
"purpose": "Scene pushes, pops, world switches, flips."
},
{
"name": "ease-out",
"value": "cubic-bezier(0.16, 1, 0.3, 1)",
"purpose": "Chrome show/hide, XP fill, state changes."
},
{
"name": "ease-in",
"value": "cubic-bezier(0.4, 0, 1, 1)",
"purpose": "Exits: toast out, dock hide."
},
{
"name": "ease-in-out",
"value": "cubic-bezier(0.65, 0, 0.35, 1)",
"purpose": "Looping sweeps."
},
{
"name": "dur-fast",
"value": "140ms",
"purpose": "Colour and border transitions."
},
{
"name": "dur-normal",
"value": "240ms",
"purpose": "Chrome slides, sheet rise."
},
{
"name": "dur-slow",
"value": "420ms",
"purpose": "Scene enter."
},
{
"name": "press",
"value": "scale 0.95, brightness 0.96, 70ms ease-out; release pressPop 380ms ease-pop",
"purpose": "feel.js press squash on every pressable."
},
{
"name": "deal-in",
"value": "dealIn 480ms ease-pop, 42ms stagger, max 14, 2.6s window",
"purpose": "Content dealt in when a scene mounts."
},
{
"name": "count-up",
"value": "700ms ease-out-cubic, tick every 70ms",
"purpose": "Numbers that count."
},
{
"name": "reduced-motion",
"value": "animation/transition 1ms, one iteration",
"purpose": "prefers-reduced-motion: reduce; sound and haptics still answer."
}
],
"breakpoints": [
{
"name": "phone-narrow",
"value": "400px",
"purpose": "max-width: HUD drops name/XP, tighter gaps, 24px purse coins."
},
{
"name": "column",
"value": "560px",
"purpose": "Max content column (.screen); dock max 540px."
}
]
},
"components": [
{
"name": "Play Button",
"kind": "button",
"refersTo": "button-primary",
"description": "Green key on a lip: play and confirm. White display text.",
"html": "<button class=\"ds-btn-primary\">العب أونلاين</button>",
"css": ".ds-btn-primary { display: inline-flex; align-items: center; justify-content: center; gap: 8px; min-height: 54px; padding: 0 22px; border: none; border-radius: var(--r-btn, 18px); background: linear-gradient(180deg, var(--btn-green-1, #20A85D), var(--btn-green-2, #20A85D)); color: #fff; font-family: var(--font-display, 'Rubik', sans-serif); font-weight: 800; font-size: 18px; line-height: 1.1; text-shadow: 0 2px 0 rgba(0,0,0,0.16); box-shadow: 0 5px 0 var(--btn-green-edge, #12753F); cursor: pointer; transition: translate 70ms ease-out, box-shadow 70ms ease-out, scale 70ms ease-out; } .ds-btn-primary:hover { filter: brightness(1.04); } .ds-btn-primary:focus-visible { outline: 3px solid rgba(255,255,255,0.85); outline-offset: 3px; } .ds-btn-primary:active { translate: 0 4px; scale: 0.97; box-shadow: 0 1px 0 var(--btn-green-edge, #12753F); }"
},
{
"name": "Reward Button",
"kind": "button",
"refersTo": "button-gold",
"description": "Gold key for coins and rewards; gold-ink text, never white.",
"html": "<button class=\"ds-btn-gold\">اجمع المكافأة</button>",
"css": ".ds-btn-gold { display: inline-flex; align-items: center; justify-content: center; gap: 8px; min-height: 54px; padding: 0 22px; border: none; border-radius: var(--r-btn, 18px); background: linear-gradient(180deg, var(--btn-gold-1, #FFCD40), var(--btn-gold-2, #FFC229)); color: var(--gold-ink, #7A4E00); font-family: var(--font-display, 'Rubik', sans-serif); font-weight: 800; font-size: 18px; line-height: 1.1; box-shadow: 0 5px 0 var(--btn-gold-edge, #CC8A05); cursor: pointer; transition: translate 70ms ease-out, box-shadow 70ms ease-out, scale 70ms ease-out; } .ds-btn-gold:hover { filter: brightness(1.04); } .ds-btn-gold:focus-visible { outline: 3px solid rgba(255,255,255,0.85); outline-offset: 3px; } .ds-btn-gold:active { translate: 0 4px; scale: 0.97; box-shadow: 0 1px 0 var(--btn-gold-edge, #CC8A05); }"
},
{
"name": "White Button",
"kind": "button",
"refersTo": "button-white",
"description": "The strong neutral on ink: white face, ink text, card lip.",
"html": "<button class=\"ds-btn-white\">ضد الكمبيوتر</button>",
"css": ".ds-btn-white { display: inline-flex; align-items: center; justify-content: center; gap: 8px; min-height: 54px; padding: 0 22px; border: none; border-radius: var(--r-btn, 18px); background: linear-gradient(180deg, #FFFFFF, #FFFFFF); color: var(--ink, #1A1722); font-family: var(--font-display, 'Rubik', sans-serif); font-weight: 800; font-size: 18px; line-height: 1.1; box-shadow: 0 5px 0 var(--btn-white-edge, #CFC9DA); cursor: pointer; transition: translate 70ms ease-out, box-shadow 70ms ease-out, scale 70ms ease-out; } .ds-btn-white:hover { filter: brightness(1.04); } .ds-btn-white:focus-visible { outline: 3px solid rgba(255,255,255,0.85); outline-offset: 3px; } .ds-btn-white:active { translate: 0 4px; scale: 0.97; box-shadow: 0 1px 0 var(--btn-white-edge, #CFC9DA); }"
},
{
"name": "Ink Button",
"kind": "button",
"refersTo": "button-secondary",
"description": "The default neutral key: ink face on a deep-ink lip.",
"html": "<button class=\"ds-btn-ink\">إعادة المحاولة</button>",
"css": ".ds-btn-ink { display: inline-flex; align-items: center; justify-content: center; gap: 8px; min-height: 54px; padding: 0 22px; border: none; border-radius: var(--r-btn, 18px); background: linear-gradient(180deg, var(--btn-wood-1, #3B3547), var(--btn-wood-2, #363042)); color: var(--text-primary, #fff); font-family: var(--font-display, 'Rubik', sans-serif); font-weight: 800; font-size: 18px; line-height: 1.1; text-shadow: 0 2px 0 rgba(0,0,0,0.16); box-shadow: 0 5px 0 var(--btn-wood-edge, #201C28); cursor: pointer; transition: translate 70ms ease-out, box-shadow 70ms ease-out, scale 70ms ease-out; } .ds-btn-ink:hover { filter: brightness(1.04); } .ds-btn-ink:focus-visible { outline: 3px solid rgba(255,255,255,0.85); outline-offset: 3px; } .ds-btn-ink:active { translate: 0 4px; scale: 0.97; box-shadow: 0 1px 0 var(--btn-wood-edge, #201C28); }"
},
{
"name": "Game Card",
"kind": "card",
"refersTo": "deck-card",
"description": "White card on a lip for game objects: games, bots, results, rewards.",
"html": "<div class=\"ds-stage\"><button class=\"ds-deck-card\"><span class=\"ds-art\"></span><span class=\"ds-name\">الشطرنج</span></button></div>",
"css": ".ds-stage { background: var(--stage, #14121A); padding: 24px; } .ds-deck-card { display: block; width: 180px; padding: 8px 8px 10px; border: none; background: var(--card, #fff); color: var(--ink, #1A1722); border-radius: var(--r-card, 22px); box-shadow: 0 5px 0 var(--card-edge, #CFC9DA), 0 14px 28px -10px rgba(0,0,0,0.55); cursor: pointer; transition: translate 70ms ease-out, box-shadow 70ms ease-out; } .ds-deck-card:focus-visible { outline: 3px solid rgba(255,255,255,0.85); outline-offset: 3px; } .ds-deck-card:active { translate: 0 3px; box-shadow: 0 1px 0 var(--card-edge, #CFC9DA), 0 6px 14px -6px rgba(0,0,0,0.5); } .ds-art { display: block; height: 100px; border-radius: 16px; background: var(--chess-primary, #20A85D); } .ds-name { display: block; padding-top: 8px; text-align: center; font-family: var(--font-display, 'Rubik', sans-serif); font-weight: 800; font-size: 20px; }"
},
{
"name": "Ink Row",
"kind": "custom",
"refersTo": "row",
"description": "Each list item is its own flat ink tile on a 3px lip.",
"html": "<div class=\"ds-stage\"><div class=\"ds-row\"><span class=\"ds-avatar\">م</span><span class=\"ds-main\"><span class=\"ds-title\">محمود</span><span class=\"ds-sub\">تقييم 1450</span></span></div></div>",
"css": ".ds-stage { background: var(--stage, #14121A); padding: 24px; } .ds-row { display: flex; align-items: center; gap: 12px; min-height: 64px; padding: 10px 14px; border-radius: 20px; background: var(--back-800, #1D1A25); box-shadow: inset 0 1px 0 rgba(255,255,255,0.04), 0 3px 0 var(--back-950, #0D0C12); color: #fff; font-family: 'Rubik', sans-serif; } .ds-avatar { width: 44px; height: 44px; border-radius: 15px; background: var(--btn-purple-2, #8C5CFA); display: flex; align-items: center; justify-content: center; font-weight: 800; } .ds-main { display: flex; flex-direction: column; } .ds-title { font-weight: 700; font-size: 16px; } .ds-sub { font-weight: 500; font-size: 14px; color: var(--text-muted, #8E889E); }"
},
{
"name": "Chip",
"kind": "chip",
"refersTo": "chip",
"description": "Ink pill on a lip; the chosen one turns white with ink text.",
"html": "<div class=\"ds-stage\"><button class=\"ds-chip ds-chip-on\" aria-pressed=\"true\">الكل</button> <button class=\"ds-chip\">أصدقاء</button></div>",
"css": ".ds-stage { background: var(--stage, #14121A); padding: 24px; display: flex; gap: 8px; } .ds-chip { min-height: 42px; padding: 0 16px; border: none; border-radius: 21px; background: var(--back-700, #262230); box-shadow: 0 3px 0 var(--back-950, #0D0C12); color: var(--text-secondary, #C8C3D4); font-family: var(--font-display, 'Rubik', sans-serif); font-weight: 800; font-size: 14px; cursor: pointer; } .ds-chip:focus-visible { outline: 3px solid rgba(255,255,255,0.85); outline-offset: 3px; } .ds-chip:active { translate: 0 2px; box-shadow: 0 1px 0 var(--back-950, #0D0C12); } .ds-chip-on { background: var(--card, #fff); color: var(--ink, #1A1722); box-shadow: 0 3px 0 var(--card-edge, #CFC9DA); }"
},
{
"name": "Input",
"kind": "input",
"refersTo": "input",
"description": "Ink field; violet border and a lighter fill on focus.",
"html": "<div class=\"ds-stage\"><input class=\"ds-input\" placeholder=\"اسم المستخدم\"></div>",
"css": ".ds-stage { background: var(--stage, #14121A); padding: 24px; } .ds-input { width: 100%; min-height: 54px; padding: 12px 16px; background: var(--back-700, #262230); border: 2px solid transparent; border-radius: var(--r-input, 16px); color: #fff; font-family: 'Rubik', sans-serif; font-size: 17px; font-weight: 600; outline: none; caret-color: var(--gold, #FFC531); transition: border-color 140ms, background 140ms; } .ds-input::placeholder { color: var(--text-dim, #6B6579); } .ds-input:focus { border-color: var(--btn-purple-1, #9C6DFF); background: var(--back-600, #302B3C); }"
},
{
"name": "Floating Dock",
"kind": "nav",
"refersTo": "dock",
"description": "Floating ink dock; the green play key stands up out of it.",
"html": "<div class=\"ds-stage\"><nav class=\"ds-dock\"><button class=\"ds-tab\">الرئيسية</button><button class=\"ds-tab ds-play\"><span class=\"ds-key\"></span>العب</button><button class=\"ds-tab\">الترتيب</button></nav></div>",
"css": ".ds-stage { background: var(--stage, #14121A); padding: 48px 16px 24px; } .ds-dock { display: flex; justify-content: space-around; align-items: stretch; height: 68px; padding-inline: 6px; border-radius: 26px; background: rgba(29,26,37,0.94); box-shadow: inset 0 1px 0 rgba(255,255,255,0.06), 0 4px 0 var(--back-950, #0D0C12), 0 18px 36px -10px rgba(0,0,0,0.7); } .ds-tab { position: relative; flex: 1; display: flex; flex-direction: column; justify-content: flex-end; align-items: center; padding-bottom: 8px; border: none; background: none; color: var(--text-muted, #8E889E); font-family: var(--font-display, 'Rubik', sans-serif); font-weight: 800; font-size: 12px; cursor: pointer; } .ds-tab:hover { color: #fff; } .ds-tab:focus-visible { outline: 3px solid rgba(255,255,255,0.85); outline-offset: 3px; border-radius: 20px; } .ds-play { color: var(--btn-green-1, #20A85D); } .ds-key { position: absolute; top: -20px; width: 58px; height: 58px; border-radius: 20px; background: var(--btn-green-2, #20A85D); box-shadow: 0 5px 0 var(--btn-green-edge, #12753F), 0 0 0 5px rgba(29,26,37,0.94); } .ds-play:active .ds-key { translate: 0 3px; box-shadow: 0 2px 0 var(--btn-green-edge, #12753F), 0 0 0 5px rgba(29,26,37,0.94); }"
},
{
"name": "HUD Purse",
"kind": "custom",
"refersTo": "hud-purse",
"description": "Ink capsule: coin at its start, a green plus key at its end.",
"html": "<div class=\"ds-stage\"><button class=\"ds-purse\"><span class=\"ds-coin\"></span><span>1,250</span><span class=\"ds-plus\"><svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"#fff\" stroke-width=\"3\" stroke-linecap=\"round\"><path d=\"M12 5v14M5 12h14\"/></svg></span></button></div>",
"css": ".ds-stage { background: var(--stage, #14121A); padding: 24px; } .ds-purse { position: relative; display: inline-flex; align-items: center; gap: 4px; height: 36px; min-width: 74px; padding-inline: 4px 32px; border: none; border-radius: 18px; background: var(--back-700, #262230); box-shadow: inset 0 1px 0 rgba(255,255,255,0.05), 0 3px 0 var(--back-950, #0D0C12); color: #fff; font-family: var(--font-display, 'Rubik', sans-serif); font-weight: 800; font-size: 16px; font-variant-numeric: tabular-nums; cursor: pointer; } .ds-purse:focus-visible { outline: 3px solid rgba(255,255,255,0.85); outline-offset: 3px; } .ds-coin { width: 26px; height: 26px; border-radius: 50%; background: var(--gold, #FFC531); box-shadow: 0 2px 0 var(--gold-dark, #D18D08); } .ds-plus { position: absolute; inset-inline-end: 4px; top: 5px; width: 26px; height: 26px; border-radius: 13px; background: var(--btn-green-2, #20A85D); box-shadow: 0 2px 0 var(--btn-green-edge, #12753F); display: flex; align-items: center; justify-content: center; } .ds-purse:active .ds-plus { translate: 0 1px; box-shadow: 0 1px 0 var(--btn-green-edge, #12753F); }"
}
],
"narrative": {
"northStar": "Night Arcade",
"overview": "**Creative North Star: \"Night Arcade\"**\n\nEl3ab is a mobile game, never an app and never a retro skeuomorph. The ground is a quiet violet-black ink stage, and the only colour on it is the game itself: each game's own hue, gold for coins, green for play. Everything a player can press is a flat block of colour standing on a solid lip of a darker shade, and the finger pushes the face down onto that lip. Everything that is a game object (a game, a bot, a result, a reward, a ticket, the player card) is a crisp white card on a lip. Lists and panels are flat ink tiles one step lighter than the stage. Rubik is the only typeface, and the display cut is pinned to ExtraBold so every title, button and number shouts.\n\nThe direction comes from the owner, 2026-10-19: **\"modern, stylized, minimal, and still a game.\"** It replaced two worlds that were both rejected that same day and must not come back. \"Cafe Deck\" was an indigo world that read as \"a big blue blob, an app AI made\". \"Royal Cafe\" was walnut, leather and felt, and read as \"is it 2003?\". Night Arcade keeps the tactility of a game (lips, squash, sound, buzz) and drops every material imitation: no textures, no gloss, no outlines, no outlined text, and no single hue flooding a screen. Night Arcade was set straight from the owner's written direction and built code-first. There was no concept comp, so the shipped code is the reference.\n\nDensity is game-sized: 54px buttons, 64px rows, 44px minimum taps, a 560px content column centred on larger screens. Motion is part of the system, not decoration. Every touch answers with a squash, a sound and a haptic. Screens deal their content in, numbers count up with a tick, and wins land with confetti and coins flying to the purse.",
"keyCharacteristics": [
"A deep ink stage (`--stage`) with a faint eight-point star lattice and two soft pools of light. Both are painted once, on `body`.",
"Three surface families: ink tiles (lists, panels), white game cards (game objects), and flat colour keys on a lip (anything pressable).",
"Colour is rationed: one hue per game, gold for coins and rewards, green for play, red for alerts and badges.",
"Depth comes from solid lips and short soft drop shadows, never from bevels, gloss or inner glows.",
"Rubik throughout. The display face is Rubik pinned to 800. Body is Rubik at 500.",
"Arabic-first and RTL: only logical properties, and directional icons mirror.",
"Feedback on everything: motion, sound and haptics, all through one global layer (`feel.js`)."
],
"rules": [
{
"name": "The Rationed Colour Rule",
"body": "Each game owns one hue. Gold is coins and rewards. Green is play. Red is alerts. Everything else is ink or white. No single hue may flood a screen. A lobby shows its game's hue on the hero and the mode keys, never on the ground.",
"section": "colors"
},
{
"name": "The Ink Ground Rule",
"body": "The stage is painted once, on `body`. Never put a full-screen background on a scene root. Scenes sit transparently on the stage.",
"section": "colors"
},
{
"name": "The Face and Lip Rule",
"body": "Every colour that can be pressed comes as a set: `-1` (top of the face), `-2` (the face) and `-edge` (the lip). The face shading goes from `-1` to `-2` and is a hair of tone, not a gloss.",
"section": "colors"
},
{
"name": "The Readable Face Rule",
"body": "White text on a coloured key is display weight (800) and at least button size. Gold faces take `gold-ink`. White faces take `ink`. Before adding a new face colour, check white text against it at 3:1 or more.",
"section": "colors"
},
{
"name": "The One Family Rule",
"body": "Rubik only. Hierarchy comes from the pinned ExtraBold display cut against 500 body, plus size. Never add a second face, and never set a display element in a system font.",
"section": "typography"
},
{
"name": "The Shout Rule",
"body": "Anything in `var(--font-display)` renders at 800 whatever weight the rule asks for. Titles, buttons, chips, tabs, HUD names and stats, badges and ribbons all use display type with `letter-spacing: 0`.",
"section": "typography"
},
{
"name": "The Number Isolation Rule",
"body": "Wrap numbers that sit inside Arabic text in `<span class=\"num\">` (LTR, isolated, tabular).",
"section": "typography"
},
{
"name": "The Logical Properties Rule",
"body": "RTL is the default direction. Use logical properties only: `margin-inline-start`, `padding-inline`, `inset-inline-end`, `text-align: start`. Directional motion multiplies by `--dir` (1 in LTR, -1 in RTL). Directional icons (back, chevrons, send) carry `.i-dir` and mirror. Asymmetric masks (the spent card's punched corner) get an explicit `[dir=\"rtl\"]` variant. Never write `left`/`right`.",
"section": "layout"
},
{
"name": "The Two-Tap Rule",
"body": "The first viewport shows the four game cards and the big green play key. A player is in a game within two taps.",
"section": "layout"
},
{
"name": "The Lip Is the Depth Rule",
"body": "If it can be pressed, it stands on a lip of its own darker shade. If it can't be pressed, it doesn't get one, apart from the ink tiles' shallow 3–4px base. Depth never comes from a bevel, a gloss sheen or an inner glow.",
"section": "elevation"
},
{
"name": "The Flat Face Rule",
"body": "A face is one colour. The `-1` to `-2` gradient is the only shading allowed.",
"section": "elevation"
}
],
"dos": [
"Do build every screen on the ink stage from the kit: `screenHead()`, `.rows > .row`, `.deck-card`, `.btn` variants, `.chip`, `.input`, `emptyState()`, `modeButton()`, `roomHero()`.",
"Do put a lip under anything pressable (5px on keys, 3–4px on ink tiles, 5px `card-edge` on white cards) and let `feel.js` push it down.",
"Do give each game its one hue (`--chess-primary`, `--ludo-primary`, `--domino-primary`, `--backgammon-primary`) and keep gold for coins and rewards and green for play.",
"Do set white text on coloured keys in display weight at button size or larger, use `--gold-ink` on gold, and use `--ink` on white.",
"Do use `gicon()` for anything that is part of the game, `icon()` for small marks in text, and `crown()` for the logo.",
"Do reference every colour, radius, duration and size through `var(--token)`. Canvas code reads `getColor('token_name', fallback)`.",
"Do use logical properties only and multiply directional motion by `--dir`.",
"Do keep taps at 44px or more and text at 12px or more.",
"Do make big moments land with motion, sound and haptics together, and keep the reduced-motion path working."
],
"donts": [
"Don't bring back the Cafe Deck (an indigo blob) or the Royal Cafe (walnut, leather, felt). The owner rejected both on 2026-10-19.",
"Don't use textures: no wood, leather, felt, paper or noise. The faint star lattice on the stage and the diamond lattice on card backs are the only ornament.",
"Don't add gloss highlights, bevels, inner glows or gradients that imitate material.",
"Don't use outlines or outlined text: no thick dark strokes around shapes and no stroked or multi-shadow lettering. One soft drop (`--text-outline`) is the limit.",
"Don't let one hue flood a screen, and don't paint a full-screen background on a scene root.",
"Don't put white text on gold, or small (sub-button-size, non-display) white text on green or orange faces.",
"Don't use an emoji or a typed symbol (such as ✕ ✓ › ← ★ ●) as an icon in markup, strings, i18n values or fallbacks. `emoji()` in `theme.js` already resolves to stickers and line icons.",
"Don't hardcode hex values, radii or durations in components. Don't write `left`/`right`, `margin-left` or `text-align: left`.",
"Don't hand-roll press squashes, tap sounds or haptics. Don't add eyebrow or kicker labels above titles."
]
}
}
# EL3AB Design Specification v3
## Identity
EL3AB is a **social gaming hub for Arabic speakers** wrapping 4 classic board games (Chess, Ludo, Domino, Backgammon) in a platform that feels alive, dense, and responsive. Reference: Yalla Ludo + competitive depth.
## Design Pillars
1. **One Thumb, Full Experience** — Every primary action reachable with one thumb. < 3 taps to play.
2. **Touch = Feedback** — Every interaction responds with visual + audio + haptic simultaneously.
3. **Cards, Not Pages** — One UI primitive (the card). Cards stack, scroll, expand into sheets.
4. **Color = Game** — Each game owns a color identity. You always know which game context you're in.
5. **Always Alive** — No screen is static. Minimum: floating glow, pulsing dots, breathing buttons.
## Anti-Pillars
- NOT a web app (no material design, no breadcrumbs, no "dashboard")
- NOT minimal (dense = purposeful, every pixel works)
- NOT chess-first (all 4 games equal)
- NOT dark-for-trendy (dark bg is functional — makes colors pop)
## Color System
| Context | Primary | Secondary | Gradient |
|---------|---------|-----------|----------|
| Chess | #2563EB (blue) | #F5B731 (gold) | blue → gold |
| Ludo | #8B5CF6 (purple) | #EC4899 (pink) | purple → pink |
| Domino | #10B981 (emerald) | #06B6D4 (cyan) | emerald → cyan |
| Backgammon | #F59E0B (amber) | #EF4444 (red) | amber → red |
| Platform/Brand | #F5B731 (gold) | #00D4FF (cyan) | — |
| Background | #030A12 | #081420 | — |
| Text Primary | #F8FAFC | — | — |
| Text Secondary | #94A3B8 | — | — |
| Win | #34D399 | — | — |
| Loss | #F87171 | — | — |
## Card Anatomy
```
┌─────────────────────────────────┐
│ GRADIENT HEADER (game color) │ 60-80px
│ ◉ Icon Game Name ● ● │
├─────────────────────────────────┤
│ CONTENT BODY (bg-1) │ flexible
├─────────────────────────────────┤
│ ACTION FOOTER (optional) │ CTA/chips
└─────────────────────────────────┘
```
- Radius: 20px
- Border: 1px solid rgba(255,255,255,0.08)
- Shadow: 0 8px 32px rgba(0,0,0,0.4)
- Touch: scale(0.97) + shadow compress + haptic
## Screen Map
### Home (vertical scroll)
1. Header (fixed): Brand + Level + Coins + Gems + Notifications
2. Streak card (daily reward, pulses if unclaimed)
3. Games 2x2 grid (equal, always visible above fold)
4. My ratings (horizontal scroll pills)
5. Recent matches (last 5)
6. Friends online (avatar row)
7. Nav bar (fixed): Home | Games | Rank | Social | Me
### Game Lobby (bottom sheet, 70% screen)
- Mode selection (chips)
- Time control / bot picker (per game)
- "Play" CTA (large, game-color gradient)
- Swipe down to dismiss
### In-Game (immersive)
- Header + Nav hide
- Full-screen game board
- Opponent bar (top) + Player bar (bottom)
- Action drawer (swipe up): resign/draw/settings
### Post-Game (results card, slides up)
- W/L/D icon + confetti/shake
- Rating change (animated counter)
- XP + coins earned
- Rematch + Home buttons
## Interaction Rules
- Button press: scale(0.95) + haptic(10ms) + audio(click)
- Card press: scale(0.97) + tilt-toward-finger + shadow compress
- Navigation: slide transitions (250ms, ease-out)
- Game enter: zoom + darken
- Game exit: zoom out + brighten
- Win: confetti burst + haptic([20,50,20]) + fanfare
- Lose: subtle shake + haptic([50,30,50]) + descending tone
- Coin earn: particles fly to header counter + tick animation
- Level up: full-screen overlay + confetti + badge reveal
---
name: EL3AB
description: Night Arcade. A modern, stylized, minimal mobile game for four classic games, Arabic-first.
colors:
stage: "#14121A"
back-950: "#0D0C12"
back-800: "#1D1A25"
back-700: "#262230"
back-600: "#302B3C"
back-500: "#3D3749"
back-line: "rgba(255, 255, 255, 0.07)"
back-line-strong: "rgba(255, 255, 255, 0.14)"
card: "#FFFFFF"
card-2: "#F4F2F8"
card-3: "#E8E5EF"
card-line: "#E3DFEA"
card-edge: "#CFC9DA"
ink: "#1A1722"
ink-2: "#4B4558"
ink-3: "#7D768C"
red-ink: "#F0384B"
text-primary: "#FFFFFF"
text-secondary: "#C8C3D4"
text-muted: "#8E889E"
text-dim: "#6B6579"
gold: "#FFC531"
gold-soft: "#FFD866"
gold-dark: "#D18D08"
gold-ink: "#7A4E00"
red: "#FF4D5E"
win: "#2ED47A"
win-ink: "#13924E"
sky: "#4DA3FF"
btn-green-1: "#20A85D"
btn-green-2: "#20A85D"
btn-green-edge: "#12753F"
btn-green-deep: "#147F45"
btn-green-deep-edge: "#0D5C31"
btn-gold-1: "#FFCD40"
btn-gold-2: "#FFC229"
btn-gold-edge: "#CC8A05"
btn-red-1: "#FF5A6A"
btn-red-2: "#FF4458"
btn-red-edge: "#C2203A"
btn-blue-1: "#4C95FF"
btn-blue-2: "#3B86FF"
btn-blue-edge: "#1F5BC4"
btn-purple-1: "#9C6DFF"
btn-purple-2: "#8C5CFA"
btn-purple-edge: "#5F33C2"
btn-orange-1: "#E06A16"
btn-orange-2: "#E06A16"
btn-orange-edge: "#A84E0C"
btn-wood-1: "#3B3547"
btn-wood-2: "#363042"
btn-wood-edge: "#201C28"
btn-white-edge: "#CFC9DA"
chess-primary: "#20A85D"
chess-secondary: "#12753F"
ludo-primary: "#E06A16"
ludo-secondary: "#A84E0C"
domino-primary: "#8C5CFA"
domino-secondary: "#5F33C2"
backgammon-primary: "#FF4458"
backgammon-secondary: "#C2203A"
ludo-red: "#FF4D5E"
ludo-blue: "#3B86FF"
ludo-green: "#2BCF76"
ludo-yellow: "#FFC531"
board-light: "#E9E6F1"
board-dark: "#8B80AC"
board-highlight-light: "#F6E58C"
board-highlight-dark: "#D3C060"
board-check: "#FF4D5E"
board-frame: "#1D1A25"
bg-board-light: "#22A57E"
bg-board-dark: "#1B9070"
bg-point-dark: "#FF4458"
bg-point-light: "#F4F2F8"
bg-checker-white: "#FFFFFF"
bg-checker-black: "#1A1722"
typography:
display:
fontFamily: "'El3ab Display', 'Rubik', system-ui, sans-serif"
fontSize: "clamp(34px, 10vw, 44px)"
fontWeight: 800
lineHeight: 1.1
letterSpacing: "0"
headline:
fontFamily: "'El3ab Display', 'Rubik', system-ui, sans-serif"
fontSize: "clamp(22px, 6.2vw, 26px)"
fontWeight: 800
lineHeight: 1.15
letterSpacing: "0"
title:
fontFamily: "'El3ab Display', 'Rubik', system-ui, sans-serif"
fontSize: "clamp(19px, 5.2vw, 22px)"
fontWeight: 800
lineHeight: 1.1
letterSpacing: "0"
button:
fontFamily: "'El3ab Display', 'Rubik', system-ui, sans-serif"
fontSize: "clamp(16px, 4.5vw, 18px)"
fontWeight: 800
lineHeight: 1.1
letterSpacing: "0"
body:
fontFamily: "'Rubik', system-ui, sans-serif"
fontSize: "clamp(15px, 4.1vw, 16px)"
fontWeight: 500
lineHeight: 1.45
fontFeature: "tnum"
body-strong:
fontFamily: "'Rubik', system-ui, sans-serif"
fontSize: "clamp(15px, 4.1vw, 16px)"
fontWeight: 700
lineHeight: 1.3
label:
fontFamily: "'Rubik', system-ui, sans-serif"
fontSize: "clamp(13px, 3.6vw, 14px)"
fontWeight: 600
lineHeight: 1.3
caption:
fontFamily: "'Rubik', system-ui, sans-serif"
fontSize: "clamp(12px, 3.2vw, 13px)"
fontWeight: 600
lineHeight: 1.3
rounded:
xs: "8px"
sm: "10px"
md: "14px"
lg: "18px"
xl: "22px"
2xl: "28px"
full: "9999px"
card: "22px"
btn: "18px"
input: "16px"
row: "20px"
dock: "26px"
spacing:
s-1: "4px"
s-2: "8px"
s-3: "12px"
s-4: "16px"
s-5: "20px"
s-6: "24px"
s-8: "32px"
s-10: "40px"
s-12: "48px"
s-16: "64px"
gutter: "16px"
components:
button-primary:
backgroundColor: "{colors.btn-green-2}"
textColor: "{colors.text-primary}"
typography: "{typography.button}"
rounded: "{rounded.btn}"
padding: "0 22px"
height: "54px"
button-gold:
backgroundColor: "{colors.btn-gold-2}"
textColor: "{colors.gold-ink}"
typography: "{typography.button}"
rounded: "{rounded.btn}"
padding: "0 22px"
height: "54px"
button-blue:
backgroundColor: "{colors.btn-blue-2}"
textColor: "{colors.text-primary}"
typography: "{typography.button}"
rounded: "{rounded.btn}"
padding: "0 22px"
height: "54px"
button-red:
backgroundColor: "{colors.btn-red-2}"
textColor: "{colors.text-primary}"
typography: "{typography.button}"
rounded: "{rounded.btn}"
padding: "0 22px"
height: "54px"
button-white:
backgroundColor: "{colors.card}"
textColor: "{colors.ink}"
typography: "{typography.button}"
rounded: "{rounded.btn}"
padding: "0 22px"
height: "54px"
button-secondary:
backgroundColor: "{colors.btn-wood-2}"
textColor: "{colors.text-primary}"
typography: "{typography.button}"
rounded: "{rounded.btn}"
padding: "0 22px"
height: "54px"
button-sm:
rounded: "{rounded.md}"
padding: "0 14px"
height: "40px"
button-lg:
typography: "{typography.title}"
rounded: "20px"
padding: "0 26px"
height: "62px"
button-xl:
typography: "{typography.headline}"
rounded: "{rounded.xl}"
padding: "0 30px"
height: "70px"
mode-button:
rounded: "24px"
padding: "10px 14px"
height: "84px"
deck-card:
backgroundColor: "{colors.card}"
textColor: "{colors.ink}"
rounded: "{rounded.card}"
deck-card-face-down:
backgroundColor: "{colors.btn-purple-2}"
textColor: "{colors.text-primary}"
rounded: "{rounded.card}"
panel:
backgroundColor: "{colors.back-800}"
textColor: "{colors.text-primary}"
rounded: "{rounded.xl}"
padding: "16px"
row:
backgroundColor: "{colors.back-800}"
textColor: "{colors.text-primary}"
rounded: "{rounded.row}"
padding: "10px 14px"
height: "64px"
chip:
backgroundColor: "{colors.back-700}"
textColor: "{colors.text-secondary}"
typography: "{typography.label}"
rounded: "21px"
padding: "0 16px"
height: "42px"
chip-active:
backgroundColor: "{colors.card}"
textColor: "{colors.ink}"
input:
backgroundColor: "{colors.back-700}"
textColor: "{colors.text-primary}"
rounded: "{rounded.input}"
padding: "12px 16px"
height: "54px"
input-focus:
backgroundColor: "{colors.back-600}"
icon-button:
backgroundColor: "{colors.back-700}"
textColor: "{colors.text-primary}"
rounded: "16px"
size: "46px"
hud-purse:
backgroundColor: "{colors.back-700}"
textColor: "{colors.text-primary}"
rounded: "18px"
height: "36px"
dock:
backgroundColor: "rgba(29, 26, 37, 0.94)"
textColor: "{colors.text-muted}"
rounded: "{rounded.dock}"
height: "68px"
dock-play-key:
backgroundColor: "{colors.btn-green-2}"
textColor: "{colors.text-primary}"
rounded: "20px"
size: "58px"
toast:
backgroundColor: "{colors.card}"
textColor: "{colors.ink}"
rounded: "20px"
padding: "12px 18px"
toast-reward:
backgroundColor: "{colors.gold}"
textColor: "{colors.gold-ink}"
toast-error:
backgroundColor: "{colors.btn-red-2}"
textColor: "{colors.text-primary}"
badge:
backgroundColor: "{colors.red}"
textColor: "{colors.text-primary}"
rounded: "11px"
height: "22px"
ribbon:
backgroundColor: "{colors.btn-red-2}"
textColor: "{colors.text-primary}"
typography: "{typography.title}"
rounded: "{rounded.md}"
padding: "6px 18px"
---
# Design System: EL3AB
## Overview
**Creative North Star: "Night Arcade"**
El3ab is a mobile game, never an app and never a retro skeuomorph. The ground is a quiet violet-black ink stage, and the only colour on it is the game itself: each game's own hue, gold for coins, green for play. Everything a player can press is a flat block of colour standing on a solid lip of a darker shade, and the finger pushes the face down onto that lip. Everything that is a game object (a game, a bot, a result, a reward, a ticket, the player card) is a crisp white card on a lip. Lists and panels are flat ink tiles one step lighter than the stage. Rubik is the only typeface, and the display cut is pinned to ExtraBold so every title, button and number shouts.
The direction comes from the owner, 2026-10-19: **"modern, stylized, minimal, and still a game."** It replaced two worlds that were both rejected that same day and must not come back. "Cafe Deck" was an indigo world that read as "a big blue blob, an app AI made". "Royal Cafe" was walnut, leather and felt, and read as "is it 2003?". Night Arcade keeps the tactility of a game (lips, squash, sound, buzz) and drops every material imitation: no textures, no gloss, no outlines, no outlined text, and no single hue flooding a screen. Night Arcade was set straight from the owner's written direction and built code-first. There was no concept comp, so the shipped code is the reference.
Density is game-sized: 54px buttons, 64px rows, 44px minimum taps, a 560px content column centred on larger screens. Motion is part of the system, not decoration. Every touch answers with a squash, a sound and a haptic. Screens deal their content in, numbers count up with a tick, and wins land with confetti and coins flying to the purse.
**Key Characteristics:**
- A deep ink stage (`--stage`) with a faint eight-point star lattice and two soft pools of light. Both are painted once, on `body`.
- Three surface families: ink tiles (lists, panels), white game cards (game objects), and flat colour keys on a lip (anything pressable).
- Colour is rationed: one hue per game, gold for coins and rewards, green for play, red for alerts and badges.
- Depth comes from solid lips and short soft drop shadows, never from bevels, gloss or inner glows.
- Rubik throughout. The display face is Rubik pinned to 800. Body is Rubik at 500.
- Arabic-first and RTL: only logical properties, and directional icons mirror.
- Feedback on everything: motion, sound and haptics, all through one global layer (`feel.js`).
## Colors
A violet-black ink world with saturated, flat game colours rationed onto it. Every colour pairs a face with a darker lip of the same hue.
### Primary
- **Play Green** (`btn-green-2`, with `btn-green-1` as the top of the face and `btn-green-edge` as its lip): the play key in the dock, primary buttons (play, confirm), purse plus keys, the avatar edit badge. Green always means "go play" or "yes". The face was deliberately deepened from the bright `win` green so that white display text holds 3:1 (3.08:1 on `btn-green-2`). Chess borrows this green as its own hue (`chess-primary`).
- **Coin Gold** (`gold`, `gold-soft`, `gold-dark`, with `btn-gold-*` for buttons): coins, rewards, the level badge, reward toasts, selected states on boards (`bg-selected`), and the focus ring of counting numbers (`gold-soft`). Gold is never a decorative accent. If it shows up, it stands for value.
- **Gold Ink** (`gold-ink`): the only text colour allowed on a gold face (4.46:1 on `btn-gold-2`). White on gold is banned.
### Secondary: the four game hues
Each game owns exactly one hue, used for its card art ground, its mode buttons and its in-game accents:
- **Chess Green** (`chess-primary` / `chess-secondary`).
- **Ludo Orange** (`ludo-primary` / `ludo-secondary`, the same values as `btn-orange-*`).
- **Domino Violet** (`domino-primary` / `domino-secondary`, the same values as `btn-purple-*`). Violet is also the card back (`.face-down`), the default avatar ground and the input focus border.
- **Backgammon Red** (`backgammon-primary` / `backgammon-secondary`, the same values as `btn-red-*`).
### Tertiary: signals
- **Alert Red** (`red`): count badges, error toasts (on `btn-red-2`), loss, chess check.
- **Win Green** (`win`, `win-ink` for text on white): wins, success, online dots. It is brighter than Play Green and is not a button face.
- **Sky** (`sky`): links, the XP bar, the blue button family (`btn-blue-*`), info.
- **Medal metals** (`silver`/`silver-edge`, `bronze`/`bronze-edge`, `rar-common`/`rar-common-edge`): podium places and rarity only.
### Neutral
- **Stage Ink** (`stage`, the same value as `back-900`): the ground of every screen. It also rings badges so they look punched into the stage.
- **Ink Tiles** (`back-800` for panels and rows; `back-700` for chips, inputs, icon keys and HUD capsules; `back-600` for focused inputs and XP tracks; `back-500` for the resting play key): one step lighter per level of nesting.
- **Deep Ink** (`back-950`): the lip under every ink tile and ink key.
- **Hairlines** (`back-line`, `back-line-strong`): the rare divider and the ghost button ring.
- **Card White** (`card`, `card-2`, `card-3`, `card-line`, `card-edge`): white game cards, their insets and dividers, and the lip under them (`card-edge`).
- **Printed Ink** (`ink`, `ink-2`, `ink-3`, `red-ink`): text and pips printed on white cards. `ink-3` (4.34:1 on white) is the floor for text on a card.
- **Stage Text** (`text-primary` white, `text-secondary` for subtitles, `text-muted` for row subs and inactive tabs, at 5.0:1 on `back-800`). `text-dim` (3.3:1) is for placeholders and offline marks only, never for reading text.
### Board palettes
Boards are flat colour. Pieces and tiles are flat colour standing on a darker lip of their own shape.
- **Chess:** lavender slate squares (`board-light`, `board-dark`), soft yellow move highlights (`board-highlight-light`, `board-highlight-dark`), `board-check` red for check, ink frame (`board-frame`).
- **Backgammon:** a teal field (`bg-board-light`, `bg-board-dark`) in an ink frame and bar (`bg-board-border`, `bg-bar-*`, `bg-bearoff`). Points alternate red and pale (`bg-point-dark`, `bg-point-light`). Checkers are white and ink (`bg-checker-white`, `bg-checker-black`). `bg-highlight` win green marks legal targets and `bg-selected` gold marks the picked checker.
- **Ludo:** four bright flat quadrants on white (`ludo-red`, `ludo-green`, `ludo-yellow`, `ludo-blue`) with `card-line` track lines and gold for the active turn.
- **Domino:** white tiles (`card`) with `card-edge` lips and `ink` pips, on an ink table or a table in the domino hue (`domino-primary` / `domino-secondary`).
- Canvas code reads every board colour through `getColor('token_name', fallback)` so the admin overrides reach the boards too.
### Named Rules
**The Rationed Colour Rule.** Each game owns one hue. Gold is coins and rewards. Green is play. Red is alerts. Everything else is ink or white. No single hue may flood a screen. A lobby shows its game's hue on the hero and the mode keys, never on the ground.
**The Ink Ground Rule.** The stage is painted once, on `body`. Never put a full-screen background on a scene root. Scenes sit transparently on the stage.
**The Face and Lip Rule.** Every colour that can be pressed comes as a set: `-1` (top of the face), `-2` (the face) and `-edge` (the lip). Where white text sits on the face (green, orange) `-1` equals `-2`: the face is one flat colour. Small keys (`.btn-sm`) on green use `btn-green-deep` / `btn-green-deep-edge`, because 13–14px text needs 4.5:1 (5.0:1 there).
**The Readable Face Rule.** White text on a coloured key is display weight (800) and at least button size. Gold faces take `gold-ink`. White faces take `ink`. Before adding a new face colour, check white text against it at 3:1 or more.
## Typography
- Arabic: IBM Plex Sans Arabic (400, 500, 600, 700, 800)
- Numbers: system monospace, tabular-nums
- Brand: "EL3AB" — always gold, weight 800
**Display Font:** El3ab Display, the self-hosted Rubik variable font pinned to weight 800 (falls back to Rubik, then system-ui).
**Body Font:** Rubik 300–900, self-hosted woff2 in Arabic, Latin and Latin Extended subsets (`font-display: swap`).
**Label/Mono Font:** none. `--font-mono` and `--font-lat` both resolve to Rubik. Numbers use tabular figures everywhere (`font-variant-numeric: tabular-nums` on body and every control).
**Character:** One rounded, friendly geometric family that handles Arabic and Latin in a single voice. It shouts in ExtraBold for the game and reads calmly at 500 for everything else. `font-synthesis: none`, so no faux weights.
### Hierarchy
- **Display** (800, `--fs-3xl` clamp(34px, 10vw, 44px), 1.1): the big moments, such as result headlines and large counters. Used sparingly.
- **Headline** (800, `--fs-xl` clamp(22px, 6.2vw, 26px), 1.15, `text-wrap: balance`): screen titles in `screenHead()`, and `.btn-xl`. `--fs-2xl` clamp(26px, 7.6vw, 32px) sits between headline and display for hero numbers.
- **Title** (800, `--fs-lg` clamp(19px, 5.2vw, 22px), 1.1): mode-button titles, empty-state titles, ribbons, `.btn-lg`, room-hero names.
- **Button / Section** (800, `--fs-md` clamp(16px, 4.5vw, 18px), 1.1): default buttons and section titles.
- **Body** (500, `--fs-base` clamp(15px, 4.1vw, 16px), 1.45): reading text. Row titles use 700 at the same size.
- **Label** (600, `--fs-sm` clamp(13px, 3.6vw, 14px)): subtitles, row subs (500), chips (800 display), `.btn-sm`.
- **Caption** (600, `--fs-xs` clamp(12px, 3.2vw, 13px)): mode-button sub-lines, HUD level. 12px is the floor. The tab label (12px) and badges (12–13px display) sit at that floor.
### Named Rules
**The One Family Rule.** Rubik only. Hierarchy comes from the pinned ExtraBold display cut against 500 body, plus size. Never add a second face, and never set a display element in a system font.
**The Shout Rule.** Anything in `var(--font-display)` renders at 800 whatever weight the rule asks for. Titles, buttons, chips, tabs, HUD names and stats, badges and ribbons all use display type with `letter-spacing: 0`.
**The Number Isolation Rule.** Wrap numbers that sit inside Arabic text in `<span class="num">` (LTR, isolated, tabular).
## Layout
A single centred mobile column on a fixed stage. The scene container fills the viewport between the floating HUD (`--hud-height` 64px plus the top safe area) and the floating dock (`--tab-height` 84px plus the bottom safe area). In a game (`.game-mode`) or with `html.no-chrome`, the chrome hides and the scene goes edge to edge.
- **Column:** `.screen` is at most 560px wide, centred with `margin-inline: auto`, with 8px top padding, a 16px gutter (`--gutter`) on both sides and 28px at the bottom.
- **Spacing scale:** 4px steps (`--s-1` to `--s-16`: 4, 8, 12, 16, 20, 24, 32, 40, 48, 64). The common rhythm is 8px between list tiles, 12–14px inside components, and 24px above a section title with 10px below it.
- **Screen header:** at least 56px tall. The back key sits on the reading side, the title is centred, and actions sit at the far end. When there is a back key and no actions, the title is offset by 56px so it stays optically centred.
- **Lists:** `.rows` is a vertical stack with an 8px gap. Each `.row` is its own tile, at least 64px tall.
- **Chips:** a horizontally scrolling strip with an 8px gap and the scrollbar hidden.
- **Tap targets:** `--tap-min` 44px on every button, link and `[role=button]`. `--tap-comfortable` is 52px. The theme loader clamps stored HUD and tab sizes to 44px or more and stored font sizes to 11px or more.
- **Small phones (max-width 400px):** the HUD drops the name and XP block (the avatar with its level badge stands for the player), the gap tightens to 6px and the side padding to 10px, the purses keep their plus keys, and coin stickers shrink from 28px to 24px.
- **Overlays:** a full-viewport scrim (`rgba(8, 7, 12, 0.72)` with a 6px blur). Sheets use `--sheet-radius` 28px on `back-800`, with a maximum height of 82vh.
### Named Rules
**The Logical Properties Rule.** RTL is the default direction. Use logical properties only: `margin-inline-start`, `padding-inline`, `inset-inline-end`, `text-align: start`. Directional motion multiplies by `--dir` (1 in LTR, -1 in RTL). Directional icons (back, chevrons, send) carry `.i-dir` and mirror. Asymmetric masks (the spent card's punched corner) get an explicit `[dir="rtl"]` variant. Never write `left`/`right`.
**The Two-Tap Rule.** The first viewport shows the four game cards and the big green play key. A player is in a game within two taps.
## Elevation & Depth
Night Arcade is flat colour lifted by **lips**: a solid, unblurred offset shadow of the surface's own darker shade, straight down. The lip makes something feel pressable and physical, and pressing collapses it (the face translates down as the lip shrinks). Above the lip, a short soft drop shadow separates floating chrome and white cards from the stage. There are no bevels, no gloss, no inner glows and no material gradients.
### Shadow Vocabulary
- **Key lip** (`box-shadow: 0 var(--btn-lip) 0 var(--be)`, `--btn-lip` 5px; 3px on `.btn-sm`, 6px on `.btn-xl`): every `.btn` variant. Pressed: `translate: 0 4px` and the lip drops to 1px.
- **Ink tile lip** (`0 3px 0 var(--back-950)` on rows, chips and HUD capsules; `0 4px 0 var(--back-950)` on panels and icon keys; `0 5px 0` on the empty-state mark): ink surfaces.
- **Card lip** (`--shadow-card`: `0 5px 0 var(--card-edge), 0 14px 28px -10px rgba(0,0,0,0.55)`): white game cards. Face-down cards use the same shape with `btn-purple-edge`.
- **Soft drops** (`--shadow-sm` `0 2px 6px rgba(0,0,0,0.25)`, `--shadow-md` `0 8px 20px -6px rgba(0,0,0,0.45)`, `--shadow-lg` `0 18px 40px -12px rgba(0,0,0,0.6)`): floating elements. `--shadow-lg` sits under a pushed scene.
- **Stage ring** (`0 0 0 3px var(--stage)`): badges and the level badge, so they look punched out of the stage.
- **Text drop** (`--text-outline`: `0 2px 0 rgba(0,0,0,0.16)`): the single soft drop under white text on a coloured key. The name is historical. It is not an outline.
### Named Rules
**The Lip Is the Depth Rule.** If it can be pressed, it stands on a lip of its own darker shade. If it can't be pressed, it doesn't get one, apart from the ink tiles' shallow 3–4px base. Depth never comes from a bevel, a gloss sheen or an inner glow.
**The Flat Face Rule.** A face is one colour. The `-1` to `-2` gradient is the only shading allowed.
## Shapes
Big, soft, confident radii and nothing sharp. Pressable keys run 14–24px depending on size. Tiles and cards run 20–22px. Pills are fully round (chips 21px on a 42px height, badges 11px on 22px). Square-round keys (16px corners on 46px keys, 15px on the HUD avatar and HUD buttons) stand for icons and people. The scale is `--r-xs` 8px, `--r-sm` 10px, `--r-md` 14px, `--r-lg` 18px, `--r-xl` 22px, `--r-2xl` 28px and `--r-full`, with role tokens `--r-card` 22px, `--r-btn` 18px and `--r-input` 16px. Bigger elements get bigger corners: `.btn-sm` 14px, `.btn` 18px, `.btn-lg` 20px, `.btn-xl` 22px, mode buttons 24px, the dock 26px, sheets 28px, the profile avatar and the empty-state mark 30px.
Two silhouettes carry meaning. A **face-down card** (violet with a white diamond lattice) means locked or not yet. A **punched corner** (`.spent`, a 12px circular mask cut-out at the leading top corner) means used up.
**Stickers** (`gicon`) share the same form language: a bold game-icons.net silhouette in one flat colour, fattened slightly, standing on a lip of its own shape (offset 28 units on a 512 grid). There are eleven colour families of face and lip: gold, silver, red, green, blue, purple, orange, cream, teal, wood (coral) and pink. The game art (`gameArt(key)`) follows the same rule: one flat SVG scene per game on that game's hue, with every object one colour on a darker lip of its own shape.
## Components
The kit lives in `public/css/core.css`, with markup helpers in `public/js/core/ui.js` and icons in `public/js/core/icons.js`. Build new screens from these. Don't restyle them locally.
### Buttons
Tactile and loud: a flat colour face on a solid lip that the finger pushes down.
- **Shape:** 18px corners (`--r-btn`) on a 54px default height (`--btn-min-height`), 0 22px padding, display type at `--fs-md`, an 8px icon gap, and line icons stroked at 2.6.
- **Variants:** `.btn-primary` / `.btn-game` green (play, confirm); `.btn-blue` / `.btn-card` blue; `.btn-gold` (rewards, coins; `gold-ink` text, no text drop); `.btn-red` / `.btn-danger`; `.btn-purple`; `.btn-orange`; `.btn-white` (white face, ink text: the strong neutral on ink); `.btn-secondary` / `.btn-wood` (ink neutral, the default); `.btn-ghost` (transparent with a 2px inset `back-line-strong` ring and `text-secondary` text, for the quietest option).
- **Sizes:** `.btn-sm` 40px, default 54px, `.btn-lg` 62px, `.btn-xl` 70px. `.btn-block` makes a button full width.
- **Press:** `translate: 0 4px` and the lip shrinks to 1px. `feel.js` adds the squash (scale 0.95, brightness 0.96, 70ms), the tap sound and a light haptic, then springs back on release (`pressPop`, 380ms, `--ease-pop`).
- **Disabled:** `grayscale(0.9) brightness(0.75)`. A tap on a disabled key shakes it (260ms) and plays an error buzz instead of doing nothing.
- **Glint (motion, not a sheen):** the one big call to play on a screen (`.btn-xl`, `.btn-lg.btn-primary`, `.btn-lg.btn-gold`) catches a narrow light sweep every 4 seconds (`glint`, behind the label). At rest the face is flat; nothing else on the screen glints.
- **Green-sticker fix:** green stickers inside a green key are knocked to white (`filter: brightness(0) invert(1)`) so they don't vanish.
### Mode buttons (signature)
`modeButton({ id, sticker, title, sub, colour })`: a full-width coloured key, at least 84px tall with 24px corners. It holds a 60px sticker tile (18px corners, 20% white wash) on the reading side, a display title with a 600-weight sub-line, and a chevron at the end that mirrors in RTL. `live: true` bobs the sticker. Mode buttons stack with a 14px gap and take the game's hue or a signal colour (green, blue, purple, gold, red, wood).
### Cards / Containers
- **White game cards** (`.deck-card`): `card` white with `ink` text, 22px corners and the card lip. Use them for game objects only: games, bots, results, rewards, tournament tickets, the player card. Pressed, a card sinks 3px and its lip collapses. `.ink-red` switches the corner indices to `red-ink`. `.face-down` means locked and `.spent` means used up.
- **Room hero** (`roomHero(gameKey)`): the game's illustrated card at the head of its room, `min(62vw, 240px)` wide, with art in a 16px-radius inset and a display name below. It deals in with a -4deg tilt.
- **Ink panels** (`.card`, `.tray`): `back-800`, 22px corners, 16px padding, a 4px deep-ink lip and a 4% white top hairline.
- **Rows** (`.rows > .row`): each list item is its own `back-800` tile, at least 64px tall, with 20px corners, 10px 14px padding and a 3px lip. A row holds a 44px avatar, then a `row-title` (700) and `row-sub` (`text-muted`), then trailing actions.
### Chips
- **Style:** ink pills on `back-700`, 42px tall, 21px corners, `text-secondary`, display type at `--fs-sm`, with a 3px deep-ink lip.
- **State:** the chosen chip (`.active`, `aria-pressed`, `aria-selected`) turns white with ink text on a `card-edge` lip. Pressed, a chip sinks 2px.
### Inputs / Fields
- **Style:** `back-700` fill, a 2px transparent border, 16px corners, 54px minimum height, 17px Rubik 600, `text-dim` placeholder and a gold caret.
- **Focus:** the border turns `btn-purple-1` and the fill steps up to `back-600` (`--dur-fast`). Inputs are the only text-selectable surface (`user-select: text`).
### Navigation
- **HUD:** floating capsules with no bar, just a fade from the stage. On the start side: the avatar (46px, 15px corners, violet on a violet lip) with a gold level badge (`gold-ink` numeral, stage ring), then the name and an XP bar (68x6px, `sky` fill that grows from the start side). On the end side: coin and gem **purses**, each an ink capsule 36px tall with the sticker at its start and a 26px green plus key at its end, plus the bell key (44px, 15px corners). A purse that gains pops (`purseGain`) while its coin spins. A ringing bell swings (`bellRing`). New badges pop in.
- **Dock:** floats 10px above the bottom edge, 12px in from the sides, at most 540px wide, 68px tall with 26px corners. Its fill is `rgba(29, 26, 37, 0.94)` with an 18px backdrop blur, a deep-ink lip and a soft drop. Inactive tabs show 32px stickers at 85% grayscale and 55% opacity with `text-muted` labels. The active tab restores full colour, lifts 2px, scales to 1.14 and pops (`tabPop`). The dock slides away when hidden.
- **The play key (signature):** the centre tab stands 20px up out of the dock as a 58px green key with 20px corners, a 5px green lip and a 5px ring in the dock colour, so it looks cut through the dock. When active it breathes (scale 1.05, 2.6s) and its label turns green. When inactive it rests on `back-500`.
- **Scene transitions:** a push slides the new scene in from the reading direction with a 1.5% overshoot (420ms, `--ease-deal`) while the old one sinks (scale 0.92, opacity 0.3). A pop flicks the old scene out. A world switch (tab to tab) slides 24% with a fade (360ms). Every direction multiplies by `--dir`.
- **Screen header** (`screenHead(title, { back, sub, actions })`): an ink back key (46px, 16px corners, arrow stroked at 2.8), a centred display title that truncates on one line, and an optional `text-secondary` sub-line.
### Feedback
- **Toasts** (`toast(text, { type })`): a white pill (20px corners, ink 700 text, card lip) that drops in from under the HUD with overshoot (`toastIn`, 520ms) carrying a 26px sticker. The `reward` and `success` types are gold with `gold-ink` text and a gold glow. The `error` type is red with white text. Each type has its own sound.
- **Badges** (`.badge`): a red bead 22px tall with an 11px radius, a white display numeral and a 3px stage ring.
- **Ribbons** (`.ribbon`, plus `.gold`, `.green`, `.blue`): a flat coloured tag with 14px corners, a 4px lip and display type at `--fs-lg`.
- **Empty states** (`emptyState({ mark, title, sub, action })`): a floating 92px ink mark (30px corners, a muted line icon at 40px), a display title, a muted sub-line of at most 34ch, and an optional action. "One big quiet mark and one honest line."
- **Loading:** `loadingRows(n)` puts shimmering ink skeletons (`back-800` to `back-700`, 1.4s) in the shape of the rows.
### Icons
- `gicon(name, size)`: flat game stickers on a lip, for anything that is part of the game (tabs, purses, rewards, mode buttons, big buttons, toasts, results). Names are in `public/assets/game/gi/_names.json`. An unknown name falls back to `star`. Bake new ones with `tools/build-gicons.mjs` and credit the author in `public/assets/game/CREDITS.md`.
- `icon(name, size)`: Lucide line glyphs re-stroked at 1.75, for small marks in text (chevrons, back, x, check, alert). Coin and crown line icons print in gold, the gem in purple, and online dots in `win`.
- `crown(size)`: the El3ab crown logo (`crown.webp`).
- Rendered assets always have explicit pixel width and height with `object-fit: contain`. Any icon slot can be replaced from the admin by uploading an asset to `icon_<name>`.
### Motion and feel
El3ab is a game: nothing a player touches is inert. `core/feel.js` is one global layer, so every screen gets the same feel without wiring it.
- **Press:** every `button`, `.deck-card`, `.chip`, `.tab-item`, `[role=button|radio|switch|tab]` and checkbox gets a squash, a `tap` sound and a `light` haptic, then a spring on release. Moving more than 10px cancels the press so the user can scroll. Opt out with `data-feel="off"`. Never hand-roll press effects or tap sounds. Mark tappable non-buttons `role="button"`.
- **Deal-in:** for 2.6s after a scene mounts, rows, white cards, section titles and `.deal-in` items deal in one after another (`dealIn`, 480ms, `--ease-pop`, 42ms stagger, at most 14 items, alternating tilts of ±2.5deg on cards and ±0.6deg on rows). Deal-in is skipped in game mode and on elements that already animate.
- **Numbers:** `countUp(el, from, to)` uses a 700ms ease-out-cubic, ticks every 70ms, swells to 1.12 in `gold-soft` while counting, then lands (`numberLand`).
- **Haptics:** `haptic(kind)` takes `light`, `medium`, `heavy`, `success` or `error`. It goes through the native Flutter bridge when present and the Vibration API otherwise, and respects the player's haptics setting.
- **Sound:** `audio.play(name)` uses Kenney CC0 sounds (tap, toggle, deal, flip, move, capture, dice, coins, tick, win, lose, reward, levelUp, match_found and more).
- **Big moments land:** wins (flip, stamp, confetti, fanfare, coins flying to the purse via `juice.coinFlyTo`), losses (shake, sting), captures, dice, turns and rank changes.
- **Tokens:** `--ease-pop` / `--ease-spring` (overshoot), `--ease-snap`, `--ease-deal` (scene and deal movement), `--ease-out`, `--ease-in`, `--ease-in-out`. Durations are `--dur-fast` 140ms, `--dur-normal` 240ms and `--dur-slow` 420ms. The CSS vocabulary is `.dealt`, `.flip-in`, `.slam-in`, `.pop-in`, `.bounce-in`, `.wiggle`, `.float`, `.breathe-glow`, `.shake`.
- **Reduced motion:** under `prefers-reduced-motion: reduce`, every animation and transition drops to 1ms and runs once. `feel.js` skips the release spring, deal-in, count-up and the disabled shake. Sound and haptics still answer.
## Do's and Don'ts
### Do:
- **Do** build every screen on the ink stage from the kit: `screenHead()`, `.rows > .row`, `.deck-card`, `.btn` variants, `.chip`, `.input`, `emptyState()`, `modeButton()`, `roomHero()`.
- **Do** put a lip under anything pressable (5px on keys, 3–4px on ink tiles, 5px `card-edge` on white cards) and let `feel.js` push it down.
- **Do** give each game its one hue (`--chess-primary`, `--ludo-primary`, `--domino-primary`, `--backgammon-primary`) and keep gold for coins and rewards and green for play.
- **Do** set white text on coloured keys in display weight at button size or larger, use `--gold-ink` on gold, and use `--ink` on white.
- **Do** use `gicon()` for anything that is part of the game, `icon()` for small marks in text, and `crown()` for the logo.
- **Do** reference every colour, radius, duration and size through `var(--token)`. Canvas code reads `getColor('token_name', fallback)`.
- **Do** use logical properties only and multiply directional motion by `--dir`.
- **Do** keep taps at 44px or more and text at 12px or more.
- **Do** make big moments land with motion, sound and haptics together, and keep the reduced-motion path working.
### Don't:
- **Don't** bring back the Cafe Deck (an indigo blob) or the Royal Cafe (walnut, leather, felt). The owner rejected both on 2026-10-19.
- **Don't** use textures: no wood, leather, felt, paper or noise. The faint star lattice on the stage and the diamond lattice on card backs are the only ornament.
- **Don't** add gloss highlights, bevels, inner glows or gradients that imitate material.
- **Don't** use outlines or outlined text: no thick dark strokes around shapes and no stroked or multi-shadow lettering. One soft drop (`--text-outline`) is the limit.
- **Don't** let one hue flood a screen, and don't paint a full-screen background on a scene root.
- **Don't** put white text on gold, or small (sub-button-size, non-display) white text on green or orange faces.
- **Don't** use an emoji or a typed symbol (such as ✕ ✓ › ← ★ ●) as an icon in markup, strings, i18n values or fallbacks. `emoji()` in `theme.js` already resolves to stickers and line icons.
- **Don't** hardcode hex values, radii or durations in components. Don't write `left`/`right`, `margin-left` or `text-align: left`.
- **Don't** hand-roll press squashes, tap sounds or haptics. Don't add eyebrow or kicker labels above titles.
## Spacing
## Theming contract
- Card padding: 16px
- Card gap: 12px
- Section gap: 20px
- Touch target minimum: 44px
- Border radius: 20px (cards), 12px (buttons), 9999px (pills/badges)
Everything visual goes through the theming system, so the admin can retune the world without a deploy:
## Responsive
1. `public/css/tokens.css` is the design and the default for every value.
2. `public/js/core/theme.js` loads the stored theme from Supabase (through `/api/branding.php`, so it survives deploys) and writes it over the tokens as inline `:root` custom properties. **Stored values win over tokens.css**, but only for keys in the whitelists: `COLOR_KEYS` (surfaces, card and ink colours, signals, text, ludo players, board colours) and `LAYOUT_PX` (HUD and tab sizes, radii, durations). Keys from the retired palettes are ignored on purpose, so an old stored value can never repaint the app. The loader enforces the 11px font floor and the 44px tap floor on stored values.
3. `/admin/branding.php` **saves only the fields that changed.** Untouched fields are disabled before submit, so a save never pins a default over tokens.css. A new token value in tokens.css therefore reaches every player unless an admin has deliberately overridden that key.
4. Visual assets resolve through the theme: `icon()`, `gicon()` and `crown()` check for an uploaded `icon_<name>` asset first, and `assetImg()` / `emoji()` resolve slots the same way. To add a slot or a token, add it to `branding.php`, to `tokens.css` and to `ASSET_REGISTRY.json`.
5. Shipping rasters carry their provenance in `public/assets/game/CREDITS.md`: game-icons.net stickers (CC BY 3.0, by author), Lucide line icons (ISC), Kenney sounds (CC0), Rubik (OFL 1.1), and the crown from the El3ab brand files.
- Mobile-first: 320px - 480px (primary)
- Tablet: 768px+ (2-column games grid becomes 4-column, sidebar appears)
- Desktop: 1024px+ (sidebar nav, wider content area)
- Game boards: always square, max-width: min(100vw - 32px, 400px)
The working brief for people and agents is `docs/game-ui-brief.md`. The direction contract (thesis, world, story, first viewport) is the comment at the top of `<body>` in `index.php`.
......@@ -364,7 +364,7 @@ html.no-chrome #hud, #hud:empty { display: none; }
width: 58px; height: 58px;
border-radius: 20px;
background: var(--btn-green-2);
box-shadow: inset 0 2px 0 rgba(255, 255, 255, 0.25), 0 5px 0 var(--btn-green-edge), 0 0 0 5px rgba(29, 26, 37, 0.94), 0 14px 24px -6px rgba(0, 0, 0, 0.6);
box-shadow: 0 5px 0 var(--btn-green-edge), 0 0 0 5px rgba(29, 26, 37, 0.94), 0 14px 24px -6px rgba(0, 0, 0, 0.6);
padding: 8px;
filter: none;
opacity: 1;
......@@ -373,7 +373,7 @@ html.no-chrome #hud, #hud:empty { display: none; }
.tab-item[data-world="play"].active .tab-icon { transform: none; animation: playBreathe 2.6s ease-in-out infinite; }
@keyframes playBreathe { 0%, 100% { scale: 1; } 50% { scale: 1.05; } }
.tab-item[data-world="play"].active span:last-child { color: var(--btn-green-1); }
.tab-item[data-world="play"]:not(.active) .tab-icon { background: var(--back-500); box-shadow: inset 0 2px 0 rgba(255, 255, 255, 0.08), 0 5px 0 var(--back-950), 0 0 0 5px rgba(29, 26, 37, 0.94); }
.tab-item[data-world="play"]:not(.active) .tab-icon { background: var(--back-500); box-shadow: 0 5px 0 var(--back-950), 0 0 0 5px rgba(29, 26, 37, 0.94); }
/* ── Scenes, and how they change (scene.js keeps the old one as a ghost) ─ */
.scene {
......@@ -615,16 +615,18 @@ a:not(.btn):not([class*="tab"]) { display: inline-flex; align-items: center; min
line-height: 1.1;
text-shadow: var(--text-outline);
cursor: pointer;
box-shadow: inset 0 2px 0 rgba(255, 255, 255, 0.18), 0 var(--btn-lip) 0 var(--be);
box-shadow: 0 var(--btn-lip) 0 var(--be);
transition: filter var(--dur-fast);
-webkit-user-select: none;
isolation: isolate;
}
.btn .i { stroke-width: 2.6; }
.btn.is-pressed { translate: 0 4px; box-shadow: inset 0 2px 0 rgba(255, 255, 255, 0.18), 0 1px 0 var(--be); }
.btn.is-pressed { translate: 0 4px; box-shadow: 0 1px 0 var(--be); }
.btn:disabled, .btn[aria-disabled="true"] { filter: grayscale(0.9) brightness(0.75); cursor: default; }
.btn-primary, .btn-game { --b1: var(--btn-green-1); --b2: var(--btn-green-2); --be: var(--btn-green-edge); }
/* Small keys carry 13-14px text, which needs 4.5:1: a deeper green. */
.btn-sm.btn-primary, .btn-sm.btn-game { --b1: var(--btn-green-deep); --b2: var(--btn-green-deep); --be: var(--btn-green-deep-edge); }
.btn-card, .btn-blue { --b1: var(--btn-blue-1); --b2: var(--btn-blue-2); --be: var(--btn-blue-edge); }
.btn-gold { --b1: var(--btn-gold-1); --b2: var(--btn-gold-2); --be: var(--btn-gold-edge); --bt: var(--gold-ink); text-shadow: none; }
.btn-danger, .btn-red { --b1: var(--btn-red-1); --b2: var(--btn-red-2); --be: var(--btn-red-edge); }
......
......@@ -110,14 +110,15 @@
--bronze: #F08A4B;
--bronze-edge: #B5592A;
/* ── Buttons: a flat face (1 to 2 is a hair of shading, not a gloss)
and the lip under it ──────────────────────────────────────────── */
--btn-green-1: #27B466; --btn-green-2: #20A85D; --btn-green-edge: #12753F; /* white text 3.1:1 */
/* ── Buttons: a flat face (-1/-2 kept as names; they are the same colour
where contrast is tight) and the lip under it ─────────────────── */
--btn-green-1: #20A85D; --btn-green-2: #20A85D; --btn-green-edge: #12753F; /* white text 3.1:1 */
--btn-green-deep: #147F45; --btn-green-deep-edge: #0D5C31; /* small keys: 5:1 */
--btn-gold-1: #FFCD40; --btn-gold-2: #FFC229; --btn-gold-edge: #CC8A05;
--btn-red-1: #FF5A6A; --btn-red-2: #FF4458; --btn-red-edge: #C2203A;
--btn-blue-1: #4C95FF; --btn-blue-2: #3B86FF; --btn-blue-edge: #1F5BC4;
--btn-purple-1: #9C6DFF; --btn-purple-2: #8C5CFA; --btn-purple-edge: #5F33C2;
--btn-orange-1: #F27C2A; --btn-orange-2: #EC7420; --btn-orange-edge: #B0510C;
--btn-orange-1: #E06A16; --btn-orange-2: #E06A16; --btn-orange-edge: #A84E0C; /* white text 3.4:1 */
--btn-wood-1: #3B3547; --btn-wood-2: #363042; --btn-wood-edge: #201C28; /* neutral */
--btn-white-1: #FFFFFF; --btn-white-2: #FFFFFF; --btn-white-edge: #CFC9DA;
--outline: #0D0C12;
......@@ -141,8 +142,8 @@
/* ── Each game owns one hue ────────────────────────────────────────── */
--chess-primary: #20A85D;
--chess-secondary: #12753F;
--ludo-primary: #EC7420;
--ludo-secondary: #B0510C;
--ludo-primary: #E06A16;
--ludo-secondary: #A84E0C;
--domino-primary: #8C5CFA;
--domino-secondary: #5F33C2;
--backgammon-primary: #FF4458;
......
// Theme loads from Supabase via API (survives deploys).
//
// tokens.css holds the design (The Cafe Deck). Values stored from
// tokens.css holds the design (Night Arcade). Values stored from
// /admin/branding.php override it, but only for the keys listed in COLOR_KEYS
// and LAYOUT_PX below: the retired palette's keys (bg_card, blue, purple...) are
// ignored on purpose, so an old stored value can never repaint the app.
......
// Shared screen furniture for The Cafe Deck. Scenes build from these so every
// Shared screen furniture for Night Arcade. Scenes build from these so every
// header, empty table and loading state reads the same way.
import * as scene from './scene.js';
import * as audio from './audio.js';
......
......@@ -346,7 +346,7 @@ const STYLE = `
border-radius: 22px;
border: none;
background: var(--btn-purple-2);
box-shadow: inset 0 2px 0 rgba(255, 255, 255, 0.18), 0 5px 0 var(--btn-purple-edge);
box-shadow: 0 5px 0 var(--btn-purple-edge);
color: #fff;
cursor: pointer;
text-align: start;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment