---
name: code-style
description: Code formatting and structure rules to apply while writing or editing code in PHP, JavaScript, CSS, HTML or Python - lines, functions, comments, naming, per-language conventions and a CSS architecture (spacing tokens, layout primitives, layers, BEM). Load before writing or changing any code. Not for prose, prompts or emails.
---

# Code style: the checklist while writing

Rules drawn from the Airbnb and Google style guides, PER Coding Style 2.0 (PHP), PEP 8 and Clean Code. Linters are the source of truth for formatting; this checklist covers what linters don't catch.

## Lines
- One statement per line. No `a(); b();`, no `if (x) a(); else b();` on one line.
- `if / else / for / while` always with braces, even for one line. `else` on the same line as `}`.
- Indentation: 2 spaces (JS/HTML/CSS), 4 (PHP/Python). Spaces, not tabs. No trailing whitespace; files end with a newline.
- Line length: JS/PHP/CSS 100, Python 88. When wrapping, put the operator at the start of the new line.
- One variable per declaration, declared where it's first used.
- Blank lines only between logical blocks.

## Functions
- Around 40 lines is a signal to split. At most 3 parameters; beyond that, an options object or array. Nesting depth at most 3.
- Guard clauses at the top with an early `return`. No `else` after `return`.
- A boolean "mode" parameter means two functions.
- The same body written twice becomes a named function.
- Caller above callee: a file reads top to bottom like a newspaper.
- Magic numbers become named constants.
- A function's name describes everything it does, including side effects. Verbs for functions, nouns for classes.

## Comments
- Explain why, not what. A comment that retells the code gets deleted. If you need to explain how, rewrite the code.
- Place comments above the line or block they explain; a short end-of-line comment is fine for data and constants.
- Every file starts with a header: what it is, where its data comes from, what the caller must provide.
- Helpers and public functions get PHPDoc / JSDoc / docstrings: how to call, what they return, types, and the shape of arrays.
- Mark client placeholders with one word, `PLACEHOLDER`, so one search finds them all.
- Not allowed: TODO/FIXME (fix it now or file an issue), commented-out code, change logs, author lines, boxes of asterisks, comments on closing braces.
- An outdated comment is worse than none: change the code, change the comment.

## Naming
- Names say what something is for, not how it's built or what type it is. No abbreviations except common ones (`id`, `url`, `src`).
- Name length grows with scope: `i` in three lines is fine, in a long function it isn't.
- JS: `camelCase`, classes `PascalCase`, module-level constants `UPPER_CASE`. PHP: classes `PascalCase`, methods `camelCase`, constants `UPPER_SNAKE`. Python: `lower_with_under`, classes `CapWords`. CSS: `block__elem--mod`, hyphens, named by purpose; `.js-*` only for behavior hooks.
- Collections are plural (`cards`, not `cardList`).

## By language
- **JS**: semicolons always, single quotes, `===` always, no nested ternaries. With a build step: `const`/`let`, arrow callbacks, ES modules. Without a build step (the script ships to the browser as is): `var` + `function`, no arrows, `const` or template strings, so older devices can run it. Don't declare `function` inside blocks.
- **PHP**: PER-CS 2.0. `<?php` → `declare(strict_types=1)` → `namespace` → `use` → code, no closing `?>`. Class and function `{` on its own line, `if/foreach` `{` on the same line. Visibility on everything, `elseif`, `[]` arrays with trailing commas when multiline, `new Foo()` with parentheses.
- **HTML**: double quotes, boolean attributes without a value, attribute order `class → id/name → data-* → src/href/type/value → title/alt → role/aria-* → tabindex → style`. Don't omit closing tags.
- **Python**: PEP 8 + Ruff/Black at 88. `is None`, `isinstance()`, specific exceptions, docstrings with `Args/Returns/Raises`, `if __name__ == '__main__':`.

## CSS: spacing, layout, architecture
- **Spacing scale as tokens, not numbers.** `--space-2 … --space-160` = 2 4 6 8 12 16 20 24 32 40 48 64 80 96 128 160 px (name = px, value in rem; 8px base with half steps). `margin/padding/gap/inset` only use `var(--space-*)`, `0` or `auto`; an optical nudge of 3px or less may be a raw number. Values like 22, 26 or 38 don't exist. Fluid top of the scale: `--space-section: clamp(3rem, 2rem + 4vw, 6rem)`. Semantic tokens on top: `--pad-x`, `--flow`, `--space-section`. Font sizes `--text-*`, z-index `--z-raised/sticky/top`.
- **Components have no outer margins.** The parent sets the distance: flex/grid → `gap`; flow content → `.flow > * + * { margin-block-start: var(--flow, var(--space-4)) }`, overridden with `--flow` on a child. If a margin must sit on an element, use one direction (`margin-block-start`), never both.
- **Size from content**: `border-box` everywhere, `min-height`/`flex-basis` instead of `width`/`height`, text `max-inline-size` around 60–66ch.
- **Layout with primitives**: wrapper (`margin-inline: auto; padding-inline: var(--gutter); max-width`), flow, cluster (`flex-wrap + gap`), grid (`repeat(auto-fill, minmax(min(100%, 15em), 1fr))` for cards, `auto-fit` for equal blocks), sidebar/switcher from Every Layout. Breakpoints come from the content, `min-width` and additive; `@container` for a component used in columns of different widths, `@media` only for the page grid and header. No device-width breakpoints.
- **Images in a box**: `aspect-ratio` + `object-fit`, never a fixed `height` + `cover`. An `<img>` inside `<picture>` fills its cell with absolute positioning or flex, not `height: 100%` (WebKit doesn't pass percentages through `<picture>`). Content images use `<picture>` with `sizes`, not `background`; art direction with `<source media>`. Always set `width` and `height`.
- **Tables on narrow screens**: `role="region" aria-labelledby tabindex="0"` + `overflow: auto` + a shadow at the cut edge; keep `<table>` semantics; don't hide the key column behind the scroll.
- **Touch**: targets at least 44px on `(pointer: coarse)` (WCAG AA minimum is 24).
- **Check widths**: 320 → 375 / 425 / 768 / 1024 / 1440, then ±1px around each of your own breakpoints, then drag the window. Two widths are not a check.
- **File in layers**: `@layer reset, base, layout, components, utilities;` first, `:root` with tokens, everything else inside layers. Inside `components`: block → its elements → their `@media` → modifiers → their `@media`. Each `@media` sits right after the rule it overrides, and modifiers come after the base rule's media queries. Resets inside `:where()`.
- **Selectors**: classes only, BEM `block__elem--mod`; no `#id`, no `ul.nav`, no `!important` (except `prefers-reduced-motion`, with a comment); chains of 3 or fewer, specificity at most (0,2,0). State from JS through `aria-*` when there's semantics, otherwise `data-state`; don't invent `.is-*` classes. A block stays under about 100 lines.
- **Nesting** only for `@media`/`@container`, `&:hover`/`&[aria-*]` and `& > * + *`; depth at most 2. `&__el` doesn't work in native CSS, so keep names flat.
- Property order: positioning → box model → typography → visual → misc (let the linter sort it). One declaration per line, double quotes, `0` without units, `0.5` with the leading zero.

## Before handing over
Linters decide formatting, with zero errors: ESLint (`curly`, `max-statements-per-line`, `no-else-return`, `complexity: 10`, `max-depth: 3`, `max-params: 3`), PHP_CodeSniffer with PSR-12, Stylelint (standard + recess-order + strict-value so spacing, fonts and colors only come from tokens), Ruff. What linters don't catch (comments that explain why, honest names, duplicated logic) you check against this list by eye.
