diff --git a/Theriapolis.Godot/Scenes/Steps/StepSpecies.cs b/Theriapolis.Godot/Scenes/Steps/StepSpecies.cs
index afaaffd..865d2d9 100644
--- a/Theriapolis.Godot/Scenes/Steps/StepSpecies.cs
+++ b/Theriapolis.Godot/Scenes/Steps/StepSpecies.cs
@@ -7,9 +7,11 @@ namespace Theriapolis.GodotHost.Scenes.Steps;
///
/// Step II — Species. Direct port of StepSpecies in
-/// src/steps.jsx plus the Phase 6.5 hybrid extension: when
-/// is true the step shows two
-/// stacked grids, one filtered by SireCladeId and one by DamCladeId.
+/// src/steps.jsx plus the Phase 6.5 hybrid extension. Hybrid mode
+/// uses a single unified grid (M6.16) — sire and dam species lists are
+/// merged, and each card carries either a Sire or Dam toggle in its
+/// header matching the clade it belongs to (sire/dam clades are
+/// guaranteed disjoint by StepClade's parent-conflict rule).
///
public partial class StepSpecies : VBoxContainer, IStep
{
@@ -17,8 +19,7 @@ public partial class StepSpecies : VBoxContainer, IStep
private VBoxContainer _purebredSection = null!;
private VBoxContainer _hybridSection = null!;
private GridContainer _purebredGrid = null!;
- private GridContainer _sireGrid = null!;
- private GridContainer _damGrid = null!;
+ private GridContainer _hybridGrid = null!;
// Phase B species trait + detriment pickers — single-pick per lineage.
private VBoxContainer _pickSection = null!;
@@ -77,13 +78,14 @@ public partial class StepSpecies : VBoxContainer, IStep
_hybridSection.AddThemeConstantOverride("separation", 16);
AddChild(_hybridSection);
- _hybridSection.AddChild(new Label { Text = "SIRE — Paternal Lineage", ThemeTypeVariation = "Eyebrow" });
- _sireGrid = MakeGrid();
- _hybridSection.AddChild(_sireGrid);
-
- _hybridSection.AddChild(new Label { Text = "DAM — Maternal Lineage", ThemeTypeVariation = "Eyebrow" });
- _damGrid = MakeGrid();
- _hybridSection.AddChild(_damGrid);
+ _hybridSection.AddChild(new Label
+ {
+ Text = "Pick one species per parent lineage. Sire's clade species "
+ + "are listed first, then Dam's — click any card to pick it.",
+ AutowrapMode = TextServer.AutowrapMode.WordSmart,
+ });
+ _hybridGrid = MakeGrid();
+ _hybridSection.AddChild(_hybridGrid);
// Phase B species pickers: one trait + one detriment per parent.
_pickSection = new VBoxContainer();
@@ -130,10 +132,7 @@ public partial class StepSpecies : VBoxContainer, IStep
if (hybrid)
{
- RefreshGrid(_sireGrid, _draft.SireCladeId, _draft.SireSpeciesId,
- spId => OnLineageSpeciesPicked("sire", spId));
- RefreshGrid(_damGrid, _draft.DamCladeId, _draft.DamSpeciesId,
- spId => OnLineageSpeciesPicked("dam", spId));
+ RefreshHybridGrid();
SyncSpeciesPicks(_sirePickCol, ref _sirePicksBuiltFor, "sire",
_draft.SireSpeciesId, _draft.SireChosenSpeciesTrait, _draft.SireChosenSpeciesDetriment);
@@ -398,6 +397,35 @@ public partial class StepSpecies : VBoxContainer, IStep
grid.AddChild(BuildCard(sp, sp.Id == selectedSpecies, onClick));
}
+ ///
+ /// Hybrid mode: rebuild the unified grid with sire-clade species
+ /// followed by dam-clade species. Sire and dam clades are
+ /// guaranteed distinct by StepClade's parent-conflict rule, so the
+ /// species lists are disjoint — each card unambiguously belongs to
+ /// one lineage and a click on the card commits the pick. Full
+ /// rebuild on every Refresh is safe because Bind installs Refresh
+ /// as a deferred callback.
+ ///
+ private void RefreshHybridGrid()
+ {
+ foreach (var c in _hybridGrid.GetChildren()) c.Free();
+ AddHybridLineageBlock("sire", _draft.SireCladeId, _draft.SireSpeciesId);
+ AddHybridLineageBlock("dam", _draft.DamCladeId, _draft.DamSpeciesId);
+ }
+
+ private void AddHybridLineageBlock(string lineage, string cladeId, string selectedSpeciesId)
+ {
+ if (string.IsNullOrEmpty(cladeId)) return;
+ var clade = CodexContent.Clade(cladeId);
+ string headerLabel = (lineage == "sire" ? "SIRE" : "DAM")
+ + (clade is null ? "" : " — " + clade.Name.ToUpperInvariant());
+ _hybridGrid.AddChild(new Label { Text = headerLabel, ThemeTypeVariation = "Eyebrow" });
+
+ foreach (var sp in CodexContent.SpeciesOfClade(cladeId))
+ _hybridGrid.AddChild(BuildCard(sp, sp.Id == selectedSpeciesId,
+ spId => OnLineageSpeciesPicked(lineage, spId)));
+ }
+
private static Control BuildCard(SpeciesDef sp, bool selected, System.Action onClick)
{
var card = CodexCard.Make();
diff --git a/Theriapolis.Godot/Scenes/Widgets/PopoverLayer.cs b/Theriapolis.Godot/Scenes/Widgets/PopoverLayer.cs
index 8fc0f21..04f6587 100644
--- a/Theriapolis.Godot/Scenes/Widgets/PopoverLayer.cs
+++ b/Theriapolis.Godot/Scenes/Widgets/PopoverLayer.cs
@@ -46,18 +46,33 @@ public partial class PopoverLayer : CanvasLayer
{
Layer = 100;
BuildPopover();
+ // Theme inheritance walks Control descendants only — CanvasLayer is
+ // a plain Node, so it breaks the propagation chain from the Wizard
+ // Control above. _Ready is bottom-up, so the parent Wizard hasn't
+ // assigned its codex theme yet — defer the lookup until parent's
+ // _Ready has run, then pull its theme onto the popup directly.
+ CallDeferred(MethodName.InheritParentTheme);
+ }
+
+ private void InheritParentTheme()
+ {
+ if (GetParent() is Control parentControl && parentControl.Theme is not null)
+ _popup.Theme = parentControl.Theme;
}
private void BuildPopover()
{
// Ignore so clicks/scroll/hover all pass through to whatever's
// beneath. The popover is purely a visual readout; the chip
- // owns the lifecycle entirely.
+ // owns the lifecycle entirely. ThemeTypeVariation pulls the
+ // CodexPopover stylebox (parchment bg2 + gild border + rounded
+ // corners + soft shadow) defined in CodexTheme.
_popup = new PanelContainer
{
Visible = false,
MouseFilter = Control.MouseFilterEnum.Ignore,
ZIndex = 100,
+ ThemeTypeVariation = "CodexPopover",
};
AddChild(_popup);
@@ -66,19 +81,29 @@ public partial class PopoverLayer : CanvasLayer
_popup.AddChild(v);
var nameRow = new HBoxContainer();
- nameRow.AddThemeConstantOverride("separation", 8);
+ nameRow.AddThemeConstantOverride("separation", 10);
v.AddChild(nameRow);
- _titleLabel = new Label();
+ // Display-serif title at H3 size (20px) — pulls the trait name
+ // out of the body copy below.
+ _titleLabel = new Label { ThemeTypeVariation = "H3" };
nameRow.AddChild(_titleLabel);
- _tagLabel = new Label { Visible = false };
+ // Mono uppercase tag label, vertically centred against the title.
+ _tagLabel = new Label
+ {
+ Visible = false,
+ ThemeTypeVariation = "Eyebrow",
+ VerticalAlignment = VerticalAlignment.Center,
+ SizeFlagsVertical = Control.SizeFlags.ShrinkCenter,
+ };
nameRow.AddChild(_tagLabel);
_descLabel = new Label
{
AutowrapMode = TextServer.AutowrapMode.WordSmart,
CustomMinimumSize = new Vector2(220, 0),
+ ThemeTypeVariation = "CardBody",
};
v.AddChild(_descLabel);
}
@@ -91,10 +116,18 @@ public partial class PopoverLayer : CanvasLayer
_tagLabel.Text = !string.IsNullOrEmpty(tag) ? tag.ToUpperInvariant()
: (detriment ? "DETRIMENT" : "");
- // M6.3 default-theme tint: detriment popover gets a red modulate so
- // it reads visually distinct from a regular trait. The proper
- // codex StyleBox swap lands in the theming pass.
- _popup.Modulate = detriment ? new Color(1f, 0.78f, 0.78f) : Colors.White;
+ // Detriment popover swaps to the seal-bordered stylebox via
+ // override; non-detriment clears the override so the default
+ // CodexPopover panel takes effect again.
+ if (detriment && _popup.HasThemeStylebox("panel_detriment", "CodexPopover"))
+ {
+ var box = _popup.GetThemeStylebox("panel_detriment", "CodexPopover");
+ _popup.AddThemeStyleboxOverride("panel", box);
+ }
+ else
+ {
+ _popup.RemoveThemeStyleboxOverride("panel");
+ }
_popup.Visible = true;
_popup.ResetSize();
diff --git a/Theriapolis.Godot/UI/CodexTheme.cs b/Theriapolis.Godot/UI/CodexTheme.cs
index 382bc90..d7b4b61 100644
--- a/Theriapolis.Godot/UI/CodexTheme.cs
+++ b/Theriapolis.Godot/UI/CodexTheme.cs
@@ -106,12 +106,19 @@ public static class CodexTheme
cardSelected.ShadowOffset = new Vector2(0, 14);
theme.SetStylebox("panel_selected", "Card", cardSelected);
- // Popover frame — gild border + soft shadow. Matches .trait-hint.
+ // Popover frame — gild border + soft shadow + rounded corners.
+ // Matches .trait-hint, with a softer corner radius than the rest of
+ // the codex (cards/buttons use 2px sharp) so the floating reveal
+ // reads as a friendlier secondary surface.
theme.SetTypeVariation("CodexPopover", "PanelContainer");
var popover = new StyleBoxFlat
{
BgColor = p.Bg2,
BorderColor = p.Gild,
+ CornerRadiusTopLeft = 14,
+ CornerRadiusTopRight = 14,
+ CornerRadiusBottomLeft = 14,
+ CornerRadiusBottomRight = 14,
ContentMarginLeft = 16,
ContentMarginRight = 16,
ContentMarginTop = 14,
@@ -120,11 +127,14 @@ public static class CodexTheme
ShadowSize = 18,
ShadowOffset = new Vector2(0, 12),
};
- popover.SetBorderWidthAll(1);
+ popover.SetBorderWidthAll(2);
theme.SetStylebox("panel", "CodexPopover", popover);
+ // Detriment swap — seal-red border drawn at 3px so the warning reads
+ // unambiguously against the parchment bg even at a glance.
var popoverDetriment = (StyleBoxFlat)popover.Duplicate();
popoverDetriment.BorderColor = p.Seal;
+ popoverDetriment.SetBorderWidthAll(3);
theme.SetStylebox("panel_detriment", "CodexPopover", popoverDetriment);
// Pill — small trait/skill chip. Mirrors .trait-chips .t-name from