diff --git a/src/pirats/crud.py b/src/pirats/crud.py
index f410b90..2377f96 100644
--- a/src/pirats/crud.py
+++ b/src/pirats/crud.py
@@ -2,7 +2,7 @@ import json
import random
from typing import List, Dict, Any, Optional, Tuple
from sqlmodel import Session, select
-from .models import Game, Player, Obstacle, Challenge, Vote
+from .models import Game, Player, Obstacle, Vote
from . import cards
# --- Card and Hand Utilities ---
@@ -30,13 +30,13 @@ def calculate_max_hand_size(player: Player, players_in_scene: List[Player]) -> i
- Captain gets +1
If all players have the same Rank, they are all 'middle' and get 3 cards.
"""
- if player.role != "pirat":
+ if player.role == "deep":
# Deep players still have hands (for when they transition), let's say they have size based on Rank
# but don't participate in the Pi-Rat ranking calculations.
# Deep player max hand size is 3 by default, or equal to Rank. Let's make it equal to Rank + 1.
return player.rank + 1
- pi_rats = [p for p in players_in_scene if p.role == "pirat"]
+ pi_rats = [p for p in players_in_scene if p.role != "deep"]
if not pi_rats:
return 3
@@ -70,9 +70,9 @@ def calculate_max_hand_size(player: Player, players_in_scene: List[Player]) -> i
def is_player_captain(player: Player, players_in_scene: List[Player]) -> bool:
"""Helper to check if a player is currently the captain in the scene."""
- if player.role != "pirat":
+ if player.role == "deep":
return False
- pi_rats = [p for p in players_in_scene if p.role == "pirat"]
+ pi_rats = [p for p in players_in_scene if p.role != "deep"]
if not pi_rats:
return False
max_rank = max(p.rank for p in pi_rats)
@@ -161,13 +161,19 @@ def get_player(db: Session, player_id: str) -> Optional[Player]:
return db.get(Player, player_id)
def add_player(db: Session, game_id: str, name: str, is_creator: bool = False) -> Player:
+ game = db.get(Game, game_id)
+ role = None
+ if game and game.phase == "scene":
+ role = "pirat"
+
player = Player(
game_id=game_id,
name=name,
is_creator=is_creator,
rank=2, # default starting rank (will be set properly when starting character creation)
hand_cards="[]",
- is_ready=False
+ is_ready=False,
+ role=role
)
db.add(player)
db.commit()
@@ -365,7 +371,13 @@ def confirm_scene_setup(db: Session, game_id: str) -> Tuple[bool, str]:
players = game.players
- # 1. Count roles
+ # 1. Count roles (default any unselected roles to 'pirat')
+ for p in players:
+ if p.role is None:
+ p.role = "pirat"
+ db.add(p)
+ db.commit()
+
pirats_count = sum(1 for p in players if p.role == "pirat")
deeps_count = sum(1 for p in players if p.role == "deep")
@@ -383,11 +395,9 @@ def confirm_scene_setup(db: Session, game_id: str) -> Tuple[bool, str]:
return False, f"{p.name} was Deep in the last scene and must play a Pi-Rat in this scene."
# Everything is valid! Start the scene.
- # A. Reset obstacles and challenges
+ # A. Reset obstacles
for obs in game.obstacles:
db.delete(obs)
- for chal in game.challenges:
- db.delete(chal)
for vote in game.votes:
db.delete(vote)
db.commit()
@@ -469,25 +479,11 @@ def confirm_scene_setup(db: Session, game_id: str) -> Tuple[bool, str]:
# --- Scene Gameplay Operations ---
-def create_challenge(db: Session, game_id: str, title: str, description: str, obstacle_ids: List[str]) -> Challenge:
- challenge = Challenge(
- game_id=game_id,
- title=title,
- description=description,
- applied_obstacle_ids=json.dumps(obstacle_ids),
- is_active=True
- )
- db.add(challenge)
- db.commit()
- db.refresh(challenge)
- return challenge
-
def play_card_on_obstacle(
db: Session,
player_id: str,
obstacle_id: str,
- card_code: str,
- challenge_id: str
+ card_code: str
) -> Tuple[bool, str, Dict[str, Any]]:
"""
Plays a card from player's hand against an active obstacle.
@@ -640,17 +636,6 @@ def play_card_on_obstacle(
db.add(player)
db.commit()
- # 4. Deactivate the challenge (since a card was played against one of its obstacles)
- # Note: If the challenge had multiple obstacles, this single play resolves it.
- # In a more advanced UI we could track individual obstacle resolutions, but to keep the flow moving,
- # resolving the active challenge is clean. Let's mark the challenge as resolved by this player.
- challenge = db.get(Challenge, challenge_id)
- if challenge:
- challenge.is_active = False
- challenge.resolved_by_player_id = player.id
- db.add(challenge)
- db.commit()
-
res_dict = {
"success": success,
"details": details,
@@ -728,11 +713,6 @@ def end_scene_and_transition(db: Session, game_id: str):
for p in game.players:
p.is_ready = False
db.add(p)
-
- # Remove active challenges
- for chal in game.challenges:
- db.delete(chal)
-
db.add(game)
db.commit()
diff --git a/src/pirats/main.py b/src/pirats/main.py
index 77a2c0d..f08cb89 100644
--- a/src/pirats/main.py
+++ b/src/pirats/main.py
@@ -11,7 +11,7 @@ from pathlib import Path
from . import crud
from . import cards
from .database import create_db_and_tables, get_session
-from .models import Game, Player, Obstacle, Challenge, Vote
+from .models import Game, Player, Obstacle, Vote
app = FastAPI(title="Rats with Gats Remote Play")
@@ -128,37 +128,20 @@ def scene_roster_snippet(request: Request, game_id: str, db: Session = Depends(g
})
@app.get("/game/{game_id}/scene/obstacles", response_class=HTMLResponse)
-def scene_obstacles_snippet(request: Request, game_id: str, player_rank: int, db: Session = Depends(get_session)):
- game = crud.get_game(db, game_id)
- if not game:
- return HTMLResponse("")
- return templates.TemplateResponse("scene_obstacles_snippet.html", {
- "request": request,
- "game": game,
- "player_rank": player_rank,
- "parse_card": cards.parse_card,
- "json_loads": json.loads
- })
-
-@app.get("/game/{game_id}/scene/challenges", response_class=HTMLResponse)
-def scene_challenges_snippet(request: Request, game_id: str, player_id: str, db: Session = Depends(get_session)):
+def scene_obstacles_snippet(request: Request, game_id: str, player_id: str, db: Session = Depends(get_session)):
game = crud.get_game(db, game_id)
player = crud.get_player(db, player_id)
if not game or not player:
return HTMLResponse("")
-
- def get_obstacle_by_id(obs_id: str) -> Optional[Obstacle]:
- return db.get(Obstacle, obs_id)
-
hand = crud.get_player_hand(player)
- return templates.TemplateResponse("scene_challenges_snippet.html", {
+ return templates.TemplateResponse("scene_obstacles_snippet.html", {
"request": request,
"game": game,
"player": player,
"hand": hand,
"parse_card": cards.parse_card,
- "get_obstacle_by_id": get_obstacle_by_id,
- "json_loads": json.loads
+ "json_loads": json.loads,
+ "is_captain": crud.is_player_captain
})
@app.get("/game/{game_id}/between/roster", response_class=HTMLResponse)
@@ -174,7 +157,13 @@ def between_scenes_roster_snippet(request: Request, game_id: str, db: Session =
# --- HTMX View Polling Render Route ---
@app.get("/game/{game_id}/player/{player_id}/view", response_class=HTMLResponse)
-def get_game_phase_view(request: Request, game_id: str, player_id: str, db: Session = Depends(get_session)):
+def get_game_phase_view(
+ request: Request,
+ game_id: str,
+ player_id: str,
+ edit: bool = False,
+ db: Session = Depends(get_session)
+):
game = crud.get_game(db, game_id)
player = crud.get_player(db, player_id)
if not game or not player:
@@ -220,7 +209,8 @@ def get_game_phase_view(request: Request, game_id: str, player_id: str, db: Sess
"is_captain": crud.is_player_captain,
"json_loads": json.loads,
"deck_count": len(crud.get_game_deck(game)),
- "inbox_tasks": inbox_tasks
+ "inbox_tasks": inbox_tasks,
+ "edit_mode": edit
}
# Select template based on current game phase
@@ -272,7 +262,7 @@ def player_save_basic_details(
db.add(player)
db.commit()
- return HTMLResponse(status_code=204)
+ return RedirectResponse(url=f"/game/{game_id}/player/{player_id}/view", status_code=status.HTTP_303_SEE_OTHER)
# 3. Edit basic character details (reset values to trigger form view)
@app.post("/game/{game_id}/player/{player_id}/edit-basic")
@@ -281,18 +271,8 @@ def player_edit_basic_details(
player_id: str,
db: Session = Depends(get_session)
):
- player = crud.get_player(db, player_id)
- if not player:
- raise HTTPException(status_code=404, detail="Player not found")
-
- player.avatar_look = ""
- player.avatar_smell = ""
- player.first_words = ""
- db.add(player)
- db.commit()
-
- # Trigger HTMX reload
- return RedirectResponse(url=f"/game/{game_id}/player/{player_id}/view", status_code=status.HTTP_303_SEE_OTHER)
+ # Trigger HTMX reload with edit mode active
+ return RedirectResponse(url=f"/game/{game_id}/player/{player_id}/view?edit=true", status_code=status.HTTP_303_SEE_OTHER)
# Auto-assign unassigned delegated tasks (Like and Hate) randomly to crewmates
@app.post("/game/{game_id}/player/{player_id}/delegate/auto")
@@ -458,7 +438,6 @@ def player_get_delegation_status(
# 5. Answer a delegated question for another player
@app.post("/game/{game_id}/player/{player_id}/submit-delegate/{target_player_id}/{question_type}")
def player_submit_delegated_answer(
- request: Request,
game_id: str,
player_id: str, # The answering player
target_player_id: str, # The player whose sheet we are filling out
@@ -467,28 +446,7 @@ def player_submit_delegated_answer(
db: Session = Depends(get_session)
):
crud.submit_delegated_answer(db, target_player_id, question_type, answer.strip(), player_id)
-
- # Re-fetch data to render the updated inbox snippet
- game = crud.get_game(db, game_id)
- player = crud.get_player(db, player_id)
- if not game or not player:
- raise HTTPException(status_code=404, detail="Not found")
-
- # Compute inbox tasks
- inbox_tasks = []
- for p in game.players:
- if p.id != player.id:
- if p.other_like_from_player_id == player.id and not p.other_like:
- inbox_tasks.append({"player": p, "type": "like"})
- if p.other_hate_from_player_id == player.id and not p.other_hate:
- inbox_tasks.append({"player": p, "type": "hate"})
-
- return templates.TemplateResponse("inbox_snippet.html", {
- "request": request,
- "game": game,
- "player": player,
- "inbox_tasks": inbox_tasks
- })
+ return HTMLResponse("")
# 6. Submit 3 Secret Pirate Techniques
@app.post("/game/{game_id}/player/{player_id}/submit-techniques")
@@ -545,15 +503,16 @@ def player_assign_face_techniques(
})
# 8. Set Role during Scene Setup
-@app.post("/game/{game_id}/player/{player_id}/set-role")
+@app.post("/game/{game_id}/player/{player_id}/set-role", response_class=HTMLResponse)
def player_set_role(
+ request: Request,
game_id: str,
player_id: str,
role: str,
db: Session = Depends(get_session)
):
crud.update_scene_role(db, player_id, role)
- return HTMLResponse(status_code=204)
+ return get_game_phase_view(request, game_id, player_id, db=db)
# 9. Start Scene (Verify Setup & Shuffle & Draw Obstacles)
@app.post("/game/{game_id}/scene/start")
@@ -569,29 +528,16 @@ def start_scene_route(request: Request, game_id: str, db: Session = Depends(get_
return HTMLResponse(status_code=204)
-# 10. Deep creates a new challenge
-@app.post("/game/{game_id}/challenge/create")
-def create_challenge_route(
- game_id: str,
- title: str = Form(...),
- description: str = Form(""),
- obstacle_ids: List[str] = Form(...),
- db: Session = Depends(get_session)
-):
- crud.create_challenge(db, game_id, title.strip(), description.strip(), obstacle_ids)
- return HTMLResponse(status_code=204)
-
-# 11. Pi-Rat plays card against an obstacle in a challenge
+# 11. Pi-Rat plays card against an obstacle
@app.post("/game/{game_id}/player/{player_id}/play-card")
def play_card_route(
game_id: str,
player_id: str,
- challenge_id: str = Form(...),
obstacle_id: str = Form(...),
card_code: str = Form(...),
db: Session = Depends(get_session)
):
- ok, msg, res = crud.play_card_on_obstacle(db, player_id, obstacle_id, card_code, challenge_id)
+ ok, msg, res = crud.play_card_on_obstacle(db, player_id, obstacle_id, card_code)
if not ok:
return HTMLResponse(f"
{msg}
", status_code=400)
@@ -603,7 +549,8 @@ def play_card_route(
f""
f"
{color_prefix} {res['details']}{drew_text}
"
f"
Click to dismiss"
- f"
"
+ f"",
+ headers={"HX-Trigger": "hand-updated"}
)
# 12. Pi-Rat plays a Joker to replace an obstacle
@@ -615,10 +562,7 @@ def play_joker_route(
obstacle_id: str = Form(...),
db: Session = Depends(get_session)
):
- # Retrieve any active challenge that contains this obstacle to use as a dummy,
- # or let play_card_on_obstacle handle challenge_id=None.
- # Wait, we can pass challenge_id = "" and check in play_card if it exists.
- ok, msg, res = crud.play_card_on_obstacle(db, player_id, obstacle_id, card_code, "")
+ ok, msg, res = crud.play_card_on_obstacle(db, player_id, obstacle_id, card_code)
if not ok:
return HTMLResponse(f"{msg}
", status_code=400)
@@ -626,7 +570,8 @@ def play_joker_route(
f""
f"
🃏 Play Joker: Discarded obstacle and drew a new replacement!
"
f"
Click to dismiss"
- f"
"
+ f"",
+ headers={"HX-Trigger": "hand-updated"}
)
# 13. Toggle Objective Completion Checklist
diff --git a/src/pirats/models.py b/src/pirats/models.py
index 7db8e6b..e20ff1a 100644
--- a/src/pirats/models.py
+++ b/src/pirats/models.py
@@ -12,7 +12,6 @@ class Game(SQLModel, table=True):
players: List["Player"] = Relationship(back_populates="game", cascade_delete=True)
obstacles: List["Obstacle"] = Relationship(back_populates="game", cascade_delete=True)
- challenges: List["Challenge"] = Relationship(back_populates="game", cascade_delete=True)
votes: List["Vote"] = Relationship(back_populates="game", cascade_delete=True)
class Player(SQLModel, table=True):
@@ -65,16 +64,14 @@ class Obstacle(SQLModel, table=True):
game: Optional[Game] = Relationship(back_populates="obstacles")
-class Challenge(SQLModel, table=True):
- id: str = Field(default_factory=lambda: str(uuid.uuid4()), primary_key=True)
- game_id: str = Field(foreign_key="game.id", index=True)
- title: str = Field(...)
- description: str = Field(default="")
- applied_obstacle_ids: str = Field(default="[]") # JSON string of list of obstacle IDs
- resolved_by_player_id: Optional[str] = Field(default=None) # Player ID if resolved (for logging/history)
- is_active: bool = Field(default=True)
-
- game: Optional[Game] = Relationship(back_populates="challenges")
+ @property
+ def success_count(self) -> int:
+ import json
+ try:
+ cards_list = json.loads(self.played_cards)
+ return sum(1 for c in cards_list if c.get("success") is True)
+ except Exception:
+ return 0
class Vote(SQLModel, table=True):
id: str = Field(default_factory=lambda: str(uuid.uuid4()), primary_key=True)
diff --git a/src/pirats/static/css/style.css b/src/pirats/static/css/style.css
index 65ae6df..1d210ae 100644
--- a/src/pirats/static/css/style.css
+++ b/src/pirats/static/css/style.css
@@ -578,6 +578,10 @@ body {
gap: 0.5rem;
}
+.inbox-list:has(.inbox-item) .inbox-empty-message {
+ display: none;
+}
+
.inbox-item {
padding: 1rem;
margin-bottom: 0.75rem;
@@ -763,12 +767,18 @@ body {
padding: 1.25rem;
background: rgba(7, 11, 18, 0.4);
display: grid;
- grid-template-columns: 0.8fr 2fr 1fr;
+ grid-template-columns: 0.8fr 2fr 1fr 1fr;
gap: 1rem;
position: relative;
overflow: hidden;
}
+@media (max-width: 768px) {
+ .obstacle-item {
+ grid-template-columns: 0.8fr 2fr 1.5fr;
+ }
+}
+
@media (max-width: 600px) {
.obstacle-item {
grid-template-columns: 1fr;
@@ -828,6 +838,21 @@ body {
padding: 0.5rem;
}
+.obstacle-successes-display {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ background: rgba(0, 255, 135, 0.04);
+ border: 1px dashed rgba(0, 255, 135, 0.25);
+ border-radius: 6px;
+ padding: 0.5rem;
+}
+
+.obstacle-successes-display .val-number {
+ color: var(--neon-emerald);
+}
+
.val-label {
font-size: 0.75rem;
color: var(--text-muted);
@@ -842,7 +867,7 @@ body {
}
.played-column {
- grid-column: span 3;
+ grid-column: span 4;
border-top: 1px solid rgba(255,255,255,0.05);
padding-top: 0.75rem;
margin-top: 0.5rem;
@@ -1028,6 +1053,71 @@ body {
margin-top: 1.5rem;
}
+/* Card Widget (Medium) */
+.card-medium {
+ width: 75px;
+ height: 112px;
+ border-radius: 6px;
+ border: 1.5px solid var(--glass-border);
+ background: linear-gradient(135deg, var(--bg-ocean-light) 0%, var(--bg-dark) 100%);
+ box-shadow: 0 3px 10px rgba(0,0,0,0.4);
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ padding: 0.3rem;
+ position: relative;
+ font-size: 0.85rem;
+}
+
+.card-medium .card-corner {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ line-height: 1;
+}
+
+.card-medium .card-corner .val {
+ font-weight: 900;
+ font-size: 0.9rem;
+}
+
+.card-medium .card-corner .suit {
+ font-size: 0.75rem;
+}
+
+.card-medium.suit-c, .card-medium.suit-s {
+ border-color: rgba(226, 232, 240, 0.15);
+}
+
+.card-medium.suit-h, .card-medium.suit-d {
+ border-color: rgba(255, 42, 95, 0.15);
+}
+
+.card-medium.joker-card {
+ border-color: var(--gold);
+ background: linear-gradient(135deg, #1d1b10 0%, var(--bg-dark) 100%);
+}
+
+.card-medium.suit-c .val, .card-medium.suit-c .suit,
+.card-medium.suit-s .val, .card-medium.suit-s .suit {
+ color: var(--black-suit);
+}
+
+.card-medium.suit-h .val, .card-medium.suit-h .suit,
+.card-medium.suit-d .val, .card-medium.suit-d .suit {
+ color: var(--red-suit);
+}
+
+.card-medium .card-center {
+ font-size: 1.8rem;
+ text-align: center;
+ align-self: center;
+ margin: auto 0;
+}
+
+.card-medium.suit-c .card-center, .card-medium.suit-s .card-center { color: var(--black-suit); }
+.card-medium.suit-h .card-center, .card-medium.suit-d .card-center { color: var(--red-suit); }
+
/* Card Widget (Large) */
.card-large {
width: 110px;
@@ -1335,3 +1425,307 @@ body {
.select-small { padding: 0.5rem; font-size: 0.9rem; flex: 1; }
.select-xsmall { padding: 0.4rem; font-size: 0.85rem; }
.btn-small { padding: 0.4rem 0.8rem; font-size: 0.85rem; }
+
+/* --- Visual & Tactile Technique Assignment --- */
+.face-tech-drag-form {
+ margin-top: 1.5rem;
+}
+
+.available-techniques-container {
+ background: rgba(255, 255, 255, 0.02);
+ border: 1px dashed var(--glass-border);
+ border-radius: 12px;
+ padding: 1.25rem;
+ margin-bottom: 2rem;
+ box-shadow: inset 0 0 15px rgba(0,0,0,0.2);
+}
+
+.available-techniques-container h4 {
+ font-family: var(--font-heading);
+ color: var(--gold);
+ font-size: 1.1rem;
+ margin-bottom: 0.25rem;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+}
+
+.instruction-help {
+ font-size: 0.8rem;
+ color: var(--text-muted);
+ margin-bottom: 1rem;
+}
+
+.techniques-drag-pool {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.75rem;
+ justify-content: center;
+ min-height: 50px;
+ align-items: center;
+}
+
+.draggable-tech-chip {
+ background: linear-gradient(135deg, rgba(22, 36, 59, 0.9) 0%, rgba(13, 23, 38, 0.9) 100%);
+ border: 1px solid var(--gold);
+ color: var(--text-primary);
+ padding: 0.6rem 1rem;
+ border-radius: 8px;
+ cursor: grab;
+ user-select: none;
+ font-family: var(--font-body);
+ font-size: 0.95rem;
+ font-weight: 500;
+ transition: var(--transition-smooth);
+ box-shadow: 0 4px 10px rgba(0,0,0,0.3), 0 0 5px rgba(212, 175, 55, 0.1);
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+}
+
+.draggable-tech-chip:hover {
+ transform: translateY(-2px);
+ border-color: var(--neon-cyan);
+ box-shadow: 0 6px 15px rgba(0, 0, 0, 0.4), 0 0 10px rgba(0, 242, 254, 0.3);
+}
+
+.draggable-tech-chip:active {
+ cursor: grabbing;
+}
+
+.draggable-tech-chip.dragging {
+ opacity: 0.4;
+ border-style: dashed;
+}
+
+.draggable-tech-chip.selected {
+ border-color: var(--neon-cyan);
+ box-shadow: 0 0 15px rgba(0, 242, 254, 0.5);
+ background: rgba(0, 242, 254, 0.1);
+}
+
+.draggable-tech-chip.assigned-hidden {
+ opacity: 0.2;
+ pointer-events: none;
+ cursor: not-allowed;
+ border-color: var(--text-muted);
+}
+
+.drag-icon {
+ color: var(--gold);
+ font-size: 0.8rem;
+ opacity: 0.7;
+}
+
+/* Slots Grid */
+.face-cards-slots-grid {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: 1.5rem;
+ margin-bottom: 2rem;
+}
+
+@media (max-width: 768px) {
+ .face-cards-slots-grid {
+ grid-template-columns: 1fr;
+ }
+}
+
+.face-card-slot-wrapper {
+ display: flex;
+ justify-content: center;
+}
+
+.face-card-slot {
+ position: relative;
+ width: 100%;
+ max-width: 220px;
+ aspect-ratio: 2 / 3;
+ min-height: 280px;
+ background: linear-gradient(135deg, rgba(13, 23, 38, 0.8) 0%, rgba(7, 11, 18, 0.9) 100%);
+ border: 2px dashed rgba(212, 175, 55, 0.3);
+ border-radius: 12px;
+ padding: 1rem;
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ transition: var(--transition-smooth);
+ overflow: hidden;
+ cursor: pointer;
+ box-shadow: 0 10px 25px rgba(0, 0, 0, 0.4);
+}
+
+.face-card-slot:hover {
+ border-color: rgba(212, 175, 55, 0.7);
+ box-shadow: 0 12px 30px rgba(212, 175, 55, 0.15);
+ transform: translateY(-4px);
+}
+
+.face-card-slot.drag-over {
+ border-color: var(--neon-cyan);
+ border-style: solid;
+ background: rgba(0, 242, 254, 0.05);
+ box-shadow: 0 0 20px rgba(0, 242, 254, 0.2);
+ transform: scale(1.02);
+}
+
+.face-card-slot.has-assignment {
+ border-style: solid;
+ border-color: var(--gold);
+ box-shadow: 0 10px 25px rgba(0, 0, 0, 0.5), 0 0 15px rgba(212, 175, 55, 0.15);
+}
+
+.card-bg-letter {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ font-family: var(--font-heading);
+ font-size: 8rem;
+ font-weight: 900;
+ color: rgba(255, 255, 255, 0.025);
+ line-height: 1;
+ pointer-events: none;
+ user-select: none;
+ transition: var(--transition-smooth);
+}
+
+.face-card-slot.has-assignment .card-bg-letter {
+ color: rgba(212, 175, 55, 0.04);
+}
+
+.card-slot-header, .card-slot-footer {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ font-family: var(--font-heading);
+ font-weight: 700;
+ font-size: 1.1rem;
+ color: var(--text-muted);
+ pointer-events: none;
+ user-select: none;
+}
+
+.face-card-slot.has-assignment .card-slot-header,
+.face-card-slot.has-assignment .card-slot-footer {
+ color: var(--gold);
+}
+
+.card-slot-body {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ text-align: center;
+ z-index: 2;
+ padding: 0.5rem 0;
+}
+
+.card-title {
+ font-family: var(--font-heading);
+ font-size: 1.2rem;
+ font-weight: 700;
+ color: var(--text-primary);
+ margin-bottom: 0.75rem;
+ pointer-events: none;
+}
+
+.face-card-slot.has-assignment .card-title {
+ color: var(--gold);
+}
+
+.drop-zone-placeholder {
+ font-size: 0.8rem;
+ color: var(--text-muted);
+ border: 1px dashed rgba(255, 255, 255, 0.1);
+ background: rgba(0, 0, 0, 0.2);
+ border-radius: 6px;
+ padding: 0.5rem 0.75rem;
+ transition: var(--transition-smooth);
+ pointer-events: none;
+}
+
+.face-card-slot:hover .drop-zone-placeholder {
+ color: var(--text-primary);
+ border-color: rgba(255, 255, 255, 0.2);
+}
+
+.assigned-tech-content {
+ width: 100%;
+ display: none;
+}
+
+.face-card-slot.has-assignment .drop-zone-placeholder {
+ display: none;
+}
+
+.face-card-slot.has-assignment .assigned-tech-content {
+ display: block;
+}
+
+.assigned-tech-chip {
+ background: rgba(212, 175, 55, 0.08);
+ border: 1px solid var(--gold);
+ color: var(--text-primary);
+ padding: 0.5rem;
+ border-radius: 8px;
+ font-size: 0.85rem;
+ position: relative;
+ box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3);
+ word-break: break-word;
+ animation: fadeIn 0.3s ease;
+}
+
+.remove-tech-btn {
+ position: absolute;
+ top: -8px;
+ right: -8px;
+ width: 18px;
+ height: 18px;
+ border-radius: 50%;
+ background: #c0392b;
+ border: 1px solid #e74c3c;
+ color: white;
+ font-size: 10px;
+ font-weight: 900;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ box-shadow: 0 2px 5px rgba(0,0,0,0.5);
+ transition: var(--transition-smooth);
+}
+
+.remove-tech-btn:hover {
+ background: #e74c3c;
+ transform: scale(1.1);
+}
+
+@keyframes fadeIn {
+ from { opacity: 0; transform: scale(0.9); }
+ to { opacity: 1; transform: scale(1); }
+}
+
+/* Drag and Drop Interface Styles */
+.card-large[draggable="true"] {
+ cursor: grab;
+}
+
+.card-large[draggable="true"]:active {
+ cursor: grabbing;
+}
+
+.card-large.dragging {
+ opacity: 0.4;
+ border-style: dashed;
+ transform: scale(0.95);
+}
+
+.obstacle-item.drag-over {
+ border-color: var(--gold) !important;
+ background: rgba(212, 175, 55, 0.1) !important;
+ box-shadow: 0 0 20px rgba(212, 175, 55, 0.2) !important;
+ transform: translateY(-2px) scale(1.01);
+ transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
+}
+
diff --git a/src/pirats/templates/character_creation_partial.html b/src/pirats/templates/character_creation_partial.html
index 6eae02d..7adc939 100644
--- a/src/pirats/templates/character_creation_partial.html
+++ b/src/pirats/templates/character_creation_partial.html
@@ -16,7 +16,7 @@
I. Basic Rat Records
- {% if player.avatar_look and player.avatar_smell and player.first_words %}
+ {% if player.avatar_look and player.avatar_smell and player.first_words and not edit_mode %}
Look: {{ player.avatar_look }}
@@ -30,7 +30,10 @@
{% else %}
-