# Implementation Plan: Fix Zero-HP Opacity **Branch**: `020-fix-zero-hp-opacity` | **Date**: 2026-03-06 | **Spec**: [spec.md](./spec.md) **Input**: Feature specification from `/specs/020-fix-zero-hp-opacity/spec.md` ## Summary Fix a CSS bug where `opacity-50` on the combatant row container cascades to absolutely-positioned popover and dropdown children, making the HP adjust popover and condition picker unusable for unconscious (0 HP) combatants. The fix removes the container-level opacity and applies dimming to individual leaf elements instead, so overlay components render at full opacity. ## Technical Context **Language/Version**: TypeScript 5.8 (strict mode, verbatimModuleSyntax) **Primary Dependencies**: React 19, Tailwind CSS v4, Lucide React (icons) **Storage**: N/A (no storage changes) **Testing**: Vitest (existing test suite) **Target Platform**: Web browser (Vite 6 dev server) **Project Type**: Web application (monorepo: apps/web + packages/domain + packages/application) **Performance Goals**: N/A (visual fix only) **Constraints**: Must pass `pnpm check` merge gate **Scale/Scope**: Single file change (`combatant-row.tsx`) ## Constitution Check *GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.* | Principle | Status | Notes | |-----------|--------|-------| | I. Deterministic Domain Core | PASS | No domain changes. Fix is adapter-layer only. | | II. Layered Architecture | PASS | Change is entirely in `apps/web` (adapter layer). No cross-layer imports added. | | III. Agent Boundary | N/A | No agent layer involvement. | | IV. Clarification-First | PASS | Bug is clearly defined; no ambiguous decisions. | | V. Escalation Gates | PASS | Fix is within spec scope. | | VI. MVP Baseline Language | PASS | No scope language changes. | | VII. No Gameplay Rules | PASS | No gameplay logic involved. | **Post-Phase 1 re-check**: All gates still pass. The fix is a single-file CSS class restructuring in the adapter layer with no architectural implications. ## Project Structure ### Documentation (this feature) ```text specs/020-fix-zero-hp-opacity/ ├── plan.md # This file ├── research.md # Root cause analysis and fix approach ├── data-model.md # No data model changes (documented) ├── quickstart.md # Verification steps └── tasks.md # Phase 2 output (/speckit.tasks command) ``` ### Source Code (repository root) ```text apps/web/src/components/ └── combatant-row.tsx # Single file to modify ``` **Structure Decision**: This is a bug fix touching a single component file. No new files, modules, or structural changes needed. The existing monorepo structure (`apps/web` → `packages/application` → `packages/domain`) is unchanged. ## Fix Approach ### Current (broken) `opacity-50` is applied to the outermost row `
` when `status === "unconscious"` (line 312). This creates a CSS stacking context that dims **all** descendants, including the absolutely-positioned `HpAdjustPopover` and `ConditionPicker`. ### Proposed (fixed) 1. **Remove** `opacity-50` from the outer row container. 2. **Add** an `unconscious` conditional class to each leaf/display element within the row: - Concentration button - Initiative input - Name display (`EditableName`) - AC display (`AcDisplay`) - HP value button (inside `ClickableHp`) - HP separator slash - Max HP display (`MaxHpDisplay`) - Condition tags (`ConditionTags`) - Remove button 3. The `HpAdjustPopover` and `ConditionPicker` are **not** wrapped in any dimmed element and render at full opacity. **Implementation detail**: Apply `opacity-50` to the inner grid container and the conditions container while ensuring popovers (`HpAdjustPopover`, `ConditionPicker`) are rendered as siblings outside those dimmed wrappers, not as children. This is the simplest approach — it avoids scattering opacity classes across every leaf element.