Imported from yunusemrejr/yunuspi (
agent/skills/css-battle/SKILL.md). Install upstream withnpx skills add yunusemrejr/yunuspi --skill css-battle. Copyright stays with the author.
CSS Battle (the 800×600 pure-CSS drawing game)
The game (cssbattle.dev): each day a target image, 800×600, one element (body, or the one <div> the editor provides), descendant/child selectors only (no classes allowed beyond the single root in the JS boards — the CSS-only boards let you use div::before/::after + the one div), no images, no SVG, no canvas, no extra elements, no JS, and the score = the character count of your CSS (fewer chars = higher stars, 4 stars = the winning line the community sets). It's the best honest training there is for gradients-as-shapes, stacking order, and the discipline of "the pixels are computed, not placed" — every skill here ports directly into product CSS (hero backgrounds, empty-state illustrations, loading anims, badge art) where the "no extra elements" constraint is a feature (one-node CSS illustrations ship as ~400B of CSS, no asset pipeline).
The rules, exact (the ones people keep getting wrong)
- Canvas: 800px × 600px,
bodyis the stage (the editor sets overflow hidden); your CSS starts at the body (or the single div in JS boards) — you can't<!doctype>or add markup, you only write the CSS (the boards give you a fixed skeleton: plain boards =body{...}, "div" boards =div{...}div::before{...}div::after{...}— know which board you're on; the one you're not allowed is the mistake). - Selectors: only what the scaffold gives (body / div / div::before / div::after, +
*where allowed — no classes, no ids, no:nthbeyond what's scaffolded); descendant/child combinators with the scaffold's nodes only. - Forbidden:
url()(images, SVG, data-URIs — the "I'll just embed the base64" is an instant DQ),@import, external anything,filter: url(), the canvas-element, Web Audio,position: fixedis allowed (it just won't help — the stage is the viewport,absoluteis the working model), SVG-in-<img>/as-background (theurl('data:image/svg')trick = DQ, the community polices it and the site flagsurl(for the human review), andfont-tricks (text is allowed only on the text boards — the non-text boards ban it; the bordering-line trick (a 1px border where a "line" is) is legal, the "draw a 1px with a text glyph" = the text-boards-only move). - Character count: the entire CSS string is counted (whitespace included — the 4-star line is often the same trick minus the spaces:
background:#000 0 0/400px 300px no-repeat,...); the community's 4-star solutions are public (the board's solution list, sorted by length — read the shorter ones before you start a board: the trick is usually in there, in 60 characters). - Scoring: 1-4 stars by count against the board's thresholds (the 4-star = the current shortest-approved; the thresholds tighten as the community improves — the "4-star today" is the "3-star next month" — the chase is the game).
The toolkit (the 7 tools that draw everything)
linear-gradient(the workhorse): a rectangle with a hard stop = a positioned block (linear-gradient(90deg,red 0 400px,blue 0)); angle (0=up, 90=right, 180=down, 270=left, CSS angles are from 12-o'clock going clockwise — the #1 direction mistake: "90deg" is left-to-right, not top-down); the hard stop (red 0 400pxdouble-position = zero-width transition) is the line (a 2-stop pair with equal positions = a 0-height seam-free block boundary — this is how you draw crisp axis-aligned lines: two stops, same position, no AA fuzz);repeating-linear-gradient= stripes (the stop math: period = the two transition positions summed, the stripe width = the difference —repeating-linear-gradient(45deg,#000 0 10px,#fff 0 20px)= 10px stripes at 45°).radial-gradient(circles + the math that's not obvious): the default =closest-sideof the box (so a100px×100pxbox withradial-gradient(#000,#000)= a circle of r=50, centered); the ellipse-axes syntax (radial-gradient(circle 100px at 50% 50%, ...)= a circle of 100px radius;ellipse 200px 100px at ...= the radii) — the "I can't get the circle the right size" bug is 100% the ellipse-vs-circle keyword + the at-position; a half-circle = a radial in a box that's half the diameter (or the 50%50%00 filter, below); a ring = two radials stacked (outer color, inner background-color) — thebackground: radial-gradient(circle, #000 60%, #fff 0)= a 60%-radius disk on a white field (the "0" on the second stop = hard edge).conic-gradient(the pie + the magic circle): the angle from 12-o'clock clockwise; hard stops = wedge boundaries (conic-gradient(#000 0 270deg,#fff 0)= a 3/4 pie); the donut = a conic in a box + amask/an inner circle (the mask-free version: a radial of the background color on top — "don't use mask where a second gradient does it" is the chars-saved rule); the full magic:conic-gradient+@property+ a blurred/masked circle = the "text around a circle"/loader tricks (and the pie chart in 4 lines — the product-port of the trick: a 4-line donut stat).box-shadow(the cheap shape language): the inset shadow = an inner ring/border (theinsetwith 0 offset = an inset border of the spread width — the "1px border without a border" =box-shadow: inset 0 0 0 2px #000— 1 char shorter thanborderin the count-chase? no —border:2px solid #000wins on chars; the inset-shadow wins when you need the radius-offset or the multi-ring); multi-shadow stacking (box-shadow: 0 0 0 10px #000, 0 0 0 20px #fff, 0 0 0 30px #000= 3 concentric rings in one declaration — the "target/bullseye in 1 declaration" — and the dot-grid =box-shadowwith N copies (the hand-typed 25-shadow dot-grid is the classic 4-star pattern; the product port: a halftone/badge texture in one property)); the blur-radius is the "soft glow" (the0 0 30px 10px color= the nebula — used sparingly, the soft-blur everywhere = the AI-slop glow,anti-ai-slop).border+border-radius(the primitives): the 4-corner independent radii (border-radius: 50% 0 0 0= the quarter-circle in a square box — the 4-radius quarter-circle is THE circle tool: a square with 3 corners at 0 and 1 at 50% = the quarter-disk, positioned withbackground-clip/box= the arc); the border-triangle (border:50px solid transparent; border-bottom-color:#000= the up-triangle — the widths sum to the size; theborder-left-color+border-right= the other orientations); the border-radius 50%/50% for the ellipse (awidth≠heightbox +border-radius:50%= the ellipse — always, the "circle in a non-square box" = an ellipse by default — the #1 shape-soup bug: a100px×50pxbox withborder-radius:50%is a stadium, not a circle — a circle in a non-square stage needs theborder-radiusto be the smallest dimension (or the radial-gradient, which is always a true circle-with-radius-syntax)).clip-path(the arbitrary polygon — the poly-count is the cost):polygon(...)with%/px per corner (the hexagon = 6 points,polygon(25% 0,75% 0,100% 50%,75% 100%,25% 100%,0 50%)— the regular hexagon's points are the 50%-offset, not the 30% — the math: the pointy-top hex = x-offset 50% only, y = 0/50/100 (the flat-top = the 25/75 x) — the "my hexagon is a rhombus" bug is the wrong point table);circle()/ellipse()/inset()as the shorthands (clip-path: circle(40% at 30% 30%)= the clipped disk — clip-path is the "cut, don't paint" tool: it never draws, it removes — a clipped gradient = the "a conic pie with a bite" — and theclip-pathon::before+ a different clip on::after= the 2-shape intersection (the Venn in 2 pseudo-elements)); the product ports: the badge/star shapes, the "notch" corners (the ticket edge), the arrow callouts.transform+ the pseudo-elements (::before/::after= the two extra shapes the scaffold gives — the whole "draw it in 3 shapes" move: body = the background field, ::before = the primary shape, ::after = the accent; the transforms:rotate(the diamond = a square at 45°, the "diamond" is 4 chars of transform, not a polygon — reach for the rotate before the clip);scale(the non-uniformscale(2,0.5)= the ellipse-from-a-circle — cheaper than the border-radius math);translate(the centering, below); the 50% 50% 0 0 border-filter = the quarter-circle-disc via the 4-sided borders (a 0-sized element +border:50% 50% 0 0 solid— the percent-border-50-50-0-0 filter: awidth:0;height:0element withborder:250px solid #000;border-right-color:transparent;border-bottom-color:transparent;border-radius:50%— the quarter-circle (a 250px-radius arc in the top-left) — the "I need the arc not the pie" tool: the stroke-of-a-circle = the border-radius + a transparent-center via the second gradient/ring trick, or aborder-circle with thebackground:transparent+ thebox-shadow-cut (the donut-cut:box-shadow: inset 0 0 0 999px background— the inverse-mask-in-a-box)).
The canonical tricks (the named moves — learn these by heart)
- The centering (grid-0):
body{display:grid;place-items:center}(8 chars) — the stage-centerer; a single child's offset from center = themarginor atransform:translate()at known px (the "the grid centers but I need it 40px up" =transform:translateY(-40px)on the child — the absolute coordinate system the battle runs on is the grid-center as (0,0): think in offsets-from-center, not viewport-px, and the 800×600 math collapses to ±400/±300). - The background-stack order:
background: a, b, cpaintsaon top (first = topmost — the opposite of thebox-shadoworder where last is on top — the #2 stacking bug, after the angle; the paint rule: the first-listed wins the overlap); each layer takes its ownposition/size/repeatin the shorthand (bg red 50% 50%/200px 100px no-repeat, blue) — the short per-layer syntax is the chars-saver: theposition/sizeslash-form, theno-repeatper layer, the implicit-top left(0 0). - The hard-stop line (the seam-free divider):
linear-gradient(#fff 0 50%, #000 0)= the exact-midpoint divider (the double-stop0 50%= the #fff fills 0→50%, the #000 starts at 50% — no 0.5px white seam — the "there's a hairline gap" bug is the single-position50%transition (the AA blend at the stop) — always the double-stop for crisp edges). - The 50%50%00 filter (the quarter-disk):
border:50% 50% 0 0 solid #000; border-radius:50% 50% 0 0wait — the clean named form: a square element withborder-radius: 0 0 0 50%(one corner) = the quarter-pie; the true quarter-circlearc (a stroked arc) = theborder-circle (a square,border:W solid #000,border-radius:50%) with 2 borders transparent (the "C" shape) + the rotate to point it — the 4-sided-border-circle (the letter-C-in-4-borders = the arc tool; the product port: the 3/4 loading ring in one element, no SVG). - The blend-mode intersection (
mix-blend-mode: multiplyon a shape over a field = the multiplicative overlap — multiply of two opaque fields = the darker (a black-on-any = black, a white-on-any = the any) — the Venn/shape-intersection in 2 elements (the ::before = the shape A (white-on-transparent… the multiply needs the field too — the 3-layer: field, shape A, shape B, with A∪B as the "ink") — andscreen/overlay/exclusionfor the light logic**; the practical one: the halftone-over-image-texture (a multiply dot-grid over a color = the "print" texture — the product port: the hero-background texture, one::before+ one blend, no asset). @property+ the animatable custom property: the conic-gradient loader (the@property --p{syntax:'<angle>'...; initial-value:0deg}+ aconic-gradient(from var(--p) ...)+ a--pkeyframe = the smooth conic sweep — without@property, the gradient angle is not animatable (it snaps) — the "my loader jitters" bug; the same for thetransform-free "text-on-circle" (the rotating wrapper is animatable without the @property — the @property is for the gradient-parameter anims (the color-morph loader, the hue-spin)).- The repeat-grid (the tile):
background: repeating-linear-gradient(a 0 b, c 0 d), repeating-linear-gradient(90deg, a 0 b, c 0 d)= the grid lines (the two repeating gradients crossed = a graph-paper — the checkboard = the 45° version's 2 gradients (the checkerboard-in-2-repeating-linear-gradients trick:repeating-conic-gradient(#000 0 25%, #fff 0 50%) 0 0/40px 40px— the repeating-conic = the 1-line checkerboard (the0 25%/0 50%in a conic = the 4-quadrant cell, the50%/size= the tile — the #1 "that should be 1 line" find); the halftone = aradial-gradienttiled (radial-gradient(#000 20%, transparent 0) 0 0/20px 20px— the dot-grid in 1 layer, the per-row size = thebackground-size(a30px 30pxvs20px 20px= the density — and the staggered dots = the 2-layer offset (the50% 50%translate on the second) — the brick/honeycomb pattern in 2 dot-layers). - The text boards (the boards that allow text): the
font-size-in-em + theletter-spacing(the spacing is the design tool:letter-spacing: .5em= the wide-tracked headline — the tracking is the CSS-Battle typography move (nofont-familyshopping — the allowed set is the system default (SF/Segoe/Roboto by platform) + the size/weight/spacing/transform) — the "elegant" 4-star text solutions are the letter-spacing + the transform: rotate (the vertical label = arotate(90deg)at the margin) + the text-shadow (the2px 2px 0 #000= the hard-offset shadow = the "sticker" type — hard shadows only, the blur is the non-battle move). - The "one element, N shapes" budget: you have body + ::before + ::after = 3 shape-budgets (each with its own full gradient-stack — a
::beforecan itself be a 3-layer background — so the real budget is ~9 gradient-layers + 3 clips + 3 transforms); the discipline: decompose the target into ≤3 primary shapes first, then fill each shape with the stacked gradients (a "target" (bullseye) = the body (the outer ring, a radial 2-stop) + ::before (the mid, a smaller radial) + ::after (the bull, a third radial) — 3 radials, done). - The sub-pixel/AA rules (the "it's almost right" fixes): an edge that's fuzzy = the AA at a non-integer boundary (the 1px line at 333.5px renders as 2px-50% — snap to whole px in the 800×600 space: design in 2s and 4s (the 800/600 divides clean), the odd-px width is where the 3-star-forever lives); a seam between two gradient layers = the 0.5px overlap at the boundary (fix: overlap by 1px behind the crisp edge — the "paint the under-layer 1px past, the over-layer crisp on top" = the painter's seam-hiding); the circles that look polygonal at small sizes = the AA undersampling (a < 20px circle is always a bit lumpy — the 4-star line accepts it or scales up (the stage is fixed, the shape is your choice: a bigger circle is a cleaner circle, always — if the reference has a 12px dot that looks lumpy in your render, the difference is the AA threshold, not your math — the community's 4-star often approximates the sub-20px detail knowingly).
The solver workflow (the 4-star method, per board)
- Decompose (2 min, don't touch CSS): name the shapes in the target (the field, the N objects, their overlaps); count them (≤3 primary? more = look for the repeat (a
repeating-*/abackground-sizetile that draws N from 1) or the blend (the 2-layer multiply that makes one shape from two)); note the palette (the exact hex — pick each hex from the reference with the eyedropper, never the "it looks right" — the 1-star→2-star jump is often the color distance on a "same" fill — the scoring metric is per-pixel, a #f5f5f5 vs #fafafa field is 1000s of pixels of delta). - Geometry in center-offsets: the stage is 800×600, the grid-center is (0,0), the corners are (±400, ±300) — place everything in those coordinates before you write a size (a "a 200px square top-left" =
translate(-300px,-200px)from center with the grid — write the offset in your notes, the CSS follows); the symmetry check first (an 800-wide target that's left-right symmetric = one half designed, the50%placement + the mirror via a second layer at100% - x— the symmetry shortcut is the #1 chars-saver ("if it's symmetric, you drew half" — the count-halving rule). - Pick the tool per shape (the decision table): an axis-aligned block/line → the linear hard-stop · a circle/oval/ring → the radial (+ the box for the true-circle) · a wedge/pie/donut → the conic · a polygon/arrow/hex → the clip-path (or the border-triangle for the 3-siders) · a grid/dots/stripes/checker → the repeating-*/tile layer · a stroke/arc/half-ring → the border-circle (4-side, 2 transparent) or the conic+mask · a soft glow → the box-shadow blur (one, one) · a cut-out (a hole in a shape) → the 2nd-layer background-color circle (the "don't mask what a painted circle erases" rule — the mask-free cut is shorter and the seams behave).
- Stack in paint order (first = top): write the topmost shape first in the
backgroundlist (or assign: body = the field/lowest, ::before = the mid, ::after = the topmost-accent — the assignment is a design decision: the most complex shape gets its own pseudo-element (its own full layer-stack), the field keeps body (the body'sbackgroundcan still be a 3-layer tile) — the "where does the conic-donut live?" answer: the pseudo-element that's also the smallest (the clip on a body would clip the field too — clip-path on a body clips everything on the body — the clip lives on the shape's own node, always)). - Iterate the pixels (the 2-min loop): the tool's diff view (the side-by-side + the pixel-delta) → find the biggest delta region (it's one bug, not seven: a wrong angle (the 90-vs-0 mix-up), a 0.5px seam, a radius that's the box's not the stage's, a color-off fill, a repeated tile that's 1px off-period (the seam in the tile = the period math —
sizemust equal the pattern's period exactly, or the boundary shows the wrap — the "seamless tile" rule: the pattern's period == thebackground-size, or therepeating-*does the repeat inside (therepeating-linear-gradient+ abackground-size= the double-repeat bug (the stripes at 2 periods) — pick one repeat mechanism, never both)) → fix one, re-diff. - The star-chase (after "correct"): the count-kill pass: the space-strip (the minified
bg:#000 0 0/400px 300px no-repeat), the shorthand-merge (theplace-itemsfor the 2-line center, theinset:0for the 4-side), the keyword (the50%→center, the100%→ the full-sizebackground-size-omission? no —coveris not the 100% (the cover = the aspect-fit — the 100% 100% = the stretch,background-size:100%single = the width-100% auto-height — the short full-bleed = no size at all (the default for a solid), or thecoveronly when the asset-aspect matches)), the px-unit-drop (the0needs no unit, the1pxis already short, the%-where-em/pxis longer), and the */*comment-out for the AB-test of two solutions in one field (the solver's private trick: keep the 4-star candidate in a comment, the diff between two solutions is the training).
- Read the shorter solutions before you submit (the board's solution wall, the 4-star sorted): the first time you see a
repeating-conicchecker or a@propertyangle-sweep, put it in the toolkit as a named pattern (the "I would never have thought to conic that" is the whole learning — the community wall is the course, the board is the exercise, the ratio of time: 20% solving / 80% mining the solutions for the new trick is the actual skill-building loop).
The difficulty tiers (what the stars are measuring)
1★ = "a plausible drawing" (the shapes are there, the colors are close, the size is off) · 2★ = "correct geometry, acceptable color/precision" (the delta is small: the seams, the color-distance, the 1-2px drifts) · 3★ = "pixel-accurate" (the delta < the threshold: every seam gone, every hex picked, every edge snapped) · 4★ = "the shortest approved that does the above" (the economy of the solution: the trick you found that the others didn't — the 4-star is never "a better drawing", it's "the same drawing in 60 fewer characters" — the 4-star skill is the vocabulary, not the geometry: the person who draws it in 600 chars and the person who draws it in 300 both saw the same target; the 300 knows the repeating-conic and the symmetry-cut). The practical read: *aim for 3★ with clean geometry (the skill that transfers to product CSS — the pixel-accuracy discipline is the "it ships this exact" skill), then do the char-chase as the separate pass (the "make it right, then make it short" — never the interleaved "short-but-wrong" which teaches the wrong habits into the real code).
The product ports (where CSS-Battle skills earn their keep in a real codebase)
- The loading states: the conic-
@propertysweep (the 4-line donut loader), the 4-border ring (the 3-side transparent border-circle, the smallest possible spinner — no SVG), the dot-grid-pulse (the radial-tile + the opacity keyframe). - The empty-state art: a primary-shape composition (the 3 shape-budgets are the empty-state illustration: the body (the field/accent block), the ::before (the main glyph-as-shape), the ::after (the detail) — a "no results" magnifier in pure CSS (the border-circle = the lens, the border-45° = the handle — 2 pseudo-elements, no icon-font, no asset — the "CSS-Battle as the empty-state library" is a real pattern (the
empty-statethat's ~400B of CSS, the a11y is thearia-hiddenon the div + the real text beside it)). - The marketing backgrounds: the repeating-conic (the subtle grid/halftone), the radial (the spotlight/vignette), the conic (the "aurora" — used with a 10-20% opacity cap and a 2-hue max or it's the slop,
anti-ai-slop), the blend-texture (the multiply dot-grid over a brand color = the "print" hero in 0 assets). - The badge/stat/donut: the 4-line conic-donut stat (the "74% used" ring —
conic-gradient(#accent calc(var(--p)*1%), #track 0)+ the@property/or the rotate-wrapper fallback for the no-@propertybrowsers (the compat of the trick: the @property is 2023+ (Chrome 85+… actually the registered properties = Chromium 85, FF 72 (partial), Safari 16.4 — the fallback: the rotating-mask trick (a half-circle mask rotated = the donut, no @property) or the SVG-free two-conic (the conic + a conic-of-the-background-color as the "gap") — the real fallback is often "just use the 4-SVG" (the product judgment: the CSS-battle trick is the zero-dependency win; a library is fine — the "no asset, no dependency, 400B" is the reason to reach for the CSS)). - The "I can't draw that line" list (know the undrawable, don't burn the hour): a true Bézier curve (a non-arc, non-polygon curve — the clip-path
polygonis straight-segments only (the curved clip = thepath()(theclip-path: path('M...')— it exists: the SVG-path-in-clip-path = the arbitrary-curve tool, the 2021+ support (Chrome 55 (2016) / Safari 13.1 (2020) / FF 97 (2022) — FF is the laggard onpath()— the FF-lag is the product blocker (the 2022 fix) — the 2026 read: it's safe (the FF 97 = 2022, 4+ years) — the arbitrary-shape clip =clip-path: path('...')is the "I can't draw that curve" answer (thed-string is the SVG-path mini-language (the M/L/C/Q/Z subset you need) — the "CSS-Battle bans it? no — the boards allow it (thepath()is in the allowed-set) — the 4-star solutions for the curvy targets are full ofpath('M0 300 Q400 -200 800 300 Z')(the quadratic = the 1-control-point curve (the "swoosh") — the cubic C = the 2-control (the "S-curve"/the logo-flow) — the learn-the-3-commands subset: M (move), L (line), Q (1-control curve), C (2-control curve), Z (close) = the entire curve vocabulary of the battle (the arc A = the circle-segment, the 6-param monster, the "use the radial for the arc, the Q for the swoosh, avoid the A" is the 4-star convention (the A's params are the 10-char count-killer (the x-axis-rotation + the large-arc + the sweep + 2 coords) — the Q/C is the curve you want, the A is the curve you avoid)**.
the failure-mode index (symptom → cause → fix)
- Hairline gap between two blocks → single-position stop (the AA blend at the boundary) → the double hard-stop (
#a 0 50%, #b 0), or overlap by 1px behind the crisp edge. - My circle is an ellipse / wrong corners →
border-radius:50%on a non-square box (that's a stadium), or the radial's default box-ellipse → thecirclekeyword + explicit radius, or a square box. - Stripes don't line up at the edges → the tile period ≠
background-size, orrepeating-*plus a size (the double-repeat bug) → one repeat mechanism; period == tile, exact. - Conic loader snaps/jitters → gradient angles aren't animatable pre-
@property→@property --a{syntax:'<angle>'}+ animate--ain thefrom; fallback: the rotating-wrapper trick, or an intentionalsteps()chunky loader. - Wrong color where shapes overlap → opaque-over-opaque: the top layer wins, full stop →
mix-blend-modeon the top layer (multiply/screen/exclusion per the target logic). The Venn/hole bug: a shape filled with the background color, painted on top = the hole (erasure is the 4th tool — shorter than a mask, and the seams behave). - Looks right, diff says wrong → color distance: a "same" fill 1-2 levels off → eyedrop the exact hex, and remember the score (like the eye) judges lightness first → fix the flats first (a 1-value-off field is ~10,000× the delta of the AA ring).
- Right shape, wrong place → wrong coordinate frame → the center frame (grid center = (0,0); corners at ±400/±300): design in offsets-from-center — symmetry (±) only exists in that frame, and it halves the bugs.
- 40 chars over the 4★ line → the minify pass: the one-line
backgroundshorthand (the single biggest saver), space-strips,inset:0,place-items,0without units,centervs50%per the shorter — then the structural cuts: the symmetry-half,repeating-conicfor checkers/stars, and one tile instead of N (a dot grid: a 2-layer radial tile beats the box-shadow stack at every size). - Star targets → the formula, not eyeballed stops: N-point star =
repeating-conic-gradient(ink 0 (50/N)%, bg 0 (100/N)%)(8-point:6.25% / 12.5%; 6-point:8.33% / 16.67%; 12-point:5% / 10%). - Curvy "I can't draw that" →
clip-path: path('M…L…Q…C…Z')(the SVG-path subset — Q = one-control swoosh, C = two-control S; avoid the 6-param A-arc: the radial/Q covers it and costs half the chars). - Hex tile seams every row → √3 is the toolkit's only irrational: type it at 5 digits (row offset 50%/25%, size W × 0.866W;
.433/1.1547as needed) — 4 digits drift ~2px across a full board. - Text boards → the 4★ is a font-size hunt (binary-search the em at 0.01 steps against the reference's letter pixels) + accept the one-ring of AA: the reference's own AA ring is the floor; flats perfect, edges accepted, never an hour on the ring.
the three doctrines (the 90-minute-mistake preventers)
- Name the precision class before writing a character: integer boards (blocks/lines/stripes/circles/stars) → snap to whole px; hex boards → √3 at 5 digits; text boards → the em-hunt + AA acceptance. Chasing sub-px on an integer board is the classic 90-minutes-for-nothing.
- Flats before edges: the field is ~90% of the canvas, the edge ring ~2%; a 1-value-off fill is a ~10,000-px delta, the AA ring a ~1,600-px one. Fix flat color, lock it, then the char-chase.
- Two passes, never interleaved: make it right first (3★, clean geometry, exact hex), then make it short (the minify/structural pass). Interleaving teaches "short-but-wrong" into your real product CSS — the separation is the habit that transfers.
the habits
- Mine before you solve (20/80): read the board's solution wall (4★, sorted by length) before starting; ~80% of the time should be mining new tricks, 20% solving. Each "era" (
repeating-conic,@property,path()) retro-simplifies ~10 old boards — retro-solving a classic with the new keyword is the practice. - Eyedrop, never eyeball: the pixel score judges color; the eye fails at lightness (the
color-theoryrule, in scoring form). - "Symmetric?" first: left-right symmetry halves the design before any shape is drawn — the symmetry-cut is the #1 char-saver.
- Formulas over the wall: every named trick has a generating rule (the star fraction above, period==size for tiles, half-offset for staggered rows). The deriving-you finds the 4★ line the wall hasn't caught up to yet.
the starting 20 (know cold)
1 grid-center (display:grid;place-items:center) · 2 the hard-stop divider (double-stop) · 3 the radial disc (circle R at C) · 4 the conic pie (hard-stop angles) · 5 the repeating-conic checker (0 25%/0 50% + 2×2 sizes) · 6 the repeating-conic star (the 50/N · 100/N formula) · 7 the radial-tile dots · 8 the 2-layer staggered dots (the brick) · 9 the repeating-linear stripes (45°, period math) · 10 the 4-border C-arc (two sides transparent) · 11 the quarter-corner (border-radius:0 0 0 50%) · 12 the border triangle · 13 the clip polygon (the hex point table) · 14 the clip path() curve (M/L/Q/C/Z) · 15 the conic donut + center-cut radial · 16 the @property sweep (animatable angle) · 17 the box-shadow rings (multi 0 0 0 W) · 18 the blend-multiply texture · 19 the paint-eraser hole (bg-color shape on top) · 20 the symmetry mirror (50% + the 100% - x layer).
The shape budget is 3 nodes (body / ::before / ::after) but each carries its own full gradient stack — so effectively ~9 gradient-layers + 3 clips + 3 transforms. A board is a composition: ≤3 primary shapes, ≤1 tool per shape (the decision table above), the field on the body, the clip on the shape's own node (a clip on body clips everything), and the topmost accent on ::after. The 20 are notes; the decompose → assign → tool-per-shape workflow is the song.
Detailed coverage
CSS Battle — the 800x600 CSS drawing game (cssbattle.dev) — the craft of reproducing any reference image using ONLY a single , descendant/child combinators, pseudo-elements, and the standard CSS toolkit (gradients — linear/conic/radial/repeating — borders, box-shadow, clip-path, transforms, @property; no SVG/canvas, no images, no extra elements), plus the canonical tricks (the 50%50%00 filter for quarter-circles, background-stacking order, blend modes for shape intersection, the @property animatable custom-property trick, the grid-0 trick for centering), difficulty tiers (4 stars = the elegant minimum), the solver workflow (decompose → palette → geometry → stacking order → star-chase), and the failure modes (sub-pixel seams, the gradient-stop math, the "I can't draw that line" list). Use when solving a CSS Battle board, teaching pure-CSS shape construction, or porting a CSS-Battle-style trick into a real product (loading states, empty-state art, marketing backgrounds).