# Research: Fix Zero-HP Opacity **Feature**: 020-fix-zero-hp-opacity **Date**: 2026-03-06 ## Root Cause Analysis ### Decision: The bug is caused by CSS `opacity` inheritance on the row container **Rationale**: In `combatant-row.tsx:312`, the class `opacity-50` is applied to the outermost `
` when the combatant's HP status is `"unconscious"`. CSS `opacity` creates a new stacking context and affects all descendant elements — it cannot be overridden by child elements setting their own opacity. The `HpAdjustPopover` and `ConditionPicker` are rendered as children of this container (positioned `absolute` within it), so they inherit the 50% opacity. **Alternatives considered**: - **Portal-based rendering**: Move popouts to a React portal outside the row container. This would work but is over-engineered for this fix and would break the current relative positioning pattern used by both components. - **CSS `filter: opacity()`**: Same inheritance problem as the `opacity` property. ## Fix Approach ### Decision: Replace `opacity-50` on the row container with scoped opacity on row content elements only **Rationale**: Instead of making the entire row container semi-transparent, apply the dimming effect only to the static content elements within the row (the grid row with name/initiative/HP/AC and the condition tags area). The popout/dropdown elements sit outside this visual flow and should not be dimmed. This can be achieved by: 1. Removing `opacity-50` from the outer container. 2. Adding a wrapper `
` (or applying directly to the existing grid `
` and conditions `
`) with `opacity-50` when unconscious — but **not** wrapping the popover/picker elements. However, since the `HpAdjustPopover` is rendered inside `ClickableHp` which is inside the grid, and `ConditionPicker` is rendered inside the conditions container, the cleanest approach is: - Apply `opacity-50` to the inner grid and conditions container directly rather than the outer wrapper. - Since the popovers are absolutely positioned children of these containers, they'll still inherit opacity. The actual fix needs to use a different visual treatment that doesn't cascade. ### Decision: Use text/color-based dimming instead of CSS opacity **Rationale**: Replace the `opacity-50` approach with `text-muted-foreground` (or similar muted color classes) applied to the row's static text elements. This achieves the same visual "dimmed" effect for the unconscious state without using CSS `opacity`, which unavoidably cascades to absolutely-positioned children. Alternatively, apply `opacity-50` specifically to individual leaf elements (initiative input, name, AC display, HP display, condition tags) rather than a container. However, the simplest single-point fix is: - Keep `opacity-50` on the row container but add `opacity-100` (via a utility class) to the popover/picker wrapper elements. Since CSS opacity on a child cannot override a parent's opacity, this won't work directly. ### Final Decision: Use CSS `filter` or restructure to isolate popovers The simplest correct fix: move the popovers outside the opacity scope by changing the structure so the row content that gets dimmed is a separate child from the popover containers. **Approach**: 1. Remove `opacity-50` from the outer row `
`. 2. Wrap the row's visible content (grid + conditions area) in a `
` that receives `opacity-50` when unconscious. 3. Render `HpAdjustPopover` and `ConditionPicker` as siblings of this wrapper (still within the row for positioning context) rather than deep inside the dimmed content. **Rejected** for being too invasive to component structure. The popover is deeply nested inside `ClickableHp`. ### Actual Simplest Fix: Use `text-opacity` / color-based dimming Replace the container-level `opacity-50` with Tailwind's color-based dimming: - `text-muted-foreground/50` for text elements - Or simply use a lighter text color and reduced border opacity **But**: This changes more styling rules than necessary and may not look identical. ### Pragmatic Fix: Apply `opacity-50` to the grid and conditions wrapper, and reset opacity on popover elements using isolation Since CSS `opacity` on a parent always affects children, the real fix is to **not apply `opacity` to any ancestor of the popovers**. The most surgical approach: 1. Remove `opacity-50` from the outer `
`. 2. Add `opacity-50` to the inner grid `
` (line 316). 3. Add `opacity-50` to the conditions container `
` (line 388). 4. The `HpAdjustPopover` (absolutely positioned, `z-10`) and `ConditionPicker` (also absolutely positioned) are children of elements **within** these containers — they'll still be affected. **Final approach**: The only way to truly isolate is to ensure popovers aren't DOM children of dimmed elements. Use a two-sibling layout within the `relative` containers: For `ClickableHp`: The popover is already inside a `
`. We can't change that without moving the component. ### Conclusion: Simplest correct approach Apply `opacity-50` to individual non-interactive display elements within the row rather than any container that has popover children. Specifically: 1. Remove `opacity-50` from the outer row container (line 312). 2. When `status === "unconscious"`, pass a prop or apply conditional classes to: - The initiative input wrapper - The name display - The AC display - The HP number display (the button text, not the popover) - The condition tags - The remove button 3. Popovers and pickers remain unaffected since they're not inside any dimmed container. This is the most targeted fix with minimal structural changes.