---
title: Gate a Raw Checkbox Against the Appearance Strip
type: task
status: planned
description: >-
  Add a check asserting every raw checkbox input under app/ has a prose_checkbox() call in its
  owning stylesheet. The global input appearance strip in normalize.css makes a bare checkbox a 0x0
  invisible control, and the trap has shipped three times against documentation that already
  prescribed the remedy.
base_uri: user:task/league/gate-raw-checkbox-against-appearance-strip.md
created_at: '2026-09-09T00:56:10.027Z'
entity_id: 0aa263d3-6fff-427a-acf9-1af7ba46d41c
observations:
  - ████████████████████████████████████████████████
owner_identity_uri: █████████████████████████
priority: Medium
public_read: true
relations:
  - relates [[user:task/league/add-batch-practice-squad-protection.md]]
tags:
  - user:tag/league-xo-football.md
  - user:tag/software-task.md
updated_at: '2026-09-09T00:56:38.081Z'
visibility_analyzed_at: '2026-11-05T23:49:32.091Z'
---

# Gate a Raw Checkbox Against the Appearance Strip

## Overview

`app/styles/normalize.css` carries a global `input { -webkit-appearance: none }`. A bare `<input type='checkbox'>` therefore renders with no box, no tick and no intrinsic size in every Chromium and WebKit browser, and `accent-color` is inert because it only applies to natively-rendered controls. The control still toggles. It is simply invisible, which reads to a user as the selection being broken.

The remedy already exists and is already written down twice — `prose_checkbox()` in `app/styles/prose-form.styl`, prescribed by `STYLE.md` and by the mixin's own comment. Knowing it has not been enough: the trap has landed three times.

This task adds the gate, so the next one fails a check instead of reaching a user.

## Background

Three occurrences, each found by a person rather than by a check:

- The waiting-list page drew its own box and wrote the reason down.
- The commissioner page did not, and shipped a 0x0 control that read as team selection being broken.
- The practice squad batch-protect row (2026-09-08) did not, and shipped the same defect to production. A manager protected ten players one at a time through the single-player context menu while the batch UI sat live and invisible on the same page. Fixed in `37cf503d5`.

The third one is the argument for the gate: the prescription was in `STYLE.md` and in `prose-form.styl` throughout, and the feature shipped anyway with a full browser verification pass on the task that introduced it.

## Why the existing checks cannot catch it

- `eslint` and `prettier` do not read `.styl`, so nothing in `yarn lint` or the pre-push guard's prettier pass sees a stylesheet at all.
- The webpack build succeeds — a bare checkbox is valid JSX and valid CSS.
- Stylus does not raise on an unknown mixin; it emits no declarations, so even a mis-typed `prose_checkbox` call fails silently.
- A browser pass can miss it. See the observation on this task: a driver that clicks an element programmatically operates on a 0x0 control exactly as it does on a visible one, so the check passes and cannot fail.

## Plan

The surface is small enough to enumerate exactly. As of 2026-09-08 the app contains three raw checkbox inputs:

```
app/views/components/player-roster/player-roster.js
app/views/pages/waitlist/waitlist.js
app/views/pages/admission-vote-commissioner/admission-vote-commissioner.js
```

All three now call `prose_checkbox()` from the stylesheet that owns them. The gate asserts that correspondence holds.

- [ ] Enumerate every `type='checkbox'` / `type="checkbox"` JSX site under `app/`, from the code rather than from the three names above — the point of the gate is to catch the fourth.
- [ ] For each, resolve the stylesheet that owns the component and require a `prose_checkbox` call in it. Resolving "the owning stylesheet" is the design question worth a moment: co-located `<component>.styl` covers the two page cases and `player-roster.js` is the one that draws from a SIBLING component's sheet (`app/views/components/player/player.styl`), so a naive co-location rule reports a false positive on the very case that motivated the gate.
- [ ] Prove it goes red. Delete the `input[type='checkbox']` block from `app/views/components/player/player.styl`, run the gate, require a failure naming that file; restore and require green. A gate not shown to fail is not evidence.
- [ ] Wire it into `scripts/verify.mjs` and `.github/workflows/test.yml`. Both, not one — `docs/guides/ship.md` records that the two lists drift and that CI runs gates `verify` does not.
- [ ] Consider whether the rule generalizes beyond checkboxes. `input { -webkit-appearance: none }` strips radio buttons and range inputs the same way. Scope creep is a real risk here; decide deliberately rather than by accident.

## Alternatives considered

**Delete the global `input { -webkit-appearance: none }`.** This is the from-scratch-ideal fix and it is why the task is not simply "add a lint rule". The declaration is a normalize.css inheritance nobody chose deliberately, and removing it would let every raw checkbox render natively with no mixin at all. The reason it is not the plan: every text input, select and button in the app has been styled for twelve years against a stripped appearance, so removing it repaints the entire application and cannot be verified by any check that exists. If someone wants to take it, it wants its own task and a visual sweep, not a line in this one.

**Document it harder.** Already done twice, in the two places a session would look. This is the option the three occurrences refute.

## Verification

- The gate fails on the deliberately-broken `player.styl` and passes on `master`, and the two runs report differently.
- A newly-added raw checkbox with no mixin call fails the gate.
- `yarn verify` and the CI Test job both run it.
