---
convoy: unify-glass-panel-surfaces
brief_number: 7
depends_on: [1, 2, 3, 4, 5, 6]
files:
  - .github/workflows/ci.yml
---

# Brief 7: `forbidden-bespoke-glass-surface` CI gate

## Goal (1 sentence)

Add a `forbidden-bespoke-glass-surface` job to `.github/workflows/ci.yml` that fails the build if `var(--glass-surface-(low|mid|high))` appears in JSX inline-style usage under `components/**` or `pages/**`, with a curated allowlist for the documented chrome exceptions (Layout sidebar nav-chip block, TopSearchBar `<header>` block, and the `<GlassSurface>` primitive itself).

## Files in scope (do not edit anything else)

- `.github/workflows/ci.yml` — append a new job after the existing `forbidden-modal-shell-without-primitive` job (around L200) or alongside it in the `forbidden-*` cluster. Don't reorder existing jobs.

**Out of scope:** every other file in the repo. This brief is pure CI surface; no application code changes.

## Conventions to follow

- **Model after the existing `forbidden-modal-shell-without-primitive` job** (`.github/workflows/ci.yml` L200-226). Same shape:
  - Single `runs-on: ubuntu-latest` step that runs a `grep` command.
  - Collects matches into a bash array.
  - Iterates and emits `::error file=${f}::<reason>` for each match.
  - Exits non-zero if any match remains.
  - The grep pattern is bounded to `pages/ components/ -r --include='*.js'`.

- **Verbatim job shape** (drop into `ci.yml` at the bottom of the forbidden-* cluster):
  ```yaml
    forbidden-bespoke-glass-surface:
      name: No bespoke var(--glass-surface-*) inline styles
      runs-on: ubuntu-latest
      steps:
        - uses: actions/checkout@v4
        - name: Fail if JSX inline styles handroll glass surfaces
          run: |
            # unify-glass-panel-surfaces convoy (PR sequence after #118 +
            # #117). Once Briefs 1-6 land, every panel-shaped surface in
            # the app composes via .glass-panel / .glass-panel-strong /
            # .page-header-glass / <GlassSurface>. Inline-style usage of
            # var(--glass-surface-low|mid|high) under pages/ or
            # components/ JSX is the regression vector this gate prevents.
            #
            # Allowlist (explicit, documented exceptions):
            #   - components/ui/GlassSurface.js          # the primitive itself
            #   - components/Layout.js                   # sidebar nav-chip chrome block (L858 area)
            #   - components/ui/TopSearchBar.js          # <header> chrome block
            # These three files intentionally compose handrolled chrome
            # surfaces ratified in PR #116 (gradient borders) + the
            # unify-glass-panel-surfaces architect decision D4. If you
            # need to add a fourth allowlist entry, that's a design-system
            # decision — open a new convoy.
            ALLOWLIST=(
              "components/ui/GlassSurface.js"
              "components/Layout.js"
              "components/ui/TopSearchBar.js"
            )
            FOUND=()
            while IFS= read -r file; do
              skip=false
              for allowed in "${ALLOWLIST[@]}"; do
                if [ "$file" = "$allowed" ]; then
                  skip=true
                  break
                fi
              done
              if [ "$skip" = true ]; then
                continue
              fi
              FOUND+=("$file")
            done < <(grep -lE "var\\(--glass-surface-(low|mid|high)\\)" \
                pages components -r --include='*.js' 2>/dev/null \
                | sort -u || true)
            if [ ${#FOUND[@]} -gt 0 ]; then
              echo "::error::Bespoke var(--glass-surface-*) inline styles detected outside the documented allowlist."
              echo "Use .glass-panel / .glass-panel-strong / .page-header-glass or compose <GlassSurface> from components/ui/ instead."
              for f in "${FOUND[@]}"; do
                echo "::error file=${f}::Replace inline var(--glass-surface-*) with the appropriate class or primitive."
              done
              exit 1
            fi
            echo "OK: no bespoke glass-surface inline styles outside the allowlist."
  ```

- **Allowlist hygiene:** the 3 allowlist entries are exact paths. If
  the relevant chrome block in `components/Layout.js` is later
  refactored into its own sub-component (e.g. `<SidebarNavChip>`),
  the new file path replaces `components/Layout.js` in the
  allowlist — that's the kind of edit a future PR would carry.

- **Pre-merge negative test:** before opening this PR, run the
  allowlist locally — verify that adding a scratch `style={{
  background: 'var(--glass-surface-low)' }}` to a non-allowlisted
  file (e.g. `pages/profile.js`) and re-running the grep produces a
  match. Then revert the scratch change. Document the negative test
  in the PR description (don't commit the scratch change).

- **CI ordering:** Brief 7 depends on Briefs 1-6 ALL landing first.
  If this job is added before any of the prior briefs ships, the
  build will fail on the in-flight migrations (every site this
  convoy is migrating IS currently a bespoke `var(--glass-surface-*)`
  inline-style usage). Conductor MUST hold dispatch until 1-6 are
  all on `main`.

- **Boundaries:** do NOT touch any other CI job. Do NOT edit any
  application file. Do NOT update AGENTS.md or other docs in this
  PR (a separate AGENTS.md update can land alongside Brief 1 if
  the architect wants — Brief 7 is pure CI).

## Acceptance criteria

- [ ] `.github/workflows/ci.yml` contains a new `forbidden-bespoke-glass-surface` job matching the verbatim shape above (job name, `runs-on`, checkout step, grep-and-allowlist shell block).
- [ ] The allowlist has exactly 3 entries: `components/ui/GlassSurface.js`, `components/Layout.js`, `components/ui/TopSearchBar.js`.
- [ ] On a clean post-Briefs-1-through-6 `main`, the new CI job is GREEN — no false positives. (Verify by running the grep locally before opening the PR.)
- [ ] Negative test: adding a scratch `style={{ background: 'var(--glass-surface-low)' }}` to e.g. `pages/profile.js` and re-running the same grep command produces a match. (Documented in PR description; not committed.)
- [ ] No other CI jobs are reordered, renamed, or modified.
- [ ] PR description links back to this brief and to the convoy file (`.convoys/unify-glass-panel-surfaces.md`).

## Rationale (≤3 sentences)

The convoy's success metric is unification, but unification without a regression gate is half a fix — a future PR can re-introduce a bespoke `var(--glass-surface-*)` inline style and undo the work. Modeling the gate on the existing `forbidden-modal-shell-without-primitive` job keeps it consistent with the repo's CI vocabulary and makes the failure message actionable. The 3-entry allowlist is small and intentional; growing it requires an explicit design-system decision, which is the right friction for a convention-enforcing gate.
