diff --git a/clients/web/assets/style.css b/clients/web/assets/style.css index 24df8c0..1e0406b 100644 --- a/clients/web/assets/style.css +++ b/clients/web/assets/style.css @@ -1291,7 +1291,7 @@ a:hover { text-decoration: underline; } pointer-events: auto; display: flex; flex-direction: column; - align-items: flex-end; + align-items: center; gap: 3px; animation: scoring-panel-enter 0.3s ease-out; } @@ -1889,8 +1889,7 @@ a:hover { text-decoration: underline; } } .free-mode-error { - display: flex; - align-items: center; + text-align: center; gap: 0.75rem; background: rgba(180, 60, 30, 0.12); border: 1px solid rgba(180, 60, 30, 0.4); @@ -1900,7 +1899,6 @@ a:hover { text-decoration: underline; } box-sizing: border-box; } .free-mode-error-msg { - flex: 1; font-family: var(--font-ui); font-size: 0.85rem; color: #8b2000; @@ -2303,3 +2301,219 @@ a:hover { text-decoration: underline; } background: rgba(200,164,72,0.1); font-weight: 600; } + +/* Prevent horizontal scrollbar from the full-bleed strip */ +.game-overlay { overflow-x: hidden !important; } + +/* Board bar: hide die slots, keep the rail as a thin divider */ +.bar-die-slot { display: none !important; } +.board-bar { width: 5px; overflow: hidden; } + +/* ── Full-width in-flow player strip ─────────────────────────────────── */ +.players-strip { + width: 100vw; + margin-top: -1.5rem; /* undo game-overlay top padding */ + display: flex; + align-items: center; + background: var(--ui-parchment); + border-bottom: 2px solid var(--ui-gold-dark); + box-shadow: 0 2px 8px rgba(0,0,0,0.18); + padding: 0.35rem 1.5rem; + gap: 0.5rem; +} + +.strip-player { display: flex; align-items: center; flex: 1; min-width: 0; } +.strip-player-left { justify-content: flex-end; } +.strip-player-right { justify-content: flex-start; } + +.strip-active-zone { + display: flex; + align-items: center; + gap: 0.7rem; + border-radius: 8px; + padding: 0.28rem 0.5rem; + transition: background 0.15s; +} +.strip-active-zone.active { background: rgba(58,42,10,0.08); } + +/* Checker-style circles */ +.strip-avatar { + width: 38px; height: 38px; + border-radius: 50%; + flex-shrink: 0; +} +.strip-avatar-me { + background-image: + radial-gradient(ellipse 50% 35% at 36% 30%, rgba(255,255,255,0.65) 0%, transparent 100%), + radial-gradient(circle, transparent 68%, rgba(160,130,70,0.22) 68.5%, rgba(160,130,70,0.22) 71.5%, transparent 72%), + radial-gradient(circle, transparent 43%, rgba(160,130,70,0.17) 43.5%, rgba(160,130,70,0.17) 46.5%, transparent 47%), + radial-gradient(circle at 38% 32%, #ffffff 0%, var(--checker-white) 52%, #c0b288 100%); + border: 1.8px solid var(--checker-ring); + box-shadow: 0 2px 6px rgba(0,0,0,0.35), inset 0 -1px 3px rgba(0,0,0,0.15); +} +.strip-avatar-opp { + background-image: + radial-gradient(ellipse 40% 28% at 36% 30%, rgba(110,65,30,0.38) 0%, transparent 100%), + radial-gradient(circle, transparent 68%, rgba(200,164,72,0.18) 68.5%, rgba(200,164,72,0.18) 71.5%, transparent 72%), + radial-gradient(circle, transparent 43%, rgba(200,164,72,0.13) 43.5%, rgba(200,164,72,0.13) 46.5%, transparent 47%), + radial-gradient(circle at 38% 32%, #4a2e1a 0%, #1c1008 45%, var(--checker-black) 100%); + border: 1.8px solid var(--checker-ring); + box-shadow: 0 2px 6px rgba(0,0,0,0.5), inset 0 -1px 3px rgba(0,0,0,0.4); +} + +/* Strip peg overrides */ +.players-strip .peg-track { gap: 3px; } +.players-strip .peg-hole { width: 12px; height: 12px; } +.players-strip .peg-hole.filled { + background: #5aab38; border-color: #3a7828; + box-shadow: 0 0 5px rgba(90,171,56,0.55); +} +.players-strip .peg-hole.peg-opp.filled { + background: #c05030; border-color: #8a3018; + box-shadow: 0 0 5px rgba(192,80,48,0.55); +} + +/* Strip score-row-name: remove fixed width from v01 */ +.players-strip .score-row-name { width: auto; } + +/* No ghost bar below pts-counter in the strip */ +.players-strip .pts-counter-wrap { padding-bottom: 0; } + +/* Center "Trictrac" title */ +.players-strip-center { + flex-shrink: 0; + padding: 0 1rem; + border-left: 1px solid rgba(138,106,40,0.2); + border-right: 1px solid rgba(138,106,40,0.2); +} +.strip-title { + font-family: var(--font-display); + font-size: 1.4rem; + font-weight: 600; + font-style: italic; + color: var(--ui-ink); + letter-spacing: 0.03em; + white-space: nowrap; +} + +/* ── Body: board + controls ──────────────────────────────────────────── */ +.main-body { + display: flex; + align-items: flex-start; + gap: 0.5rem; +} + +/* ── Controls column (sidebar on wide, row on narrow) ────────────────── */ +.controls { + display: flex; + flex-direction: column; + gap: 0.5rem; + align-self: stretch; +} +@media (min-width: 920px) { + .controls { + width: 200px; + } +} + +.ctrl-dice { + background: var(--board-rail); + border-radius: 5px; + border-top: 2px solid var(--ui-gold-dark); + box-shadow: 0 2px 6px rgba(0,0,0,0.2); + padding: 0.6rem 0.75rem 0.75rem; + display: flex; + flex-direction: column; + align-items: center; + gap: 0.5rem; + flex-shrink: 0; +} +.ctrl-dice-row { + display: flex; + gap: 0.55rem; + align-items: center; + justify-content: center; +} + +/* Free-mode toggle: light text on dark board-rail background */ +.ctrl-dice .free-mode-toggle { + color: var(--ui-parchment); + font-size: 0.7rem; + flex-wrap: wrap; + justify-content: center; + text-align: center; + gap: 0.3rem; +} +.ctrl-dice .free-mode-help { + border-color: rgba(242,232,208,0.35); + color: rgba(242,232,208,0.5); +} + +.ctrl-status { + background: var(--ui-parchment); + border-radius: 5px; + border-top: 2px solid var(--ui-gold-dark); + box-shadow: 0 2px 6px rgba(0,0,0,0.2); + padding: 0.65rem 0.75rem 0.75rem; + display: flex; + flex-direction: column; + justify-content: space-around; + align-items: center; + gap: 0.4rem; + flex: 1; + min-width: 0; +} +.ctrl-status .game-status { + color: var(--ui-ink); + text-shadow: none; + font-size: 1rem; + padding: 0; + width: auto; + text-align: center; + line-height: 1.3; +} +.ctrl-status .board-actions { + flex-wrap: wrap; + justify-content: center; + min-height: 0; +} +.ctrl-status .game-sub-prompt { + color: #887766; + padding: 0; + width: auto; + text-align: center; + font-size: 0.67rem; + line-height: 1.4; + margin: 0; +} + +.scoring-row .scoring-panels-container { + position: static; + top: auto; left: auto; right: auto; bottom: auto; + z-index: auto; + padding: 0; + pointer-events: auto; + display: flex; + flex-direction: column; + align-items: stretch; + gap: 4px; +} +.scoring-row .scoring-panel { + box-sizing: border-box; + margin: 0; +} + +/* ── Responsive: ≤919px → controls becomes a bottom bar ─────────────── */ +@media (max-width: 919px) { + .main-body { + flex-direction: column; + align-items: stretch; + } + .controls { + flex-direction: row; + width: 100%; + } + .ctrl-status { flex: 1; } + /* Hide pegs on small screens to save space in the strip */ + .players-strip .peg-track { display: none; } +} diff --git a/clients/web/src/game/components/board.rs b/clients/web/src/game/components/board.rs index 34266ca..b614789 100644 --- a/clients/web/src/game/components/board.rs +++ b/clients/web/src/game/components/board.rs @@ -39,7 +39,7 @@ fn field_zone_class(field_num: u8) -> &'static str { } /// Returns (d0_used, d1_used) for the bar dice display. -fn bar_matched_dice_used(staged: &[(u8, u8)], dice: (u8, u8)) -> (bool, bool) { +pub(crate) fn bar_matched_dice_used(staged: &[(u8, u8)], dice: (u8, u8)) -> (bool, bool) { let mut d0 = false; let mut d1 = false; for &(from, to) in staged { @@ -251,7 +251,11 @@ fn free_mode_origins_for(board: [i8; 24], staged: &[(u8, u8)], is_white: bool) - (1u8..=24) .filter(|&f| { let v = displayed_value(board, staged, is_white, f); - if is_white { v > 0 } else { v < 0 } + if is_white { + v > 0 + } else { + v < 0 + } }) .collect() } @@ -278,7 +282,11 @@ fn free_mode_dests_for( let &(f0, t0) = &staged[0]; if t0 == 0 { // First move was an exit — can't reliably infer die, offer both - if dice.0 == dice.1 { vec![dice.0] } else { vec![dice.0, dice.1] } + if dice.0 == dice.1 { + vec![dice.0] + } else { + vec![dice.0, dice.1] + } } else { let dist: u8 = if is_white { t0.saturating_sub(f0) @@ -299,7 +307,11 @@ fn free_mode_dests_for( let opp_present = |f: u8| -> bool { let v = displayed_value(board, staged, is_white, f); - if is_white { v < 0 } else { v > 0 } + if is_white { + v < 0 + } else { + v > 0 + } }; let mut dests = vec![]; @@ -676,23 +688,11 @@ pub fn Board( (&TOP_LEFT_B, &TOP_RIGHT_B, &BOT_LEFT_B, &BOT_RIGHT_B) }; - // Zone label pairs (top-left, top-right, bot-left, bot-right) per perspective. - let (label_tl, label_tr, label_bl, label_br) = if is_white { - ("", "jan de retour", "grand jan", "petit jan") - } else { - ("petit jan", "grand jan", "jan de retour", "") - }; - view! { // board-wrapper keeps zone labels outside .board so the SVG overlay // inside .board stays correctly positioned (position:absolute top:0 left:0 // is relative to .board, not the wrapper).
-
-
{label_tl}
-
-
{label_tr}
-
{fields_from(tl, true)}
@@ -833,11 +833,6 @@ pub fn Board( }) }}
-
-
{label_bl}
-
-
{label_br}
-
} } diff --git a/clients/web/src/game/components/game_screen.rs b/clients/web/src/game/components/game_screen.rs index 7a73edf..e04e8c4 100644 --- a/clients/web/src/game/components/game_screen.rs +++ b/clients/web/src/game/components/game_screen.rs @@ -4,15 +4,17 @@ use std::collections::VecDeque; use futures::channel::mpsc::UnboundedSender; use gloo_storage::Storage as _; use leptos::prelude::*; -use trictrac_store::{Board as StoreBoard, CheckerMove, Color, Dice as StoreDice, Jan, MoveError, MoveRules}; +use trictrac_store::{ + Board as StoreBoard, CheckerMove, Color, Dice as StoreDice, Jan, MoveError, MoveRules, +}; +use super::board::{bar_matched_dice_used, Board}; use super::die::Die; use crate::app::{GameUiState, NetCommand, PauseReason}; use crate::game::trictrac::types::{PlayerAction, PreGameRollState, SerStage, SerTurnStage}; use crate::i18n::*; use crate::portal::lobby::{qr_svg, room_url}; -use super::board::Board; use super::score_panel::MergedScorePanel; use super::scoring::ScoringPanel; @@ -47,14 +49,9 @@ pub fn GameScreen(state: GameUiState) -> impl IntoView { let pending = use_context::>>().expect("pending not found in context"); let cmd_tx_effect = cmd_tx.clone(); - // Non-reactive counter so we can detect when staged_moves grows without - // returning a value from the Effect (which causes a Leptos reactive loop - // when the Effect also writes to the same signal it reads). let prev_staged_len = Cell::new(0usize); // ── Free-play mode ───────────────────────────────────────────────────────── - // When enabled the board shows all own-checker fields as valid origins and - // invalid moves produce an explanatory error rather than being suppressed. fn load_free_mode() -> bool { gloo_storage::LocalStorage::get::("trictrac_free_mode").unwrap_or(false) } @@ -62,13 +59,11 @@ pub fn GameScreen(state: GameUiState) -> impl IntoView { gloo_storage::LocalStorage::set("trictrac_free_mode", val).ok(); } let free_mode: RwSignal = RwSignal::new(load_free_mode()); - // None = no error; Some(None) = generic invalid; Some(Some(e)) = specific rule error let move_error: RwSignal>> = RwSignal::new(None); Effect::new(move |_| { let moves = staged_moves.get(); let n = moves.len(); - // Play checker sound whenever a move is added (own moves, immediate feedback). if n > prev_staged_len.get() { crate::game::sound::play_checker_move(); } @@ -81,7 +76,6 @@ pub fn GameScreen(state: GameUiState) -> impl IntoView { let m2 = to_cm(&moves[1]); if free_mode.get_untracked() { - // Mirror moves to White-perspective for validation (MoveRules always works as White) let (vm1, vm2) = if player_id == 0 { (m1, m2) } else { @@ -90,14 +84,17 @@ pub fn GameScreen(state: GameUiState) -> impl IntoView { let mut store_board = StoreBoard::new(); store_board.set_positions(&Color::White, vs_board); let store_dice = StoreDice { values: vs_dice }; - let color = if player_id == 0 { Color::White } else { Color::Black }; + let color = if player_id == 0 { + Color::White + } else { + Color::Black + }; let rules = MoveRules::new(&color, &store_board, store_dice); if rules.moves_follow_rules(&(vm1, vm2)) { cmd_tx_effect .unbounded_send(NetCommand::Action(PlayerAction::Move(m1, m2))) .ok(); } else { - // moves_allowed gives the specific TricTrac rule that was broken (if any) let specific_err = rules.moves_allowed(&(vm1, vm2)).err(); move_error.set(Some(specific_err)); } @@ -109,20 +106,11 @@ pub fn GameScreen(state: GameUiState) -> impl IntoView { staged_moves.set(vec![]); selected_origin.set(None); - // Reset the counter so the next turn starts clean. prev_staged_len.set(0); } }); // ── Auto-roll effect ───────────────────────────────────────────────────── - // GameScreen is fully re-mounted on every ViewState update (state is a - // plain prop, not a signal), so this effect fires exactly once per - // RollDice phase entry and will not double-send. - // Guard: suppressed while waiting_for_confirm — the AfterOpponentMove - // buffered state shows the human's RollDice turn but the auto-roll must - // wait until the buffer is drained and the live screen state is shown. - // Guard: never auto-roll during the pre-game ceremony (the ceremony overlay - // has its own Roll button for PlayerAction::PreGameRoll). let show_roll = is_my_turn && vs.turn_stage == SerTurnStage::RollDice && vs.stage != SerStage::PreGameRoll; if show_roll && !waiting_for_confirm { @@ -141,14 +129,11 @@ pub fn GameScreen(state: GameUiState) -> impl IntoView { let cmd_tx_go = cmd_tx.clone(); let cmd_tx_end_quit = cmd_tx.clone(); let cmd_tx_end_replay = cmd_tx.clone(); - // Only show the fallback Go button when there is no ScoringPanel showing it. let show_hold_go = is_my_turn && vs.turn_stage == SerTurnStage::HoldOrGoChoice && state.my_scored_event.is_none(); // ── Valid move sequences for this turn ───────────────────────────────────── - // Computed once per ViewState snapshot; used by Board (highlighting) and the - // empty-move button (visibility). let valid_sequences: Vec<(CheckerMove, CheckerMove)> = if is_move_stage && dice != (0, 0) { let mut store_board = StoreBoard::new(); store_board.set_positions(&Color::White, vs.board); @@ -170,14 +155,13 @@ pub fn GameScreen(state: GameUiState) -> impl IntoView { } else { vec![] }; - // Clone for the empty-move button reactive closure (Board consumes the original). let valid_seqs_empty = valid_sequences.clone(); // ── Scores ───────────────────────────────────────────────────────────────── let my_score = vs.scores[player_id as usize].clone(); let opp_score = vs.scores[1 - player_id as usize].clone(); - // ── Ceremony state (extracted before vs is moved into Board) ──────────────── + // ── Ceremony state ────────────────────────────────────────────────────────── let is_ceremony = vs.stage == SerStage::PreGameRoll; let pre_game_roll_data: Option = vs.pre_game_roll.clone(); let my_name_ceremony = my_score.name.clone(); @@ -188,8 +172,6 @@ pub fn GameScreen(state: GameUiState) -> impl IntoView { let my_scored_event = state.my_scored_event.clone(); let opp_scored_event = state.opp_scored_event.clone(); - // Values for MergedScorePanel — extracted before events are consumed. - // Don't animate points when a hole was gained (points wrap around 12). let my_pts_earned: u8 = my_scored_event.as_ref().map_or(0, |e| { if e.holes_gained == 0 { e.points_earned @@ -214,7 +196,6 @@ pub fn GameScreen(state: GameUiState) -> impl IntoView { let last_moves = state.last_moves; - // fields where a battue (hit) was scored; ripple animation shown there. let hit_fields: Vec = { let is_hit_jan = |jan: &Jan| { matches!( @@ -246,10 +227,7 @@ pub fn GameScreen(state: GameUiState) -> impl IntoView { fields }; - // ── Sound effects (fire once on mount = once per state snapshot) ────────── - // Dice roll: dice are fresh for the currently active player (Move stage means - // someone just rolled). Skipped on turn-switch states where the old dice linger - // in RollDice/MarkPoints stage before the opponent has rolled. + // ── Sound effects ────────────────────────────────────────────────────────── let active_is_move_stage = matches!( vs.turn_stage, SerTurnStage::Move | SerTurnStage::HoldOrGoChoice @@ -257,12 +235,9 @@ pub fn GameScreen(state: GameUiState) -> impl IntoView { if show_dice && last_moves.is_none() && active_is_move_stage && !suppress_dice_anim { crate::game::sound::play_dice_roll(); } - // Checker move: moves were committed in the preceding action. if last_moves.is_some() { crate::game::sound::play_checker_move(); } - // Scoring: hole fanfare plays immediately; per-point ticks are driven by - // MergedScorePanel's counter animation so play_points_scored is not called here. if let Some(ref ev) = my_scored_event { if ev.holes_gained > 0 { crate::game::sound::play_hole_scored(); @@ -282,6 +257,13 @@ pub fn GameScreen(state: GameUiState) -> impl IntoView { let room_id = state.room_id.clone(); let is_bot_game = state.is_bot_game; + // ── Active player indicator ──────────────────────────────────────────────── + let active_player_is_me: Option = if stage == SerStage::InGame { + Some(is_my_turn) + } else { + None + }; + // ── Game-over info ───────────────────────────────────────────────────────── let stage_is_ended = stage == SerStage::Ended; let winner_is_me = my_score.holes >= 12; @@ -303,8 +285,8 @@ pub fn GameScreen(state: GameUiState) -> impl IntoView { }; view! { - // ── Game container ────────────────────────────────────────────────────
+ // ── Share popover (while waiting for opponent) ─────────────────── {(!is_bot_game && stage == SerStage::PreGame).then(|| { let url_label = share_url.clone(); @@ -346,20 +328,197 @@ pub fn GameScreen(state: GameUiState) -> impl IntoView { } })} - // ── Merged scoreboard + scoring panels ───────────── - // score-area is position:relative so the scoring-panels-container - // can be absolute-positioned at the right of the hole counter. -
- + + // ── Board + controls (sidebar on wide, footer on narrow) ───────── +
+ - // Scoring detail panels — stacked at the right, overlapping if needed. + + // ── Controls: dice card + status/actions card ──────────────── +
+ {show_dice.then(|| view! { +
+
+ {move || { + let staged = staged_moves.get(); + let (u0, u1) = if suppress_dice_anim { + (true, true) + } else if is_move_stage { + bar_matched_dice_used(&staged, dice) + } else { + (false, false) + }; + view! { + + + } + }} +
+ +
+ })} + +
+
+ {move || { + if let Some(ref reason) = pause_reason { + return String::from(match reason { + PauseReason::AfterOpponentRoll => t_string!(i18n, after_opponent_roll), + PauseReason::AfterOpponentGo => t_string!(i18n, after_opponent_go), + PauseReason::AfterOpponentMove => t_string!(i18n, after_opponent_move), + PauseReason::AfterOpponentPreGameRoll => t_string!(i18n, after_opponent_pre_game_roll), + }); + } + let n = staged_moves.get().len(); + if is_move_stage { + t_string!(i18n, select_move, n = n + 1) + } else { + String::from(match (&stage, is_my_turn, &turn_stage) { + (SerStage::Ended, _, _) => t_string!(i18n, game_over), + (SerStage::PreGame, _, _) | (SerStage::PreGameRoll, _, _) => t_string!(i18n, waiting_for_opponent), + (SerStage::InGame, true, SerTurnStage::RollDice) => t_string!(i18n, your_turn_roll), + (SerStage::InGame, true, SerTurnStage::HoldOrGoChoice) => t_string!(i18n, hold_or_go), + (SerStage::InGame, true, _) => t_string!(i18n, your_turn), + (SerStage::InGame, false, _) => t_string!(i18n, opponent_turn), + }) + } + }} +
+ {move || { + let hint: String = if waiting_for_confirm { + t_string!(i18n, hint_continue).to_owned() + } else if is_move_stage { + t_string!(i18n, hint_move).to_owned() + } else if is_my_turn && turn_stage_for_sub == SerTurnStage::HoldOrGoChoice { + t_string!(i18n, hint_hold_or_go).to_owned() + } else { + String::new() + }; + (!hint.is_empty()).then(|| view! {

{hint}

}) + }} + // ── Free-mode error banner ───────────────────────────── + {move || { + move_error.get().map(|opt_err| { + let msg: String = match opt_err { + None => t_string!(i18n, err_invalid_move).to_owned(), + Some(MoveError::OpponentCorner) => t_string!(i18n, err_opponent_corner).to_owned(), + Some(MoveError::CornerNeedsTwoCheckers) => t_string!(i18n, err_corner_needs_two).to_owned(), + Some(MoveError::CornerByEffectPossible) => t_string!(i18n, err_corner_by_effect).to_owned(), + Some(MoveError::ExitNeedsAllCheckersOnLastQuarter) => t_string!(i18n, err_exit_needs_all_in_last_jan).to_owned(), + Some(MoveError::ExitByEffectPossible) => t_string!(i18n, err_exit_by_effect).to_owned(), + Some(MoveError::ExitNotFarthest) => t_string!(i18n, err_exit_not_farthest).to_owned(), + Some(MoveError::OpponentCanFillQuarter) => t_string!(i18n, err_opponent_can_fill_quarter).to_owned(), + Some(MoveError::MustFillQuarter) => t_string!(i18n, err_must_fill_quarter).to_owned(), + Some(MoveError::MustPlayAllDice) => t_string!(i18n, err_must_play_all_dice).to_owned(), + Some(MoveError::MustPlayStrongerDie) => t_string!(i18n, err_must_play_stronger_die).to_owned(), + }; + view! { +
+ {msg} + +
+ } + }) + }} +
+ {waiting_for_confirm.then(|| view! { + + })} + {show_hold_go.then(|| view! { + + })} + {move || { + let staged = staged_moves.get(); + let show = is_move_stage && staged.len() < 2 && ( + valid_seqs_empty.is_empty() || match staged.len() { + 0 => valid_seqs_empty.iter().any(|(m1, _)| m1.get_from() == 0), + 1 => { + let (f0, t0) = staged[0]; + valid_seqs_empty.iter() + .filter(|(m1, _)| { + m1.get_from() as u8 == f0 + && m1.get_to() as u8 == t0 + }) + .any(|(_, m2)| m2.get_from() == 0) + } + _ => false, + } + ); + show.then(|| view! { + + }) + }} + {move || { + (is_move_stage && staged_moves.get().len() == 1).then(|| view! { + + }) + }} +
+
+
+
+ + // ── Scoring notification panels ─────────────────────────────────── +
{my_scored_event.map(|event| view! { @@ -370,157 +529,6 @@ pub fn GameScreen(state: GameUiState) -> impl IntoView {
- // ── Board ──────────────────────────────────────────────────────── - - - // ── Status, hints, and actions — cream strip below board ─ -
-
- {move || { - if let Some(ref reason) = pause_reason { - return String::from(match reason { - PauseReason::AfterOpponentRoll => t_string!(i18n, after_opponent_roll), - PauseReason::AfterOpponentGo => t_string!(i18n, after_opponent_go), - PauseReason::AfterOpponentMove => t_string!(i18n, after_opponent_move), - PauseReason::AfterOpponentPreGameRoll => t_string!(i18n, after_opponent_pre_game_roll), - }); - } - let n = staged_moves.get().len(); - if is_move_stage { - t_string!(i18n, select_move, n = n + 1) - } else { - String::from(match (&stage, is_my_turn, &turn_stage) { - (SerStage::Ended, _, _) => t_string!(i18n, game_over), - (SerStage::PreGame, _, _) | (SerStage::PreGameRoll, _, _) => t_string!(i18n, waiting_for_opponent), - (SerStage::InGame, true, SerTurnStage::RollDice) => t_string!(i18n, your_turn_roll), - (SerStage::InGame, true, SerTurnStage::HoldOrGoChoice) => t_string!(i18n, hold_or_go), - (SerStage::InGame, true, _) => t_string!(i18n, your_turn), - (SerStage::InGame, false, _) => t_string!(i18n, opponent_turn), - }) - } - }} -
- {move || { - let hint: String = if waiting_for_confirm { - t_string!(i18n, hint_continue).to_owned() - } else if is_move_stage { - t_string!(i18n, hint_move).to_owned() - } else if is_my_turn && turn_stage_for_sub == SerTurnStage::HoldOrGoChoice { - t_string!(i18n, hint_hold_or_go).to_owned() - } else { - String::new() - }; - (!hint.is_empty()).then(|| view! {

{hint}

}) - }} - // ── Free-mode error banner ───────────────────────────────────── - {move || { - move_error.get().map(|opt_err| { - let msg: String = match opt_err { - None => t_string!(i18n, err_invalid_move).to_owned(), - Some(MoveError::OpponentCorner) => t_string!(i18n, err_opponent_corner).to_owned(), - Some(MoveError::CornerNeedsTwoCheckers) => t_string!(i18n, err_corner_needs_two).to_owned(), - Some(MoveError::CornerByEffectPossible) => t_string!(i18n, err_corner_by_effect).to_owned(), - Some(MoveError::ExitNeedsAllCheckersOnLastQuarter) => t_string!(i18n, err_exit_needs_all_in_last_jan).to_owned(), - Some(MoveError::ExitByEffectPossible) => t_string!(i18n, err_exit_by_effect).to_owned(), - Some(MoveError::ExitNotFarthest) => t_string!(i18n, err_exit_not_farthest).to_owned(), - Some(MoveError::OpponentCanFillQuarter) => t_string!(i18n, err_opponent_can_fill_quarter).to_owned(), - Some(MoveError::MustFillQuarter) => t_string!(i18n, err_must_fill_quarter).to_owned(), - Some(MoveError::MustPlayAllDice) => t_string!(i18n, err_must_play_all_dice).to_owned(), - Some(MoveError::MustPlayStrongerDie) => t_string!(i18n, err_must_play_stronger_die).to_owned(), - }; - view! { -
- {msg} - -
- } - }) - }} -
- {waiting_for_confirm.then(|| view! { - - })} - // Fallback Go button when no scoring panel (e.g. after reconnect) - {show_hold_go.then(|| view! { - - })} - {move || { - let staged = staged_moves.get(); - let show = is_move_stage && staged.len() < 2 && ( - valid_seqs_empty.is_empty() || match staged.len() { - 0 => valid_seqs_empty.iter().any(|(m1, _)| m1.get_from() == 0), - 1 => { - let (f0, t0) = staged[0]; - valid_seqs_empty.iter() - .filter(|(m1, _)| { - m1.get_from() as u8 == f0 - && m1.get_to() as u8 == t0 - }) - .any(|(_, m2)| m2.get_from() == 0) - } - _ => false, - } - ); - show.then(|| view! { - - }) - }} - {move || { - (is_move_stage && staged_moves.get().len() == 1).then(|| view! { - - }) - }} -
- // ── Free-play mode toggle ───────────────────────────────────── - -
- // ── Pre-game ceremony overlay ───────────────────────────────────── {is_ceremony.then(|| { let pgr = pre_game_roll_data.unwrap_or(PreGameRollState { diff --git a/clients/web/src/game/components/score_panel.rs b/clients/web/src/game/components/score_panel.rs index bd531f3..94e5f8a 100644 --- a/clients/web/src/game/components/score_panel.rs +++ b/clients/web/src/game/components/score_panel.rs @@ -32,19 +32,18 @@ pub fn jan_label(jan: &Jan) -> String { } } -/// Merged scoreboard showing both players above the board. +/// Full-width player strip at the top of the game screen. /// -/// - Two stacked rows for a clear race-to-12 visual comparison. -/// - Points shown as an animated jackpot counter (ticks up on each new point). -/// - Hole pegs are larger and use green (me) / red (opponent) instead of gold. -/// - When a hole is gained, the new peg pops in and a brief non-blocking label -/// appears instead of the old blocking toast popup. +/// - Left side: me (right-aligned toward center): avatar → name → pegs → pts. +/// - Center: "Trictrac" italic title. +/// - Right side: opponent (left-aligned from center): pts → pegs → name → avatar. +/// - Active player zone gets a subtle rounded highlight. +/// - Points animate as a jackpot counter; new peg pops in with an animation. #[component] pub fn MergedScorePanel( my_score: PlayerScore, opp_score: PlayerScore, - /// Points just earned this turn; 0 = no animation. Set to 0 when a hole - /// was gained (points wrap around 12, counter stays at end value). + /// Points just earned this turn; 0 = no animation. #[prop(default = 0)] my_points_earned: u8, #[prop(default = 0)] opp_points_earned: u8, @@ -55,14 +54,13 @@ pub fn MergedScorePanel( /// True when my hole was scored under bredouille (shows ×2 in the flash). #[prop(default = false)] my_bredouille: bool, + /// `Some(true)` = my turn active, `Some(false)` = opponent active, `None` = no active turn. + #[prop(default = None)] + active_player_is_me: Option, ) -> impl IntoView { let i18n = use_i18n(); // ── Points counter signals ────────────────────────────────────────────── - // When no hole was gained: start from (current - earned) and tick up. - // When a hole was gained: points wrapped around 12, so skip the animation. - // On non-WASM there is no animation; start directly at the final value. - // Suppress the unused-variable warning for animation-only params. #[cfg(not(target_arch = "wasm32"))] let _ = (my_points_earned, opp_points_earned); #[cfg(not(target_arch = "wasm32"))] @@ -122,10 +120,6 @@ pub fn MergedScorePanel( } } - // ── Ghost bar widths (show the end value immediately — static reference) ─ - let my_bar_style = format!("width:{}%", (my_score.points as u32 * 100 / 12).min(100)); - let opp_bar_style = format!("width:{}%", (opp_score.points as u32 * 100 / 12).min(100)); - // ── Hole peg tracks ───────────────────────────────────────────────────── let my_holes = my_score.holes; let opp_holes = opp_score.holes; @@ -163,73 +157,77 @@ pub fn MergedScorePanel( let my_can_bredouille = my_score.can_bredouille; let opp_can_bredouille = opp_score.can_bredouille; + let my_active = active_player_is_me == Some(true); + let opp_active = active_player_is_me == Some(false); + view! { -
+
- // ── My player row ─────────────────────────────────────────── -
-
- {my_name} - {t!(i18n, you_suffix)} -
-
-
-
+ // ── My player: left side, right-aligned toward center ─────────── +
+
+
+
+ {my_name} + {t!(i18n, you_suffix)}
-
- {move || my_displayed_pts.get()} - "/12" -
-
-
{my_pegs}
- {my_can_bredouille.then(|| view! { - - "B" - - })} - // Flash sits in the free space to the right of the pegs. - // margin-left:auto keeps it right-aligned inside the flex row - // without adding a new row, so the board never shifts down. - {(my_holes_gained > 0).then(|| { - let label = if my_bredouille { - format!("Trou {} · ×2 bredouille", my_holes) - } else { - format!("Trou {}", my_holes) - }; - view! { -
- {label} + {my_can_bredouille.then(|| view! { + + "B" + + })} +
{my_pegs}
+
+
+ {move || my_displayed_pts.get()} + "/12"
- } - })} +
+ {(my_holes_gained > 0).then(|| { + let label = if my_bredouille { + format!("Trou {} · ×2 bredouille", my_holes) + } else { + format!("Trou {}", my_holes) + }; + view! { +
+ {label} +
+ } + })} +
-
- - // ── Opponent row ──────────────────────────────────────────── -
-
- {opp_name} -
-
-
-
-
-
- {move || opp_displayed_pts.get()} - "/12" -
-
-
{opp_pegs}
- {opp_can_bredouille.then(|| view! { - - "B" - - })} + // ── Center title ──────────────────────────────────────────────── +
+ "Trictrac"
+ + // ── Opponent: right side, left-aligned from center ────────────── +
+
+
+
+ {move || opp_displayed_pts.get()} + "/12" +
+
+
{opp_pegs}
+ {opp_can_bredouille.then(|| view! { + + "B" + + })} +
+ {opp_name} +
+
+
+
+
} } diff --git a/devenv.lock b/devenv.lock index 3f0905b..e6e8ef6 100644 --- a/devenv.lock +++ b/devenv.lock @@ -17,62 +17,6 @@ "type": "github" } }, - "flake-compat": { - "flake": false, - "locked": { - "lastModified": 1767039857, - "owner": "NixOS", - "repo": "flake-compat", - "rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab", - "type": "github" - }, - "original": { - "owner": "NixOS", - "repo": "flake-compat", - "type": "github" - } - }, - "git-hooks": { - "inputs": { - "flake-compat": "flake-compat", - "gitignore": "gitignore", - "nixpkgs": [ - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1778507602, - "owner": "cachix", - "repo": "git-hooks.nix", - "rev": "61ab0e80d9c7ab14c256b5b453d8b3fb0189ba0a", - "type": "github" - }, - "original": { - "owner": "cachix", - "repo": "git-hooks.nix", - "type": "github" - } - }, - "gitignore": { - "inputs": { - "nixpkgs": [ - "git-hooks", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1762808025, - "owner": "hercules-ci", - "repo": "gitignore.nix", - "rev": "cb5e3fdca1de58ccbc3ef53de65bd372b48f567c", - "type": "github" - }, - "original": { - "owner": "hercules-ci", - "repo": "gitignore.nix", - "type": "github" - } - }, "nixpkgs": { "locked": { "lastModified": 1779102034, @@ -108,15 +52,11 @@ "root": { "inputs": { "devenv": "devenv", - "git-hooks": "git-hooks", "nixpkgs": "nixpkgs", - "nixpkgs-cmake3": "nixpkgs-cmake3", - "pre-commit-hooks": [ - "git-hooks" - ] + "nixpkgs-cmake3": "nixpkgs-cmake3" } } }, "root": "root", "version": 7 -} +} \ No newline at end of file diff --git a/doc/design/snapshots/2026-06-06-7b036e3ee1a8d3d686a664d74717fd23df1d3169.html b/doc/design/snapshots/2026-06-06-7b036e3ee1a8d3d686a664d74717fd23df1d3169.html new file mode 100644 index 0000000..4ac9d36 --- /dev/null +++ b/doc/design/snapshots/2026-06-06-7b036e3ee1a8d3d686a664d74717fd23df1d3169.html @@ -0,0 +1,153 @@ + + + + + + Trictrac + + + + + + + +
Anonymous (you)
6/12
Trictrac
6/12
Bot
jan de retour
13
14
15
16
17
18
19
20
21
22
23
24
8
12
11
10
9
8
7
6
5
4
3
2
1
11
grand jan
petit jan
Move a checker (1 of 2)

Click a highlighted field to move a checker

Cannot play in a quarter the opponent can still fill
+2 pts
True hit (big jan)simple×1+2
diff --git a/doc/design/snapshots/2026-06-06-7b036e3ee1a8d3d686a664d74717fd23df1d3169_files/style-b42680e382d603c7.css b/doc/design/snapshots/2026-06-06-7b036e3ee1a8d3d686a664d74717fd23df1d3169_files/style-b42680e382d603c7.css new file mode 100644 index 0000000..58db762 --- /dev/null +++ b/doc/design/snapshots/2026-06-06-7b036e3ee1a8d3d686a664d74717fd23df1d3169_files/style-b42680e382d603c7.css @@ -0,0 +1,2528 @@ +/* ── Google Fonts ───────────────────────────────────────────────── */ +@import url('https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,400;0,600;1,400&family=Jost:wght@300;400;500&display=swap'); + +/* ── Design tokens ──────────────────────────────────────────────────── */ +:root { + --board-felt: #1d3d28; + --board-rail: #2a1508; + --field-ivory: #f0e6c8; + --field-burgundy: #7a1e2a; + --field-blue: #e5eadc; + --field-blue-light: #1a4f72; + --field-brown: #f2dfa0; + --field-brown-light: #6a2810; + --field-corner: #b8900a; + --checker-white: #f5edd8; + --checker-black: #1a0f06; + --checker-ring: #c8a448; + --ui-parchment: #f2e8d0; + --ui-parchment-dark: #e4d8b8; + --ui-ink: #2a1a08; + --ui-gold: #c8a448; + --ui-gold-dark: #8a6a28; + --ui-green-accent: #3a6b2a; + --ui-red-accent: #7a1e2a; + --font-display: 'Cormorant Garamond', Georgia, serif; + --font-ui: 'Jost', system-ui, sans-serif; +} + + +/* ── Reset & base ──────────────────────────────────────────────────── */ +*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } + +body { + font-family: var(--font-ui); + background: #8a7050; + background-image: + radial-gradient(ellipse at 20% 10%, rgba(80,48,16,0.35) 0%, transparent 60%), + radial-gradient(ellipse at 80% 90%, rgba(40,24,8,0.3) 0%, transparent 55%), + repeating-linear-gradient( + 45deg, transparent, transparent 3px, + rgba(0,0,0,0.03) 3px, rgba(0,0,0,0.03) 4px + ); + display: flex; + flex-direction: column; + min-height: 100vh; +} + +.hidden { display: none !important; } + +/* -- svg icons -- */ +.icon { + width: 1.2em; + height: 1.2em; + color: var(--ui-parchment); + vertical-align: -0.25em; + margin-right: 0.7em; +} + +/* ── Site navigation ─────────────────────────────────────────────── */ +.site-nav { + background: var(--board-rail); + border-bottom: 2px solid var(--ui-gold-dark); + padding: 0 1.5rem; + height: 52px; + display: flex; + align-items: center; + gap: 1.5rem; + position: sticky; + top: 0; + z-index: 50; + box-shadow: 0 2px 8px rgba(0,0,0,0.4); + flex-shrink: 0; +} + +.site-nav-brand { + font-family: var(--font-display); + font-size: 1.5rem; + font-weight: 600; + color: var(--ui-gold); + text-decoration: none; + letter-spacing: 0.1em; +} +.site-nav-brand:hover { color: #e0b840; } + +.site-nav-spacer { flex: 1; } + +.site-nav a { + font-family: var(--font-ui); + font-size: 0.9rem; + color: var(--ui-parchment); + text-decoration: none; + opacity: 0.8; + transition: opacity 0.15s, color 0.15s; +} +.site-nav a:hover { opacity: 1; } + +.site-nav-btn { + padding: 0.3rem 0.9rem; + font-family: var(--font-ui); + font-size: 0.85rem; + font-weight: 500; + letter-spacing: 0.03em; + border: 1px solid rgba(200,164,72,0.4); + border-radius: 4px; + background: transparent; + color: var(--ui-parchment); + cursor: pointer; + transition: background 0.15s, border-color 0.15s; +} +.site-nav-btn:hover { + background: rgba(200,164,72,0.12); + border-color: var(--ui-gold); +} + +/* ── Portal main content area ────────────────────────────────────── */ +.portal-main { + flex: 1; + max-width: 900px; + width: 100%; + margin: 2rem auto; + padding: 0 1.5rem; +} + +.portal-card { + background: var(--ui-parchment); + border-radius: 8px; + box-shadow: + 0 20px 60px rgba(0,0,0,0.55), + 0 0 3px 3px rgba(42,21,8,0.9) + ; + /* box-shadow: 0 4px 16px rgba(0,0,0,0.18); */ + /* border: 1px solid rgba(200,164,72,0.3); */ + /* border-top: 3px solid var(--ui-gold-dark); */ + padding: 1.75rem 2rem; + margin-bottom: 1.5rem; +} + +.portal-card h1 { + font-family: var(--font-display); + font-size: 2rem; + font-weight: 600; + color: var(--ui-ink); + letter-spacing: 0.04em; + margin-bottom: 0.25rem; +} +.portal-card h2 { + font-family: var(--font-display); + font-size: 1.35rem; + font-weight: 600; + color: var(--ui-ink); + margin-bottom: 0.75rem; +} + +.portal-meta { + font-size: 0.85rem; + color: #665544; + margin-bottom: 1.5rem; + font-family: var(--font-ui); +} + +/* ── Stats grid ──────────────────────────────────────────────────── */ +.stats-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 1rem; + margin-bottom: 1.5rem; +} +.stat-box { + background: var(--ui-parchment); + border: 1px solid rgba(200,164,72,0.28); + border-radius: 6px; + padding: 1rem; + text-align: center; + box-shadow: 0 1px 4px rgba(0,0,0,0.1); +} +.stat-box .value { + font-family: var(--font-display); + font-size: 2.2rem; + font-weight: 600; + color: var(--ui-gold-dark); +} +.stat-box .label { + font-size: 0.78rem; + color: #665544; + margin-top: 0.2rem; + letter-spacing: 0.04em; + text-transform: uppercase; +} + +/* ── Tables ──────────────────────────────────────────────────────── */ +table { + width: 100%; + border-collapse: collapse; + font-size: 0.9rem; + font-family: var(--font-ui); +} +th { + text-align: left; + padding: 0.5rem 0.75rem; + border-bottom: 2px solid rgba(200,164,72,0.4); + color: #665544; + font-weight: 500; + font-size: 0.8rem; + letter-spacing: 0.05em; + text-transform: uppercase; +} +td { + padding: 0.5rem 0.75rem; + border-bottom: 1px solid rgba(200,164,72,0.12); + color: var(--ui-ink); +} +tr:last-child td { border-bottom: none; } +tr:hover td { background: rgba(200,164,72,0.05); } + +a { color: var(--ui-gold-dark); text-decoration: none; } +a:hover { text-decoration: underline; } + +/* ── Outcome classes ─────────────────────────────────────────────── */ +.outcome-win { color: var(--ui-green-accent); font-weight: 600; } +.outcome-loss { color: var(--ui-red-accent); font-weight: 600; } +.outcome-draw { color: #c07020; font-weight: 600; } + +/* ── Portal tabs ─────────────────────────────────────────────────── */ +.portal-tabs { + display: flex; + gap: 0; + margin-bottom: 1.5rem; + border-bottom: 1px solid rgba(200,164,72,0.3); +} +.portal-tab-btn { + padding: 0.55rem 1.5rem; + font-family: var(--font-ui); + font-size: 0.9rem; + background: transparent; + border: none; + cursor: pointer; + color: #665544; + border-bottom: 2px solid transparent; + margin-bottom: -1px; + transition: color 0.15s, border-color 0.15s; +} +.portal-tab-btn.active { + color: var(--ui-ink); + border-bottom-color: var(--ui-gold-dark); + font-weight: 500; +} + +/* ── Portal form ─────────────────────────────────────────────────── */ +.portal-label { + display: block; + font-size: 0.82rem; + color: #665544; + margin-bottom: 0.3rem; + letter-spacing: 0.03em; +} +.portal-input { + width: 100%; + padding: 0.55rem 0.85rem; + font-size: 0.95rem; + font-family: var(--font-ui); + border: 1px solid rgba(138,106,40,0.35); + border-radius: 5px; + background: rgba(255,252,240,0.8); + color: var(--ui-ink); + outline: none; + margin-bottom: 1rem; + transition: border-color 0.15s, box-shadow 0.15s; +} +.portal-input:focus { + border-color: var(--ui-gold); + box-shadow: 0 0 0 3px rgba(200,164,72,0.18); +} +.portal-submit-btn { + padding: 0.6rem 2rem; + font-family: var(--font-ui); + font-size: 0.9rem; + font-weight: 500; + background: linear-gradient(160deg, #4a7a38 0%, #2e5222 100%); + color: #e8f0e0; + border: none; + border-radius: 5px; + cursor: pointer; + box-shadow: 0 2px 5px rgba(0,0,0,0.25); + transition: opacity 0.15s; +} +.portal-submit-btn:disabled { opacity: 0.45; cursor: not-allowed; } +.portal-submit-btn:not(:disabled):hover { opacity: 0.9; } + +.portal-page-btn { + padding: 0.35rem 0.9rem; + font-family: var(--font-ui); + font-size: 0.85rem; + background: var(--board-rail); + color: var(--ui-parchment); + border: none; + border-radius: 4px; + cursor: pointer; + opacity: 0.85; + transition: opacity 0.15s; +} +.portal-page-btn:hover { opacity: 1; } + +.portal-loading { color: #665544; font-style: italic; padding: 1rem 0; } +.portal-empty { color: #aa9070; font-style: italic; padding: 1rem 0; } +.portal-error { color: var(--ui-red-accent); font-size: 0.875rem; margin-top: 0.5rem; } +.portal-success { color: var(--ui-green-accent); font-size: 0.875rem; margin-top: 0.5rem; } + +.flash-banner { + position: fixed; + top: 1.25rem; + left: 50%; + transform: translateX(-50%); + z-index: 500; + display: flex; + align-items: center; + gap: 1rem; + padding: 0.75rem 1.25rem; + background: var(--ui-green-accent); + color: #f5edd8; + border-radius: 6px; + box-shadow: 0 4px 16px rgba(0,0,0,0.35); + font-family: var(--font-ui); + font-size: 0.95rem; + max-width: 90vw; + animation: flash-in 0.2s ease; +} +@keyframes flash-in { + from { opacity: 0; transform: translateX(-50%) translateY(-0.5rem); } + to { opacity: 1; transform: translateX(-50%) translateY(0); } +} +.flash-dismiss { + background: none; + border: none; + color: inherit; + cursor: pointer; + font-size: 1rem; + opacity: 0.75; + padding: 0; + line-height: 1; +} +.flash-dismiss:hover { opacity: 1; } + +.portal-danger-zone { + border: 1px solid rgba(122, 30, 42, 0.4); + background: rgba(122, 30, 42, 0.04); +} +.portal-danger-zone h2 { + color: var(--ui-red-accent); +} +.portal-danger-btn { + padding: 0.5rem 1.25rem; + font-family: var(--font-ui); + font-size: 0.9rem; + background: var(--ui-red-accent); + color: #f5edd8; + border: none; + border-radius: 4px; + cursor: pointer; + transition: opacity 0.15s; +} +.portal-danger-btn:hover { opacity: 0.85; } +.portal-danger-btn:disabled { opacity: 0.45; cursor: not-allowed; } + +.portal-link { + color: var(--ui-gold); + text-decoration: none; + font-size: 0.875rem; +} +.portal-link:hover { text-decoration: underline; } + +.portal-verification-banner { + background: rgba(200,164,72,0.08); + border: 1px solid rgba(200,164,72,0.35); + border-radius: 6px; + padding: 1.25rem; + text-align: center; +} +.portal-verification-banner p { + margin-bottom: 0.75rem; + font-size: 0.9rem; +} + +/* ── Share URL row (lobby waiting card + game top bar) ──────────── */ +.share-url-row { + display: flex; + align-items: center; + gap: 0.5rem; + background: rgba(0,0,0,0.18); + border: 1px solid rgba(200,164,72,0.25); + border-radius: 5px; + padding: 0.4rem 0.6rem; +} +.share-url-text { + flex: 1; + font-family: var(--font-ui); + font-size: 0.72rem; + color: rgba(242,232,208,0.75); + word-break: break-all; + user-select: all; +} +.share-copy-btn { + flex-shrink: 0; + font-family: var(--font-ui); + font-size: 0.72rem; + padding: 0.2rem 0.6rem; + border: 1px solid rgba(200,164,72,0.4); + border-radius: 3px; + background: rgba(200,164,72,0.1); + color: var(--ui-parchment); + cursor: pointer; + transition: background 0.15s; + white-space: nowrap; +} +.share-copy-btn:hover { background: rgba(200,164,72,0.22); } + +/* ── QR code container ───────────────────────────────────────────── */ +.qr-container { + width: 160px; + height: 160px; + margin: 0 auto; + border-radius: 4px; + overflow: hidden; +} +.qr-container svg { width: 100%; height: 100%; display: block; } + +/* ── Share popover (in-game top bar) ─────────────────────────────── */ +.share-popover { + width: 100%; + background: rgba(0,0,0,0.3); + border: 1px solid rgba(200,164,72,0.2); + border-radius: 6px; + padding: 0.75rem 1rem; + display: flex; + flex-direction: column; + align-items: center; + gap: 0.4rem; + margin-bottom: 0.5rem; +} +.share-popover .qr-container { width: 120px; height: 120px; } +.share-popover-label { + font-size: 0.75rem; + color: rgba(242,232,208,0.6); + text-align: center; + margin: 0; +} + +/* ── Game overlay (full-screen, covers portal during play) ───────── */ +.game-overlay { + position: fixed; + inset: 0; + background: #8a7050; + background-image: + radial-gradient(ellipse at 20% 10%, rgba(80,48,16,0.35) 0%, transparent 60%), + radial-gradient(ellipse at 80% 90%, rgba(40,24,8,0.3) 0%, transparent 55%), + repeating-linear-gradient( + 45deg, transparent, transparent 3px, + rgba(0,0,0,0.03) 3px, rgba(0,0,0,0.03) 4px + ); + z-index: 200; + display: flex; + justify-content: center; + align-items: flex-start; + padding: 1.5rem; + overflow-y: auto; +} + +/* ── Login card (§11) ───────────────────────────────────────────────── */ +.login-card { + background: var(--ui-parchment); + border-radius: 8px; + box-shadow: + 0 20px 60px rgba(0,0,0,0.55), + 0 0 3px 3px rgba(42,21,8,0.9) + ; + /* box-shadow: + 0 20px 60px rgba(0,0,0,0.55), + 0 0 0 1px rgba(200,164,72,0.35), + 0 0 0 5px rgba(42,21,8,0.9), + 0 0 0 6px rgba(200,164,72,0.2); + */ + /* border-top: 3px solid var(--ui-gold-dark); */ + width: 340px; + margin-top: 5vh; + overflow: hidden; +} + +/* Decorative header — row of triangular flèches like the actual board */ +.login-card-header { + height: 52px; + background: var(--board-felt); + position: relative; + overflow: hidden; +} + +.login-board-stripe { + position: absolute; + inset: 0; + background: + repeating-linear-gradient( + 90deg, + var(--field-burgundy) 0, var(--field-burgundy) 50%, + var(--field-ivory) 50%, var(--field-ivory) 100% + ); + background-size: 34px 100%; + clip-path: polygon( + 0% 0%, 2.94% 100%, 5.88% 0%, 8.82% 100%, 11.76% 0%, + 14.7% 100%, 17.65% 0%, 20.59% 100%, 23.53% 0%, + 26.47% 100%, 29.41% 0%, 32.35% 100%, 35.29% 0%, + 38.24% 100%, 41.18% 0%, 44.12% 100%, 47.06% 0%, + 50% 100%, 52.94% 0%, 55.88% 100%, 58.82% 0%, + 61.76% 100%, 64.71% 0%, 67.65% 100%, 70.59% 0%, + 73.53% 100%, 76.47% 0%, 79.41% 100%, 82.35% 0%, + 85.29% 100%, 88.24% 0%, 91.18% 100%, 94.12% 0%, + 97.06% 100%, 100% 0% + ); + opacity: 0.9; +} + +.login-card-body { + display: flex; + flex-direction: column; + align-items: center; + gap: 0; + padding: 1.5rem 2rem 2rem; +} + +.login-lang-switcher { + align-self: flex-end; + margin-bottom: 0.75rem; +} + +/* Override lang-switcher colours for the parchment card */ +.login-card .lang-switcher button { + color: var(--ui-ink); + border-color: rgba(42,21,8,0.2); + opacity: 0.5; +} +.login-card .lang-switcher button.lang-active { + opacity: 1; + background: rgba(42,21,8,0.08); + border-color: rgba(42,21,8,0.35); +} + +.login-title { + font-family: var(--font-display); + font-size: 3.25rem; + font-weight: 600; + color: var(--ui-ink); + letter-spacing: 0.12em; + text-align: center; + line-height: 1; + margin-bottom: 0.3rem; +} + +.login-subtitle { + font-family: var(--font-display); + font-size: 0.85rem; + color: rgba(42,26,8,0.55); + text-align: center; + letter-spacing: 0.06em; + font-style: italic; + margin-bottom: 1.25rem; +} +.login-subtitle sup { + font-size: 0.65em; + vertical-align: super; +} + +.login-ornament { + color: var(--ui-gold); + font-size: 1rem; + opacity: 0.7; + margin-bottom: 1.25rem; + letter-spacing: 0.3em; + text-align: center; +} + +.error-msg { + color: #c03030; + font-size: 0.85rem; + text-align: center; + margin-bottom: 0.5rem; +} + +.login-input { + width: 100%; + padding: 0.55rem 0.85rem; + font-size: 0.95rem; + font-family: var(--font-ui); + border: 1px solid rgba(138,106,40,0.4); + border-radius: 5px; + background: rgba(255,252,240,0.8); + color: var(--ui-ink); + outline: none; + transition: border-color 0.15s, box-shadow 0.15s; + margin-bottom: 1rem; +} +.login-input:focus { + border-color: var(--ui-gold); + box-shadow: 0 0 0 3px rgba(200,164,72,0.2); +} + +.login-actions { + display: flex; + flex-direction: column; + gap: 0.55rem; + width: 100%; +} + +/* Login buttons styled as embossed wooden tiles */ +.login-btn { + width: 100%; + padding: 0.65rem 1rem; + font-family: var(--font-ui); + font-size: 0.9rem; + font-weight: 500; + letter-spacing: 0.04em; + border: none; + border-radius: 5px; + cursor: pointer; + transition: opacity 0.15s, transform 0.1s, box-shadow 0.15s; + position: relative; +} +.login-btn:disabled { opacity: 0.35; cursor: default; } +.login-btn:not(:disabled):hover { opacity: 0.92; transform: translateY(-1px); } +.login-btn:not(:disabled):active { transform: translateY(0); } + +.login-btn-primary { + background: linear-gradient(160deg, #4a7a38 0%, #2e5222 100%); + color: #e8f0e0; + box-shadow: 0 2px 6px rgba(0,0,0,0.3), inset 0 1px 0 rgba(255,255,255,0.12); +} +.login-btn-secondary { + background: linear-gradient(160deg, #3a2010 0%, #241408 100%); + color: #e4d4b4; + box-shadow: 0 2px 6px rgba(0,0,0,0.3), inset 0 1px 0 rgba(255,255,255,0.08); +} +.login-btn-bot { + background: linear-gradient(160deg, #2a4a6a 0%, #183050 100%); + color: #d0e0f0; + box-shadow: 0 2px 6px rgba(0,0,0,0.3), inset 0 1px 0 rgba(255,255,255,0.08); +} + +/* ── Connecting screen ──────────────────────────────────────────────── */ +.connecting { + font-family: var(--font-display); + font-size: 1.4rem; + font-style: italic; + margin-top: 4rem; + text-align: center; + color: var(--ui-parchment); + text-shadow: 0 1px 4px rgba(0,0,0,0.4); +} + +/* ── Game-action buttons ─────────────────────────────────────────────── */ +.btn { + padding: 0.5rem 1.25rem; + font-size: 0.95rem; + font-family: var(--font-ui); + font-weight: 500; + letter-spacing: 0.03em; + border: none; + border-radius: 4px; + cursor: pointer; + transition: opacity 0.15s, box-shadow 0.15s; +} +.btn:disabled { opacity: 0.4; cursor: default; } +.btn-primary { background: var(--ui-green-accent); color: #fff; } +.btn-secondary { background: var(--board-rail); color: #e8d8b8; } +.btn-bot { background: #2a5a7a; color: #fff; } +.btn:not(:disabled):hover { + opacity: 0.9; + box-shadow: 0 2px 6px rgba(0,0,0,0.25); +} + +/* ── Game container ─────────────────────────────────────────────────── */ +.game-container { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.6rem; +} + +/* ── Language switcher (in-game) ────────────────────────────────────── */ +.lang-switcher { display: flex; gap: 0.25rem; } + +.lang-switcher button { + font-size: 0.7rem; + font-family: var(--font-ui); + letter-spacing: 0.05em; + padding: 0.15rem 0.4rem; + border: 1px solid rgba(200,164,72,0.3); + border-radius: 3px; + background: transparent; + cursor: pointer; + color: var(--ui-parchment); + opacity: 0.55; +} +.lang-switcher button.lang-active { + opacity: 1; + font-weight: 500; + background: rgba(200,164,72,0.15); + border-color: rgba(200,164,72,0.6); +} + +/* ── Top bar ─────────────────────────────────────────────────────────── */ +.top-bar { + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; + color: var(--ui-parchment); + font-size: 0.85rem; + opacity: 0.8; +} + +.quit-link { + font-size: 0.8rem; + color: var(--ui-parchment); + text-decoration: underline; + text-underline-offset: 2px; + cursor: pointer; + opacity: 0.7; +} +.quit-link:hover { opacity: 1; } + +.playing-as { + font-size: 0.75rem; + color: rgba(242,232,208,0.7); + font-family: var(--font-ui); +} +.playing-as strong { color: rgba(242,232,208,0.9); } + +/* ── Game status bar (§10b) — above board ───────────────────────────── */ +.game-status { + font-family: var(--font-display); + font-size: 1.2rem; + font-style: italic; + color: var(--ui-parchment); + text-align: center; + letter-spacing: 0.04em; + padding: 0.2rem 1rem 0; + width: 100%; + text-shadow: 0 1px 4px rgba(0,0,0,0.4); +} + +/* ── Contextual sub-prompt (§8a) ────────────────────────────────────── */ +.game-sub-prompt { + font-family: var(--font-ui); + font-size: 0.72rem; + color: rgba(240,228,192,0.5); + text-align: center; + letter-spacing: 0.04em; + padding: 0.15rem 1rem 0; + width: 100%; +} + +/* ── Player score panel ─────────────────────────────────────────────── */ +.player-score-panel { + background: var(--ui-parchment); + border-radius: 5px; + padding: 0.45rem 1.25rem; + font-size: 0.88rem; + box-shadow: 0 2px 6px rgba(0,0,0,0.25); + width: 100%; + border-top: 2px solid var(--ui-gold-dark); + display: flex; + align-items: center; + gap: 1.5rem; +} + +.player-score-header { + flex-shrink: 0; + min-width: 90px; +} + +.player-name { + font-family: var(--font-display); + font-weight: 600; + font-size: 1.05rem; + color: var(--ui-ink); + letter-spacing: 0.02em; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + min-width: 0; +} + +.score-bars { display: flex; flex-direction: row; gap: 1.5rem; flex: 1; align-items: center; } + +.score-bar-row { + display: flex; + align-items: center; + gap: 0.5rem; + flex: 1; +} + +.score-bar-label { + font-size: 0.75rem; + color: #665544; + width: 3rem; + text-align: right; + flex-shrink: 0; +} + +/* ── Points bar ─────────────────────────────────────────────────────── */ +.score-bar { + flex: 1; + max-width: 220px; + height: 8px; + background: rgba(0,0,0,0.1); + border-radius: 4px; + overflow: hidden; + flex-shrink: 0; +} + +.score-bar-fill { + height: 100%; + border-radius: 4px; + transition: width 0.35s ease-out; +} + +.score-bar-points { background: linear-gradient(90deg, var(--ui-green-accent), #5a9b3a); } + +.score-bar-value { + font-size: 0.75rem; + color: #665544; + min-width: 2.5rem; + font-variant-numeric: tabular-nums; +} + +/* ── Hole peg tracker (§7a) ─────────────────────────────────────────── */ +.peg-track { + display: flex; + align-items: center; + gap: 3px; + flex-shrink: 0; +} + +.peg-hole { + width: 10px; + height: 10px; + border-radius: 50%; + border: 1.5px solid rgba(138,106,40,0.45); + background: rgba(0,0,0,0.06); + flex-shrink: 0; + transition: background 0.3s ease-out, border-color 0.3s, box-shadow 0.3s; +} + +.peg-hole.filled { + background: var(--ui-gold); + border-color: var(--ui-gold-dark); + box-shadow: 0 0 4px rgba(200,164,72,0.6); +} + +.bredouille-badge { + font-size: 0.62rem; + font-weight: 500; + color: #fff8e0; + background: linear-gradient(135deg, #c88800, #8a5800); + border: 1px solid rgba(200,164,72,0.5); + border-radius: 3px; + padding: 0.1em 0.4em; + letter-spacing: 0.06em; + cursor: default; + box-shadow: 0 1px 3px rgba(0,0,0,0.25); +} + +/* ── Merged scoreboard (both players, above board) ──────────────────── */ +.merged-score-panel { + background: var(--ui-parchment); + border-radius: 5px; + padding: 0.5rem 1.25rem 0.45rem; + font-size: 0.88rem; + box-shadow: 0 2px 6px rgba(0,0,0,0.25); + width: 100%; + border-top: 2px solid var(--ui-gold-dark); + display: flex; + flex-direction: column; + gap: 0.2rem; +} + +.score-row { + display: flex; + align-items: center; + gap: 1rem; +} + +.score-row-name { + width: 120px; + flex-shrink: 0; + display: flex; + align-items: baseline; + gap: 0.35rem; + overflow: hidden; +} + +.you-tag { + font-family: var(--font-ui); + font-size: 0.7rem; + color: #887766; + font-style: italic; + white-space: nowrap; + flex-shrink: 0; +} + +/* ── Jackpot points counter ─────────────────────────────────────────── */ +.pts-counter-wrap { + position: relative; + display: flex; + flex-direction: column; + align-items: center; + width: 72px; + flex-shrink: 0; + padding-bottom: 4px; +} + +.pts-ghost-bar-track { + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 3px; + background: rgba(0,0,0,0.07); + border-radius: 2px; + overflow: hidden; +} + +.pts-ghost-bar-fill { + height: 100%; + background: rgba(58,107,42,0.45); + border-radius: 2px; +} + +.pts-ghost-bar-opp { + background: rgba(122,30,42,0.4); +} + +.pts-counter-row { + display: flex; + align-items: baseline; + gap: 0.1rem; +} + +.pts-counter { + font-family: var(--font-display); + font-size: 1.9rem; + font-weight: 600; + color: var(--ui-ink); + line-height: 1; + font-variant-numeric: tabular-nums; + min-width: 1.4em; + text-align: right; +} + +.pts-max { + font-family: var(--font-ui); + font-size: 0.7rem; + color: #998877; + line-height: 1; + padding-bottom: 2px; +} + +/* ── Hole pegs — larger and coloured (me = green, opp = red) ─────────── */ +.merged-score-panel .peg-track { + gap: 4px; +} + +.merged-score-panel .peg-hole { + width: 14px; + height: 14px; + border-radius: 50%; + border: 1.5px solid rgba(138,106,40,0.3); + background: rgba(0,0,0,0.06); + flex-shrink: 0; + transition: background 0.3s ease-out, border-color 0.3s, box-shadow 0.3s; +} + +.merged-score-panel .peg-hole.filled { + background: #5aab38; + border-color: #3a7828; + box-shadow: 0 0 5px rgba(90,171,56,0.55); +} + +.merged-score-panel .peg-hole.peg-opp.filled { + background: #c05030; + border-color: #8a3018; + box-shadow: 0 0 5px rgba(192,80,48,0.55); +} + +/* Peg pop-in animation when a new hole is scored */ +@keyframes peg-pop { + 0% { transform: scale(0.15); opacity: 0; } + 45% { transform: scale(1.55); } + 70% { transform: scale(0.88); } + 100% { transform: scale(1.0); opacity: 1; } +} + +.merged-score-panel .peg-hole.peg-new { + animation: peg-pop 0.52s cubic-bezier(0.22, 0.61, 0.36, 1) forwards; +} + +/* Thin separator between the two player rows */ +.score-row-sep { + height: 1px; + background: rgba(0,0,0,0.07); + margin: 0.05rem 0; +} + +/* ── Non-blocking hole flash (replaces old toast) ───────────────────── */ +@keyframes hole-flash-in-out { + 0% { opacity: 0; transform: translateY(-3px); } + 14% { opacity: 1; transform: translateY(0); } + 65% { opacity: 1; } + 100% { opacity: 0; transform: translateY(2px); } +} + +.hole-flash { + margin-left: auto; + flex-shrink: 0; + white-space: nowrap; + font-family: var(--font-display); + font-size: 0.88rem; + font-weight: 600; + color: var(--ui-green-accent); + letter-spacing: 0.05em; + animation: hole-flash-in-out 2.5s ease-out forwards; + pointer-events: none; +} + +.hole-flash.hole-flash-bredouille { + color: var(--ui-gold-dark); +} + +/* ── Game bottom strip — status, hints, buttons on cream ────────────── */ +.game-bottom-strip { + background: var(--ui-parchment); + border-radius: 5px; + padding: 0.55rem 1.25rem 0.65rem; + width: 100%; + box-shadow: 0 2px 6px rgba(0,0,0,0.2); + border-top: 2px solid var(--ui-gold-dark); + display: flex; + flex-direction: column; + align-items: center; + gap: 0.35rem; + min-height: 3.2rem; +} + +/* Override text colours for the parchment background context */ +.game-bottom-strip .game-status { + color: var(--ui-ink); + text-shadow: none; + padding: 0; + font-size: 1.05rem; + width: auto; +} + +.game-bottom-strip .game-sub-prompt { + color: #887766; + padding: 0; + width: auto; +} + +/* ── Board + side panel ─────────────────────────────────────────────── */ +.board-and-panel { + position: relative; +} + +.side-panel { + position: absolute; + right: -8px; + top: 10px; + z-index: 20; + display: flex; + flex-direction: column; + gap: 0.5rem; + padding-top: 0.15rem; + pointer-events: none; +} + +.action-buttons { display: flex; flex-direction: column; gap: 0.5rem; } + +/* ── Dice bar ───────────────────────────────────────────────────────── */ +.dice-bar { + display: flex; + align-items: center; + gap: 0.6rem; + padding: 0.4rem 0.6rem; + background: rgba(42,21,8,0.15); + border-radius: 6px; + border: 1px solid rgba(200,164,72,0.2); + width: fit-content; +} + +/* ── Die face (SVG) ─────────────────────────────────────────────────── */ +@keyframes die-tumble { + 0% { transform: rotate(-45deg) scale(0.4) translateY(-8px); opacity: 0; } + 25% { transform: rotate(18deg) scale(1.22) translateY(0); opacity: 1; } + 45% { transform: rotate(-10deg) scale(0.91); } + 62% { transform: rotate(6deg) scale(1.06); } + 76% { transform: rotate(-3deg) scale(0.98); } + 88% { transform: rotate(1.5deg) scale(1.01); } + 100% { transform: rotate(0deg) scale(1); opacity: 1; } +} + +.die-face { + filter: drop-shadow(0 2px 3px rgba(0,0,0,0.3)); + animation: die-tumble 0.55s cubic-bezier(0.22, 0.61, 0.36, 1) both; +} + +.die-face rect { + fill: #fffef0; + stroke: #2a1a00; + stroke-width: 2; + transition: fill 0.18s, stroke 0.18s; +} +.die-face circle { + fill: #1a0a00; + transition: fill 0.18s; +} + +.bar-die-slot { + display: flex; + align-items: center; + justify-content: center; +} + +.die-face.die-double rect { stroke: var(--ui-gold); stroke-width: 2.5; } +.die-face.die-double { + filter: drop-shadow(0 0 6px rgba(200,164,72,0.7)) drop-shadow(0 2px 3px rgba(0,0,0,0.3)); +} + +.die-face.die-used { animation: none; opacity: 0.55; } +.die-face.die-used rect { fill: #d4d0c4; stroke: #9a8a70; } +.die-face.die-used circle { fill: #9a8a70; } + +.die-face .die-question { fill: #1a0a00; font-family: sans-serif; } +.die-face.die-used .die-question { fill: #9a8a70; } + +/* ── Jan panel ──────────────────────────────────────────────────────── */ +.jan-panel { + display: flex; + flex-direction: column; + gap: 2px; + background: var(--ui-parchment); + border-radius: 5px; + padding: 0.4rem 0.9rem; + font-size: 0.88rem; + box-shadow: 0 1px 4px rgba(0,0,0,0.15); + min-width: 260px; + border-top: 2px solid rgba(138,106,40,0.35); +} + +.jan-row { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 2px 4px; + border-radius: 3px; +} +.jan-expandable { cursor: pointer; } +.jan-expandable:hover { background: rgba(0,0,0,0.05); } + +.jan-positive { color: #1a5c1a; } +.jan-negative { color: #8b1a1a; } + +.jan-label { flex: 1; } +.jan-tag { + font-size: 0.72rem; + padding: 0.1em 0.4em; + border-radius: 3px; + background: rgba(0,0,0,0.07); + color: #665544; + white-space: nowrap; +} +.jan-pts { font-weight: 600; text-align: right; min-width: 3rem; } + +.jan-moves { padding: 1px 4px 4px 1rem; display: flex; flex-direction: column; gap: 2px; } +.jan-moves.hidden { display: none; } +.jan-move-line { font-family: monospace; font-size: 0.78rem; color: #555; } + +/* ── Game-over overlay (§12) ────────────────────────────────────────── */ +.game-over-overlay { + position: fixed; + inset: 0; + background: rgba(0,0,0,0.65); + display: flex; + align-items: center; + justify-content: center; + z-index: 100; +} + +@keyframes game-over-appear { + from { transform: translateY(-24px) scale(0.94); opacity: 0; } + to { transform: translateY(0) scale(1); opacity: 1; } +} + +.game-over-box { + background: var(--ui-parchment); + border-radius: 8px; + padding: 2.5rem 3rem; + text-align: center; + box-shadow: 0 12px 40px rgba(0,0,0,0.5), 0 0 0 2px var(--ui-gold-dark); + display: flex; + flex-direction: column; + gap: 1.1rem; + min-width: 300px; + animation: game-over-appear 0.4s cubic-bezier(0.22, 0.61, 0.36, 1); +} + +.game-over-box h2 { + font-family: var(--font-display); + font-size: 2rem; + font-weight: 600; + color: var(--ui-ink); + letter-spacing: 0.06em; +} + +.game-over-winner { + font-family: var(--font-display); + font-size: 1.25rem; + color: var(--ui-green-accent); + font-style: italic; +} + +.game-over-score { + display: flex; + align-items: center; + justify-content: center; + gap: 0.75rem; + padding: 0.6rem 1rem; + background: rgba(0,0,0,0.05); + border-radius: 5px; + border: 1px solid rgba(138,106,40,0.2); +} + +.game-over-score-name { + font-family: var(--font-display); + font-size: 0.9rem; + color: #665544; + font-style: italic; + max-width: 80px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.game-over-score-nums { + font-family: var(--font-display); + font-size: 2.25rem; + font-weight: 600; + color: var(--ui-ink); + letter-spacing: 0.08em; + line-height: 1; +} + +.game-over-actions { display: flex; gap: 0.75rem; justify-content: center; } + +/* ── Score-area: position:relative wrapper for merged panel + scoring ── */ +.score-area { + position: relative; + width: 100%; +} + +/* ── Scoring panels container — right of the hole counter ───────────── */ +/* Stacked column, right-aligned, covering the free space in each row. */ +/* overflow:visible lets tall panels float over the board below. */ +.scoring-panels-container { + position: absolute; + top: 0; + bottom: 0; + right: 0; + display: flex; + flex-direction: column; + justify-content: space-around; + align-items: flex-end; + padding: 4px 8px; + z-index: 10; + pointer-events: none; + overflow: visible; +} + +/* ── Scoring notification panel (§6b) ───────────────────────────────── */ +@keyframes scoring-panel-enter { + from { opacity: 0; transform: translateX(10px); } + to { opacity: 1; transform: translateX(0); } +} + +.scoring-panel-wrapper { + pointer-events: auto; + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 3px; + animation: scoring-panel-enter 0.3s ease-out; +} + +/* "+" expand button: hidden while the panel is expanded */ +.scoring-panel-wrapper:not(.scoring-minimized) .scoring-expand-btn { + display: none; +} + +/* Full panel card: hidden once minimised */ +.scoring-panel-wrapper.scoring-minimized .scoring-panel { + display: none; +} + +/* "+" expand button ─────────────────────────────────────────────────── */ +.scoring-expand-btn { + font-family: var(--font-display); + font-size: 0.9rem; + line-height: 1; + background: var(--ui-parchment); + border: 1.5px solid var(--ui-gold-dark); + border-radius: 3px; + padding: 2px 7px; + cursor: pointer; + color: var(--ui-ink); + opacity: 0.72; + box-shadow: 0 1px 3px rgba(0,0,0,0.18); + transition: opacity 0.15s; +} +.scoring-expand-btn:hover { opacity: 1; } + +/* ── Panel head: scoring total + "−" collapse link ──────────────────── */ +.scoring-panel-head { + display: flex; + align-items: baseline; + gap: 0.5rem; +} + +.scoring-collapse-btn { + font-size: 0.78rem; + line-height: 1; + background: none; + border: none; + cursor: pointer; + color: rgba(0,0,0,0.35); + padding: 0 1px; + margin-left: auto; + flex-shrink: 0; + transition: color 0.15s; +} +.scoring-collapse-btn:hover { color: rgba(0,0,0,0.65); } + +/* ── Inner scoring card ─────────────────────────────────────────────── */ +.scoring-panel { + background: var(--ui-parchment); + border-radius: 5px; + padding: 0.45rem 0.85rem; + font-size: 0.84rem; + box-shadow: 0 2px 8px rgba(0,0,0,0.22); + border-left: 3px solid var(--ui-green-accent); + display: flex; + flex-direction: column; + gap: 4px; + width: 320px; +} + +.scoring-total { + font-family: var(--font-display); + font-weight: 600; + font-size: 1rem; + color: #1a5c1a; + white-space: nowrap; +} + +.scoring-jan-row { + display: flex; + align-items: center; + gap: 0.4rem; + padding: 2px 3px; + border-radius: 3px; + cursor: default; + white-space: nowrap; +} +.scoring-jan-row:hover { background: rgba(0,0,0,0.05); } + +.scoring-panel-opp { border-left-color: var(--board-rail); } +.scoring-panel-opp .scoring-total { color: var(--ui-red-accent); } + +.scoring-hole { + display: flex; + align-items: center; + gap: 0.4rem; + font-weight: 600; + color: var(--ui-red-accent); + margin-top: 3px; + padding-top: 4px; + border-top: 1px solid rgba(0,0,0,0.1); +} + +.hold-go-buttons { display: flex; gap: 0.5rem; margin-top: 4px; } + +@media (min-width: 1492px) { + .side-panel { + right: auto; + left: calc(100% + 1rem); + } +} + +/* ── Board wrapper ──────────────────────────────────────────────────── */ +.board-wrapper { + display: flex; + flex-direction: column; + gap: 3px; +} + +/* ── Zone labels (§2a) ──────────────────────────────────────────────── */ +.zone-labels-row { + display: flex; + gap: 4px; + padding: 0 8px; +} + +.zone-label { + font-family: var(--font-display); + font-size: 0.57rem; + font-style: italic; + color: rgba(240,228,192,0.48); + letter-spacing: 0.1em; + text-align: center; + pointer-events: none; + line-height: 1; +} + +.zone-label-quarter { width: 370px; flex-shrink: 0; } +.zone-label-bar { width: 68px; flex-shrink: 0; } + +/* ── Board ──────────────────────────────────────────────────────────── */ +.board { + background: var(--board-felt); + border: 4px solid var(--board-rail); + border-radius: 6px; + padding: 4px; + display: flex; + flex-direction: column; + gap: 4px; + user-select: none; + box-shadow: + 0 6px 16px rgba(0,0,0,0.5), + inset 0 1px 0 rgba(255,255,255,0.04); + position: relative; +} + +.board-row { display: flex; gap: 4px; } +.board-quarter { display: flex; gap: 2px; } + +.board-bar { + width: 68px; + background: var(--board-rail); + border-radius: 4px; + box-shadow: inset 0 0 6px rgba(0,0,0,0.5); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + overflow: visible; +} + +.board-center-bar { + height: 12px; + background: var(--board-rail); + border-radius: 2px; + box-shadow: inset 0 0 4px rgba(0,0,0,0.4); +} + +/* ── Fields (§1) ────────────────────────────────────────────────────── */ +.field { + --fc: var(--field-ivory); + width: 60px; + height: 180px; + background: transparent; + isolation: isolate; + border-radius: 3px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: flex-end; + padding: 4px 2px; + position: relative; +} + +.field::before { + content: ''; + position: absolute; + inset: 0; + z-index: -1; + background: var(--fc); + clip-path: polygon(0% 100%, 50% 0%, 100% 100%); + transition: background 0.12s; +} + +.top-row .field::before { + clip-path: polygon(0% 0%, 100% 0%, 50% 100%); +} + +.top-row .field { justify-content: flex-start; } + +/* ── Zone alternating colours ────────────────────────────────── */ +.board-quarter .field.zone-petit:nth-child(odd), +.board-quarter .field.zone-grand:nth-child(odd) { --fc: var(--field-burgundy); } +.board-quarter .field.zone-petit:nth-child(even), +.board-quarter .field.zone-grand:nth-child(even) { --fc: var(--field-ivory); } + +.board-quarter .field.zone-opponent:nth-child(odd), +.board-quarter .field.zone-retour:nth-child(odd) { --fc: var(--field-burgundy); } +.board-quarter .field.zone-opponent:nth-child(even), +.board-quarter .field.zone-retour:nth-child(even) { --fc: var(--field-ivory); } + +/* ── Point indicator: first N fields reflect each player's score & bredouille */ +.board-quarter .field.zone-petit.point-bredouille:nth-child(odd), +.board-quarter .field.zone-grand.point-bredouille:nth-child(odd) { --fc: var(--field-blue-light); } +.board-quarter .field.zone-petit.point-bredouille:nth-child(even), +.board-quarter .field.zone-grand.point-bredouille:nth-child(even) { --fc: var(--field-blue); } + +.board-quarter .field.zone-petit.point-no-bredouille:nth-child(odd), +.board-quarter .field.zone-grand.point-no-bredouille:nth-child(odd) { --fc: var(--field-blue-light); } +.board-quarter .field.zone-petit.point-no-bredouille:nth-child(even), +.board-quarter .field.zone-grand.point-no-bredouille:nth-child(even) { --fc: var(--field-blue); } + +.board-quarter .field.zone-opponent.point-bredouille:nth-child(odd), +.board-quarter .field.zone-retour.point-bredouille:nth-child(odd) { --fc: var(--field-blue-light); } +.board-quarter .field.zone-opponent.point-bredouille:nth-child(even), +.board-quarter .field.zone-retour.point-bredouille:nth-child(even) { --fc: var(--field-blue); } + +.board-quarter .field.zone-opponent.point-no-bredouille:nth-child(odd), +.board-quarter .field.zone-retour.point-no-bredouille:nth-child(odd) { --fc: var(--field-blue-light); } +.board-quarter .field.zone-opponent.point-no-bredouille:nth-child(even), +.board-quarter .field.zone-retour.point-no-bredouille:nth-child(even) { --fc: var(--field-blue); } + +.field.corner::after { + content: '♛'; + position: absolute; + z-index: -1; + bottom: 22px; + font-size: 0.7rem; + color: rgba(255,248,210,0.38); + pointer-events: none; + line-height: 1; +} +.top-row .field.corner::after { bottom: auto; top: 22px; } + +@keyframes corner-pulse { + 0%, 100% { filter: drop-shadow(0 0 0px rgba(200,164,72,0)); } + 50% { filter: drop-shadow(0 0 7px rgba(200,164,72,0.55)); } +} +.field.corner.corner-available { + animation: corner-pulse 1.5s ease-in-out infinite; +} + +@keyframes exit-glow { + 0%, 100% { filter: drop-shadow(0 0 0px rgba(232,192,96,0)); } + 50% { filter: drop-shadow(0 0 5px rgba(232,192,96,0.5)); } +} +.field.exit-eligible { + animation: exit-glow 2s ease-in-out infinite; +} + +/* ── Exit sign (§8c) — circle+arrow outside the board ──────────────── */ +.exit-btn { + pointer-events: none; + opacity: 0.3; + transition: opacity 0.2s, transform 0.15s; +} +.exit-btn.exit-active { + pointer-events: auto; + cursor: pointer; + opacity: 1; + animation: exit-btn-pulse 1.4s ease-in-out infinite; +} +.exit-btn.exit-active:hover { + transform: scale(1.1); +} +@keyframes exit-btn-pulse { + 0%, 100% { filter: drop-shadow(0 0 3px rgba(200,160,20,0.3)); } + 50% { filter: drop-shadow(0 0 9px rgba(200,160,20,0.85)); } +} + +.field.jan-hovered { + --fc: rgba(190, 140, 35, 0.8) !important; +} + +@keyframes hit-ripple { + from { transform: translate(-50%, -50%) scale(0.4); opacity: 0.9; } + to { transform: translate(-50%, -50%) scale(2.2); opacity: 0; } +} +.hit-ripple { + position: absolute; + left: 50%; + width: 36px; + height: 36px; + border-radius: 50%; + border: 2px solid rgba(200, 164, 72, 0.9); + pointer-events: none; + animation: hit-ripple 0.5s ease-out forwards; +} +.hit-ripple-top { top: 26px; } +.hit-ripple-bot { bottom: 26px; } + +.field.clickable { + cursor: pointer; +} +.field.clickable:hover { + --fc: rgba(200,170,50,0.18) !important; +} +.field.selected { + /* natural triangle color; tab is the indicator */ +} + +/* ── Tab indicators: small markers at the field's wide base ──────── */ +/* Bot-row: tabs hang below; top-row: tabs hang above. */ +/* The tab sits at ≈ -6px which lands on the board's wooden rail. */ + +.field.clickable::after, +.field.selected::after { + content: ''; + position: absolute; + left: 50%; + transform: translateX(-50%); + width: 22px; + height: 8px; + pointer-events: none; + z-index: 2; +} + +.bot-row .field.clickable::after, +.bot-row .field.selected::after { + bottom: -6px; + top: auto; + border-radius: 0 0 10px 10px; +} +.top-row .field.clickable::after, +.top-row .field.selected::after { + top: -6px; + bottom: auto; + border-radius: 10px 10px 0 0; +} + +/* Possible origin: hollow gold outline */ +.field.clickable:not(.dest):not(.selected)::after { + background: rgba(210,170,30,0.15); + border: 1.5px solid rgba(210,170,30,0.75); + box-shadow: 0 0 4px rgba(210,170,30,0.3); +} + +/* Selected origin: filled amber, breathing glow */ +.field.selected::after { + background: linear-gradient(to bottom, #e8b020, #c07808); + border: 1px solid rgba(255,225,65,0.55); + animation: tab-pulse 1.2s ease-in-out infinite; +} + +@keyframes tab-pulse { + 0%, 100% { box-shadow: 0 0 5px rgba(220,155,15,0.55), 0 0 2px rgba(255,220,50,0.3); } + 50% { box-shadow: 0 0 13px rgba(240,178,22,0.88), 0 0 6px rgba(255,230,60,0.6); } +} + +/* Valid destination: soft ivory/pearl */ +.field.clickable.dest:not(.selected)::after { + background: rgba(240,230,205,0.88); + border: 1.5px solid rgba(190,165,105,0.65); + box-shadow: 0 0 3px rgba(190,165,105,0.2); +} +.field.clickable.dest:not(.selected):hover::after { + background: rgba(228,210,162,0.95); + border-color: rgba(210,175,40,0.72); + box-shadow: 0 0 7px rgba(210,175,40,0.42); +} + +.field-num { + font-size: 0.58rem; + color: rgba(0,0,0,0.28); + position: absolute; + bottom: 3px; + line-height: 1; + font-variant-numeric: tabular-nums; +} + +.board-quarter .field.zone-petit:nth-child(odd) .field-num, +.board-quarter .field.zone-grand:nth-child(odd) .field-num, +.board-quarter .field.zone-retour:nth-child(odd) .field-num, +.board-quarter .field.zone-opponent:nth-child(odd) .field-num { + color: rgba(240,215,190,0.38); +} + +.field.corner .field-num { color: rgba(255,248,200,0.4); } +.top-row .field-num { bottom: auto; top: 3px; } + +/* ── Checkers ───────────────────────────────────────────────────────── */ +.checker-stack { + display: flex; + flex-direction: column; + align-items: center; +} + +.checker { + width: 40px; + height: 40px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 0.78rem; + font-weight: 600; + flex-shrink: 0; +} + +.checker + .checker { margin-top: -4px; } + +.checker.white { + background-image: + radial-gradient(ellipse 50% 35% at 36% 30%, + rgba(255,255,255,0.65) 0%, transparent 100%), + radial-gradient(circle, + transparent 68%, rgba(160,130,70,0.22) 68.5%, + rgba(160,130,70,0.22) 71.5%, transparent 72%), + radial-gradient(circle, + transparent 43%, rgba(160,130,70,0.17) 43.5%, + rgba(160,130,70,0.17) 46.5%, transparent 47%), + radial-gradient(circle at 38% 32%, + #ffffff 0%, var(--checker-white) 52%, #c0b288 100%); + border: 1.8px solid var(--checker-ring); + box-shadow: + 0 2px 6px rgba(0,0,0,0.4), + inset 0 -1px 3px rgba(0,0,0,0.15); + color: #443322; +} + +.checker.black { + background-image: + radial-gradient(ellipse 40% 28% at 36% 30%, + rgba(110,65,30,0.38) 0%, transparent 100%), + radial-gradient(circle, + transparent 68%, rgba(200,164,72,0.18) 68.5%, + rgba(200,164,72,0.18) 71.5%, transparent 72%), + radial-gradient(circle, + transparent 43%, rgba(200,164,72,0.13) 43.5%, + rgba(200,164,72,0.13) 46.5%, transparent 47%), + radial-gradient(circle at 38% 32%, + #4a2e1a 0%, #1c1008 45%, var(--checker-black) 100%); + border: 1.8px solid var(--checker-ring); + box-shadow: + 0 2px 6px rgba(0,0,0,0.55), + inset 0 -1px 3px rgba(0,0,0,0.4); + color: #c8b898; +} + +/* ── Hole toast (§6a) ───────────────────────────────────────────────── */ +@keyframes toast-rise { + from { transform: translate(-50%, -40%); opacity: 0; } + to { transform: translate(-50%, -50%); opacity: 1; } +} +@keyframes toast-fade { + from { opacity: 1; } + to { opacity: 0; pointer-events: none; } +} + +.hole-toast { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background: rgba(22,10,2,0.93); + border: 2px solid var(--ui-gold); + border-radius: 8px; + padding: 1.5rem 3.5rem; + text-align: center; + z-index: 50; + pointer-events: none; + box-shadow: + 0 12px 40px rgba(0,0,0,0.65), + 0 0 0 1px rgba(200,164,72,0.25), + inset 0 1px 0 rgba(200,164,72,0.1); + animation: + toast-rise 0.25s cubic-bezier(0.22, 0.61, 0.36, 1), + toast-fade 0.5s ease-in 1.4s forwards; +} + +.hole-toast-title { + font-family: var(--font-display); + font-size: 3.25rem; + font-weight: 600; + color: var(--ui-gold); + letter-spacing: 0.1em; + line-height: 1; +} + +.hole-toast-count { + font-family: var(--font-display); + font-size: 1.1rem; + color: rgba(200,164,72,0.68); + margin-top: 0.35rem; + letter-spacing: 0.06em; +} + +.hole-toast-bredouille { + font-family: var(--font-ui); + font-size: 0.75rem; + letter-spacing: 0.08em; + color: rgba(200,164,72,0.55); + margin-top: 0.4rem; + text-transform: uppercase; +} + +@keyframes bredouille-shimmer { + 0%, 100% { box-shadow: 0 12px 40px rgba(0,0,0,0.65), 0 0 0 2px rgba(200,164,72,0.4), inset 0 0 0 rgba(200,164,72,0); } + 50% { box-shadow: 0 12px 40px rgba(0,0,0,0.65), 0 0 0 4px rgba(200,164,72,0.7), inset 0 0 24px rgba(200,164,72,0.08); } +} +.hole-toast.hole-toast-bredouille { + border-width: 2.5px; + border-color: var(--ui-gold); + padding: 2rem 4rem; + animation: + toast-rise 0.3s cubic-bezier(0.22, 0.61, 0.36, 1), + bredouille-shimmer 0.9s ease-in-out 0.3s 2, + toast-fade 0.5s ease-in 2.2s forwards; +} +.hole-toast.hole-toast-bredouille .hole-toast-title { font-size: 3.75rem; } +.hole-toast.hole-toast-bredouille .hole-toast-bredouille { + font-size: 0.85rem; + color: rgba(200,164,72,0.8); + letter-spacing: 0.14em; +} + +/* ── Checker slide animation (§4a) ─────────────────────────────────── */ +@keyframes checker-slide-in { + from { transform: translate(var(--slide-dx, 0px), var(--slide-dy, 0px)); } + to { transform: none; } +} +.checker.arriving { + animation: checker-slide-in 0.28s cubic-bezier(0.25, 0.46, 0.45, 0.94); +} +.field:has(.checker.arriving) { + isolation: auto; + z-index: 10; + position: relative; +} + +/* ── Checker lift on selected field (§4b) ───────────────────────────── */ +.field.selected .checker-stack { + transform: translateY(-5px); + filter: drop-shadow(0 8px 12px rgba(0,0,0,0.6)); + transition: transform 0.12s ease-out, filter 0.12s ease-out; +} + +/* ── Action buttons below board (§10c) ──────────────────────────────── */ +.board-actions { + display: flex; + gap: 0.55rem; + justify-content: center; + align-items: center; + flex-wrap: wrap; + min-height: 2rem; +} + +/* ── Free-play mode ─────────────────────────────────────────────────────── */ +.free-mode-toggle { + display: flex; + align-items: center; + gap: 0.4rem; + font-family: var(--font-ui); + font-size: 0.78rem; + color: #887766; + cursor: pointer; + user-select: none; + padding-top: 0.1rem; +} +.free-mode-toggle input[type="checkbox"] { + accent-color: var(--ui-gold); + cursor: pointer; + width: 0.85rem; + height: 0.85rem; +} +.free-mode-help { + display: inline-flex; + align-items: center; + justify-content: center; + width: 1rem; + height: 1rem; + border-radius: 50%; + border: 1px solid #a89880; + font-size: 0.65rem; + font-style: normal; + color: #a89880; + cursor: help; + flex-shrink: 0; +} + +.free-mode-error { + display: flex; + align-items: center; + gap: 0.75rem; + background: rgba(180, 60, 30, 0.12); + border: 1px solid rgba(180, 60, 30, 0.4); + border-radius: 4px; + padding: 0.4rem 0.75rem; + width: 100%; + box-sizing: border-box; +} +.free-mode-error-msg { + flex: 1; + font-family: var(--font-ui); + font-size: 0.85rem; + color: #8b2000; + font-style: italic; +} + +/* ── Pre-game ceremony overlay ──────────────────────────────────────────── */ +.ceremony-overlay { + position: fixed; + inset: 0; + background: rgba(0,0,0,0.65); + display: flex; + align-items: center; + justify-content: center; + z-index: 100; +} + +.ceremony-box { + background: var(--ui-parchment); + border-radius: 8px; + padding: 2.5rem 3rem; + text-align: center; + box-shadow: 0 12px 40px rgba(0,0,0,0.5), 0 0 0 2px var(--ui-gold-dark); + display: flex; + flex-direction: column; + align-items: center; + gap: 1.4rem; + min-width: 300px; + animation: game-over-appear 0.4s cubic-bezier(0.22, 0.61, 0.36, 1); +} + +.ceremony-box h2 { + font-family: var(--font-display); + font-size: 1.8rem; + font-weight: 600; + color: var(--ui-ink); + letter-spacing: 0.06em; +} + +.ceremony-dice { + display: flex; + gap: 3rem; + align-items: flex-end; +} + +.ceremony-die-slot { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.5rem; +} + +.ceremony-die-label { + font-family: var(--font-ui); + font-size: 0.85rem; + color: var(--ui-ink); + font-weight: 500; +} + +.ceremony-tie { + font-family: var(--font-display); + font-size: 1rem; + color: var(--ui-red-accent); + font-style: italic; +} + +.ceremony-result { + font-family: var(--font-display); + font-size: 1.15rem; + font-weight: 600; + color: var(--ui-gold-dark); + letter-spacing: 0.04em; +} + +/* ── Nickname modal (anonymous player name chooser) ─────────────────── */ +.nickname-backdrop { + position: fixed; + inset: 0; + background: rgba(0,0,0,0.6); + display: flex; + align-items: center; + justify-content: center; + z-index: 300; +} + +.nickname-modal { + background: var(--ui-parchment); + border-radius: 8px; + padding: 2rem 2rem 1.75rem; + width: min(360px, 90vw); + display: flex; + flex-direction: column; + gap: 1rem; + box-shadow: + 0 20px 60px rgba(0,0,0,0.55), + 0 0 0 1px rgba(200,164,72,0.35), + 0 0 0 5px rgba(42,21,8,0.9), + 0 0 0 6px rgba(200,164,72,0.2); + animation: game-over-appear 0.25s cubic-bezier(0.22, 0.61, 0.36, 1); +} + +.nickname-modal-title { + font-family: var(--font-display); + font-size: 1.5rem; + font-weight: 600; + color: var(--ui-ink); + text-align: center; + letter-spacing: 0.04em; +} + +.nickname-modal-hint { + font-family: var(--font-ui); + font-size: 0.8rem; + color: rgba(42,26,8,0.6); + text-align: center; + margin-bottom: -0.25rem; +} + +.nickname-modal-alt { + text-align: center; + font-size: 0.8rem; + color: rgba(42,26,8,0.55); + padding-top: 0.5rem; + border-top: 1px solid rgba(138,106,40,0.2); +} + +.nickname-modal-alt a { + color: var(--ui-gold-dark); + text-decoration: none; + font-weight: 500; +} + +.nickname-modal-alt a:hover { text-decoration: underline; } + +/* ── Game hamburger button (☰ → ✕ animation) ────────────────────────── */ +.game-hamburger { + position: fixed; + top: 0.6rem; + left: 0.6rem; + z-index: 251; + width: 36px; + height: 36px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 5px; + background: var(--board-rail); + border: 1px solid rgba(200,164,72,0.35); + border-radius: 5px; + cursor: pointer; + transition: background 0.15s, border-color 0.15s; +} +.game-hamburger:hover { + background: #3d1f0a; + border-color: rgba(200,164,72,0.65); +} + +.hb-bar { + display: block; + width: 16px; + height: 2px; + background: var(--ui-parchment); + border-radius: 1px; + transition: transform 0.25s cubic-bezier(0.22, 0.61, 0.36, 1), opacity 0.2s; + transform-origin: center; +} +/* Top bar rotates down to form \ */ +.game-hamburger-open .hb-top { transform: translateY(7px) rotate(45deg); } +/* Middle bar fades out */ +.game-hamburger-open .hb-mid { opacity: 0; transform: scaleX(0); } +/* Bottom bar rotates up to form / */ +.game-hamburger-open .hb-bot { transform: translateY(-7px) rotate(-45deg); } + +/* ── Game sidebar ────────────────────────────────────────────────────── */ +.game-sidebar { + position: fixed; + top: 0; + left: 0; + height: 100vh; + width: 280px; + z-index: 250; + background: var(--board-rail); + border-right: 1px solid rgba(200,164,72,0.25); + display: flex; + flex-direction: column; + transform: translateX(-100%); + transition: transform 0.25s cubic-bezier(0.22, 0.61, 0.36, 1); + overflow-y: auto; +} +.game-sidebar-open { + transform: translateX(0); +} + +.game-sidebar-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 1rem 1rem; + border-bottom: 1px solid rgba(200,164,72,0.2); + flex-shrink: 0; +} + +.game-sidebar-brand { + font-family: var(--font-display); + font-size: 1.3rem; + font-weight: 600; + color: var(--ui-gold); + letter-spacing: 0.06em; + margin-left: 45px; +} + +.game-sidebar-close { + width: 28px; + height: 28px; + display: flex; + align-items: center; + justify-content: center; + background: transparent; + border: 1px solid rgba(200,164,72,0.25); + border-radius: 4px; + color: var(--ui-parchment); + font-size: 0.85rem; + cursor: pointer; + opacity: 0.65; + transition: opacity 0.15s; +} +.game-sidebar-close:hover { opacity: 1; } + +.game-sidebar-section { + padding: 0.9rem 1rem; + border-bottom: 1px solid rgba(200,164,72,0.12); + display: flex; + flex-direction: row; + gap: 0.55rem; +} + +.game-sidebar-label { + font-size: 0.7rem; + font-family: var(--font-ui); + letter-spacing: 0.07em; + text-transform: uppercase; + color: rgba(242,232,208,0.45); +} + +.game-sidebar-link { + font-family: var(--font-ui); + font-size: 0.85rem; + color: var(--ui-parchment); + text-decoration: none; + opacity: 0.8; + transition: opacity 0.15s; + cursor: pointer; +} +.game-sidebar-link:hover { opacity: 1; text-decoration: underline; text-underline-offset: 2px; } + +.game-sidebar-btn { + font-family: var(--font-ui); + font-size: 0.82rem; + padding: 0.4rem 0.75rem; + border: 1px solid rgba(200,164,72,0.35); + border-radius: 4px; + background: rgba(200,164,72,0.1); + color: var(--ui-parchment); + cursor: pointer; + text-align: left; + transition: background 0.15s; +} +.game-sidebar-btn:hover { background: rgba(200,164,72,0.22); } + +.game-sidebar-btn-newgame { + background: rgba(58,107,42,0.25); + border-color: rgba(58,107,42,0.55); + font-weight: 500; +} +.game-sidebar-btn-newgame:hover { background: rgba(58,107,42,0.42); } + +.game-sidebar-qr { + width: 100%; + height: auto; + aspect-ratio: 1; + max-width: 200px; + margin: 0 auto; +} + +/* Push the version wrapper to the bottom of the sidebar flex column */ +.sidebar-footer { + margin-top: auto; + border-top: 1px solid rgba(200,164,72,0.12); +} + +.site-nav-infolinks { + margin: 2em 0 1em; + text-align: center; + font-size: 0.9rem; + color: rgba(200,164,72,0.4); + display: flex; + flex-direction: row; + align-items: center; +} + +.site-nav-infolinks > a { + width: 100%; +} + +.site-nav-version { + margin: 2em 0 1em; + display: block; + text-align: center; + font-family: var(--font-ui); + font-size: 0.7rem; + letter-spacing: 0.06em; + color: rgba(200,164,72,0.4); +} + +/* ── Content pages (markdown-rendered) ─────────────────────────────────────── */ + +.content-page h1 { + font-family: var(--font-display); + font-size: 2rem; + font-weight: 600; + color: var(--ui-ink); + letter-spacing: 0.04em; + margin-bottom: 0.5rem; +} +.content-page h2 { + font-family: var(--font-display); + font-size: 1.4rem; + font-weight: 600; + color: var(--ui-ink); + margin: 1.75rem 0 0.5rem; + border-bottom: 1px solid rgba(200,164,72,0.25); + padding-bottom: 0.25rem; +} +.content-page h3 { + font-family: var(--font-display); + font-size: 1.1rem; + font-weight: 600; + color: var(--ui-ink); + margin: 1.25rem 0 0.4rem; +} +.content-page p { + line-height: 1.7; + margin-bottom: 0.9rem; + color: var(--ui-ink); +} +.content-page ul, +.content-page ol { + margin: 0.5rem 0 1rem 1.5rem; + line-height: 1.7; + color: var(--ui-ink); +} +.content-page li { + margin-bottom: 0.25rem; +} +.content-page a { + color: var(--ui-gold-dark); + text-decoration: underline; +} +.content-page a:hover { + color: var(--ui-ink); +} +.content-page code { + font-family: monospace; + background: rgba(0,0,0,0.07); + border-radius: 3px; + padding: 0.1em 0.35em; + font-size: 0.88em; +} +.content-page pre { + background: rgba(0,0,0,0.07); + border-radius: 5px; + padding: 1rem 1.25rem; + overflow-x: auto; + margin-bottom: 1rem; +} +.content-page pre code { + background: none; + padding: 0; +} +.content-page blockquote { + border-left: 3px solid rgba(200,164,72,0.5); + margin: 0.75rem 0; + padding: 0.25rem 1rem; + color: #665544; + font-style: italic; +} +.content-page table { + border-collapse: collapse; + width: 100%; + margin-bottom: 1rem; +} +.content-page th, +.content-page td { + border: 1px solid rgba(200,164,72,0.3); + padding: 0.4rem 0.75rem; + text-align: left; +} +.content-page th { + background: rgba(200,164,72,0.1); + font-weight: 600; +} + +/* ══════════════════════════════════════════════════════════════════════ + Layout variation 07 — scrolling strip + sidebar controls + ══════════════════════════════════════════════════════════════════════ */ + +/* Prevent horizontal scrollbar from the full-bleed strip */ +.game-overlay { overflow-x: hidden !important; } + +/* Board bar: hide die slots, keep the rail as a thin divider */ +.bar-die-slot { display: none !important; } +.board-bar { width: 5px; overflow: hidden; } + +/* ── Full-width in-flow player strip ─────────────────────────────────── */ +.v07-strip { + width: 100vw; + margin-top: -1.5rem; /* undo game-overlay top padding */ + margin-left: calc(50% - 50vw); /* align to viewport left */ + display: flex; + align-items: center; + background: var(--ui-parchment); + border-bottom: 2px solid var(--ui-gold-dark); + box-shadow: 0 2px 8px rgba(0,0,0,0.18); + padding: 0.35rem 1.5rem; + gap: 0.5rem; +} + +.v07-player { display: flex; align-items: center; flex: 1; min-width: 0; } +.v07-player-left { justify-content: flex-end; } +.v07-player-right { justify-content: flex-start; } + +.v07-active-zone { + display: flex; + align-items: center; + gap: 0.7rem; + border-radius: 8px; + padding: 0.28rem 0.5rem; + transition: background 0.15s; +} +.v07-active-zone.active { background: rgba(58,42,10,0.08); } + +/* Checker-style circles */ +.v07-avatar { + width: 38px; height: 38px; + border-radius: 50%; + flex-shrink: 0; +} +.v07-avatar-me { + background-image: + radial-gradient(ellipse 50% 35% at 36% 30%, rgba(255,255,255,0.65) 0%, transparent 100%), + radial-gradient(circle, transparent 68%, rgba(160,130,70,0.22) 68.5%, rgba(160,130,70,0.22) 71.5%, transparent 72%), + radial-gradient(circle, transparent 43%, rgba(160,130,70,0.17) 43.5%, rgba(160,130,70,0.17) 46.5%, transparent 47%), + radial-gradient(circle at 38% 32%, #ffffff 0%, var(--checker-white) 52%, #c0b288 100%); + border: 1.8px solid var(--checker-ring); + box-shadow: 0 2px 6px rgba(0,0,0,0.35), inset 0 -1px 3px rgba(0,0,0,0.15); +} +.v07-avatar-opp { + background-image: + radial-gradient(ellipse 40% 28% at 36% 30%, rgba(110,65,30,0.38) 0%, transparent 100%), + radial-gradient(circle, transparent 68%, rgba(200,164,72,0.18) 68.5%, rgba(200,164,72,0.18) 71.5%, transparent 72%), + radial-gradient(circle, transparent 43%, rgba(200,164,72,0.13) 43.5%, rgba(200,164,72,0.13) 46.5%, transparent 47%), + radial-gradient(circle at 38% 32%, #4a2e1a 0%, #1c1008 45%, var(--checker-black) 100%); + border: 1.8px solid var(--checker-ring); + box-shadow: 0 2px 6px rgba(0,0,0,0.5), inset 0 -1px 3px rgba(0,0,0,0.4); +} + +/* Strip peg overrides */ +.v07-strip .peg-track { gap: 3px; } +.v07-strip .peg-hole { width: 12px; height: 12px; } +.v07-strip .peg-hole.filled { + background: #5aab38; border-color: #3a7828; + box-shadow: 0 0 5px rgba(90,171,56,0.55); +} +.v07-strip .peg-hole.peg-opp.filled { + background: #c05030; border-color: #8a3018; + box-shadow: 0 0 5px rgba(192,80,48,0.55); +} + +/* Strip score-row-name: remove fixed width from v01 */ +.v07-strip .score-row-name { width: auto; } + +/* No ghost bar below pts-counter in the strip */ +.v07-strip .pts-counter-wrap { padding-bottom: 0; } + +/* Center "Trictrac" title */ +.v07-strip-center { + flex-shrink: 0; + padding: 0 1rem; + border-left: 1px solid rgba(138,106,40,0.2); + border-right: 1px solid rgba(138,106,40,0.2); +} +.v07-title { + font-family: var(--font-display); + font-size: 1.4rem; + font-weight: 600; + font-style: italic; + color: var(--ui-ink); + letter-spacing: 0.03em; + white-space: nowrap; +} + +/* ── Body: board + controls ──────────────────────────────────────────── */ +.v07-body { + display: flex; + align-items: flex-start; + gap: 0.5rem; + width: 100%; +} + +/* ── Controls column (sidebar on wide, row on narrow) ────────────────── */ +.v07-controls { + display: flex; + flex-direction: column; + gap: 0.5rem; + width: 152px; + flex-shrink: 0; + align-self: stretch; +} + +.v07-ctrl-dice { + background: var(--board-rail); + border-radius: 5px; + border-top: 2px solid var(--ui-gold-dark); + box-shadow: 0 2px 6px rgba(0,0,0,0.2); + padding: 0.6rem 0.75rem 0.75rem; + display: flex; + flex-direction: column; + align-items: center; + gap: 0.5rem; + flex-shrink: 0; +} +.v07-ctrl-dice-row { + display: flex; + gap: 0.55rem; + align-items: center; + justify-content: center; +} + +/* Free-mode toggle: light text on dark board-rail background */ +.v07-ctrl-dice .free-mode-toggle { + color: var(--ui-parchment); + font-size: 0.7rem; + flex-wrap: wrap; + justify-content: center; + text-align: center; + gap: 0.3rem; +} +.v07-ctrl-dice .free-mode-help { + border-color: rgba(242,232,208,0.35); + color: rgba(242,232,208,0.5); +} + +.v07-ctrl-status { + background: var(--ui-parchment); + border-radius: 5px; + border-top: 2px solid var(--ui-gold-dark); + box-shadow: 0 2px 6px rgba(0,0,0,0.2); + padding: 0.65rem 0.75rem 0.75rem; + display: flex; + flex-direction: column; + align-items: center; + gap: 0.4rem; + flex: 1; + min-width: 0; +} +.v07-ctrl-status .game-status { + color: var(--ui-ink); + text-shadow: none; + font-size: 1rem; + padding: 0; + width: auto; + text-align: center; + line-height: 1.3; +} +.v07-ctrl-status .board-actions { + flex-wrap: wrap; + justify-content: center; + min-height: 0; +} +.v07-ctrl-status .game-sub-prompt { + color: #887766; + padding: 0; + width: auto; + text-align: center; + font-size: 0.67rem; + line-height: 1.4; + margin: 0; +} + +/* ── Scoring panels row (below board+controls, in-flow) ──────────────── */ +.v07-scoring-row { width: 100%; } + +/* Reset absolute positioning from the old score-area context */ +.v07-scoring-row .scoring-panels-container { + position: static; + top: auto; left: auto; right: auto; bottom: auto; + z-index: auto; + padding: 0; + pointer-events: auto; + display: flex; + flex-direction: column; + align-items: stretch; + gap: 4px; +} +.v07-scoring-row .scoring-panel { + width: 100%; + box-sizing: border-box; + margin: 0; +} + +/* ── Responsive: ≤919px → controls becomes a bottom bar ─────────────── */ +@media (max-width: 919px) { + .v07-body { + flex-direction: column; + align-items: stretch; + } + .v07-controls { + flex-direction: row; + width: 100%; + } + .v07-ctrl-status { flex: 1; } + /* Hide pegs on small screens to save space in the strip */ + .v07-strip .peg-track { display: none; } +} diff --git a/doc/design/variations/07-scrolling-header.html b/doc/design/variations/07-scrolling-header.html new file mode 100644 index 0000000..122e772 --- /dev/null +++ b/doc/design/variations/07-scrolling-header.html @@ -0,0 +1,711 @@ + + + + + + Variation 07 — Scrolling header · Responsive sidebar/footer + + + + + + + + +
+
+ Trictrac +
+ + +
+
+ +
+ + Se connecter +
+ +
+ + +
+
+ +
+
+ + +
+
+ + +
+ + +
+
+ +
A
+ +
+ Anonyme + (vous) +
+ +
+
+
+
+
+
+
+
+
+
+
+ +
+
+ 6 + /12 +
+
+
+
+ + +
+ Trictrac +
+ + +
+
+ +
+
+ 2 + /12 +
+
+ +
+
+
+
+
+
+
+
+
+ +
+ Bot +
+ +
B
+
+
+ +
+ + +
+ + +
+
+ + +
+
+
13
+
14
+
+ 15 +
+
+
+
+
16
+
17
+
18
+
+
+
+
+ 19 +
+
+
+
20
+
21
+
22
+
23
+
+ 24 +
+
+
11
+
+
+
+
+ +
+ + +
+
+
+ 12 +
+
11
+
10
+
+ 9 +
+
+
+ 8 +
+
+
+ 7 +
+
+
+
+
+
6
+
5
+
4
+
3
+
2
+
+ 1 +
+
10
+
+
+
+
+
+ + + +
+
+ + +
+ +
+
+ + + + + + + + + + + + + + + + +
+ +
+ +
+
Déplacez une dame (1 sur 2)
+
+ +
+
+ +
+ +
+ + +
+ +
+
+ + + + + + + + + + + + + + + + +
+ +
+ +
+
Déplacez une dame (1 sur 2)
+
+ +
+
+ +
+ + +
+
+
+ +
+
+
+4 pts
+ +
+
+ Battage à vrai (petit jan) + simple + ×1 + +4 +
+
+
+
+
+ +
+
+ + +