From c0409d6121e02692091e4e3dfe85f79e4271f25a Mon Sep 17 00:00:00 2001 From: Henri Bourcereau Date: Fri, 10 Apr 2026 19:37:31 +0200 Subject: [PATCH 1/3] feat(client_web): hit animations --- client_web/assets/style.css | 23 +++++++++++++++ client_web/src/components/board.rs | 34 ++++++++++++++++++++-- client_web/src/components/game_screen.rs | 36 +++++++++++++++++++++++- 3 files changed, 89 insertions(+), 4 deletions(-) diff --git a/client_web/assets/style.css b/client_web/assets/style.css index 39d7fc8..5edde6e 100644 --- a/client_web/assets/style.css +++ b/client_web/assets/style.css @@ -827,6 +827,29 @@ body { animation: exit-glow 2s ease-in-out infinite; } +/* ── §6c — Jan hover field highlight ────────────────────────────────── */ +.field.jan-hovered { + --fc: rgba(190, 140, 35, 0.8) !important; +} + +/* ── §6e — Hit (battue) ripple ──────────────────────────────────────── */ +@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; } + /* ── Interactive states — after .corner to take visual priority ─────── */ .field.clickable { cursor: pointer; diff --git a/client_web/src/components/board.rs b/client_web/src/components/board.rs index e9d1308..5a82b13 100644 --- a/client_web/src/components/board.rs +++ b/client_web/src/components/board.rs @@ -263,6 +263,9 @@ pub fn Board( /// Checker moves to animate on mount (None when board unchanged). #[prop(default = None)] last_moves: Option<(CheckerMove, CheckerMove)>, + /// Fields where a hit (battue) was scored this turn — show ripple animation. + #[prop(default = vec![])] + hit_fields: Vec, ) -> impl IntoView { let board = view_state.board; let is_move_stage = view_state.active_mp_player == Some(player_id) @@ -318,6 +321,8 @@ pub fn Board( (dx.abs() >= 1.0 || dy.abs() >= 1.0).then_some((dx, dy)) }) }); + // §6e — ripple on hit fields (battue). + let is_hit_field = hit_fields.contains(&field_num); view! {
0).then(|| { + // §6e — ripple on hit (battue) fields; must be inside the + // reactive closure so Leptos uses the same direct rendering + // path as .arriving (avoids node-move that resets animation). + let ripple = is_hit_field.then(|| { + let cls = if is_top_row { "hit-ripple hit-ripple-top" } else { "hit-ripple hit-ripple-bot" }; + view! {
}.into_any() + }); + let stack = (count > 0).then(|| { let color = if val > 0 { "white" } else { "black" }; let display_n = (count as usize).min(4); // outermost index: last for top rows, first for bottom rows. @@ -448,8 +475,9 @@ pub fn Board(
{label}
}.into_any() }).collect(); - view! {
{chips}
} - }) + view! {
{chips}
}.into_any() + }); + (ripple, stack) }}
} diff --git a/client_web/src/components/game_screen.rs b/client_web/src/components/game_screen.rs index f5c08b8..2fa14c3 100644 --- a/client_web/src/components/game_screen.rs +++ b/client_web/src/components/game_screen.rs @@ -2,7 +2,7 @@ use std::collections::VecDeque; use futures::channel::mpsc::UnboundedSender; use leptos::prelude::*; -use trictrac_store::{Board as StoreBoard, CheckerMove, Color, Dice as StoreDice, MoveRules}; +use trictrac_store::{Board as StoreBoard, CheckerMove, Color, Dice as StoreDice, Jan, MoveRules}; use crate::app::{GameUiState, NetCommand, PauseReason}; use crate::i18n::*; @@ -121,6 +121,39 @@ pub fn GameScreen(state: GameUiState) -> impl IntoView { let last_moves = state.last_moves; + // §6e — fields where a battue (hit) was scored; ripple animation shown there. + let hit_fields: Vec = { + let is_hit_jan = |jan: &Jan| matches!( + jan, + Jan::TrueHitSmallJan + | Jan::TrueHitBigJan + | Jan::TrueHitOpponentCorner + | Jan::FalseHitSmallJan + | Jan::FalseHitBigJan + ); + let mut fields: Vec = vec![]; + for event_opt in [&my_scored_event, &opp_scored_event] { + if let Some(event) = event_opt { + for entry in &event.jans { + if is_hit_jan(&entry.jan) { + for (m1, m2) in &entry.moves { + for m in [m1, m2] { + let to = m.get_to() as u8; + if to != 0 && !fields.contains(&to) { + fields.push(to); + } + } + } + } + } + } + } + if !fields.is_empty() { + leptos::logging::log!("[6e] hit_fields = {:?}", fields); + } + fields + }; + // ── Capture for closures ─────────────────────────────────────────────────── let stage = vs.stage.clone(); let turn_stage = vs.turn_stage.clone(); @@ -217,6 +250,7 @@ pub fn GameScreen(state: GameUiState) -> impl IntoView { bar_is_move=is_move_stage bar_is_double=is_double_dice last_moves=last_moves + hit_fields=hit_fields /> // ── Side panel (scoring panels only) ───────────────────────── From 874a302524053223f312222afc07d94d30a7d879 Mon Sep 17 00:00:00 2001 From: Henri Bourcereau Date: Fri, 10 Apr 2026 20:25:49 +0200 Subject: [PATCH 2/3] fix(client_web): "jan de retour" location --- client_web/assets/style.css | 12 +-- client_web/src/components/board.rs | 10 +-- doc/client_web_design_proposals.md | 2 +- doc/refs/vocabulary.md | 127 ++++++++++++++--------------- 4 files changed, 74 insertions(+), 77 deletions(-) diff --git a/client_web/assets/style.css b/client_web/assets/style.css index 5edde6e..ddc024e 100644 --- a/client_web/assets/style.css +++ b/client_web/assets/style.css @@ -785,13 +785,13 @@ body { .board-quarter .field.zone-petit:nth-child(even), .board-quarter .field.zone-grand:nth-child(even) { --fc: var(--field-ivory); } -/* Jan de retour — cooler: dark teal / silvery-green ivory */ -.board-quarter .field.zone-retour:nth-child(odd) { --fc: #1e3d32; } -.board-quarter .field.zone-retour:nth-child(even) { --fc: #e5eadc; } +/* Opponent's grand-jan — cooler: dark teal / silvery-green ivory */ +.board-quarter .field.zone-opponent:nth-child(odd) { --fc: #1e3d32; } +.board-quarter .field.zone-opponent:nth-child(even) { --fc: #e5eadc; } -/* Dernier jan — warmer: amber-brown / warm amber ivory */ -.board-quarter .field.zone-dernier:nth-child(odd) { --fc: #6a2810; } -.board-quarter .field.zone-dernier:nth-child(even) { --fc: #f2dfa0; } +/* Jan de retour — warmer: amber-brown / warm amber ivory */ +.board-quarter .field.zone-retour:nth-child(odd) { --fc: #6a2810; } +.board-quarter .field.zone-retour:nth-child(even) { --fc: #f2dfa0; } /* ── Rest corner (§3) — before .clickable so green wins when interactive ── */ .field.corner { --fc: var(--field-corner) !important; } diff --git a/client_web/src/components/board.rs b/client_web/src/components/board.rs index 5a82b13..9ab94ae 100644 --- a/client_web/src/components/board.rs +++ b/client_web/src/components/board.rs @@ -32,8 +32,8 @@ fn field_zone_class(field_num: u8) -> &'static str { match field_num { 1..=6 => "zone-petit", 7..=12 => "zone-grand", - 13..=18 => "zone-retour", - 19..=24 => "zone-dernier", + 13..=18 => "zone-opponent", + 19..=24 => "zone-retour", _ => "", } } @@ -143,7 +143,6 @@ fn field_center(f: usize, is_white: bool) -> Option<(f32, f32)> { Some((x, y)) } - /// SVG `` element drawing one arrow (shadow + gold) from `fp` to `tp`. fn arrow_svg(fp: (f32, f32), tp: (f32, f32)) -> AnyView { let (x1, y1) = fp; @@ -515,7 +514,6 @@ pub fn Board( } }; - let (tl, tr, bl, br) = if is_white { (&TOP_LEFT_W, &TOP_RIGHT_W, &BOT_LEFT_W, &BOT_RIGHT_W) } else { @@ -524,9 +522,9 @@ pub fn Board( // 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", "dernier jan", "grand jan", "petit jan") + ("", "jan de retour", "grand jan", "petit jan") } else { - ("petit jan", "grand jan", "dernier jan", "jan de retour") + ("petit jan", "grand jan", "jan de retour", "") }; view! { diff --git a/doc/client_web_design_proposals.md b/doc/client_web_design_proposals.md index c0237ca..598e9c5 100644 --- a/doc/client_web_design_proposals.md +++ b/doc/client_web_design_proposals.md @@ -37,7 +37,7 @@ The board body between triangles becomes visible as the wood/felt surface — th **Proposals**: ### 2a. Zone labels -Add thin labels (`"petit jan"`, `"grand jan"`, `"jan de retour"`, `"dernier jan"`) beneath the board-row (or as a subtle strip above/below the quarter). These should use the serif font at very small size and low opacity — decorative, not noisy. +Add thin labels (`"petit jan"`, `"grand jan"`, `"jan de retour"`) beneath the board-row (or as a subtle strip above/below the quarter). These should use the serif font at very small size and low opacity — decorative, not noisy. ### 2b. Field color shift per zone The physical game uses alternating colors within each quarter, but different quarters can use slightly different base hues: diff --git a/doc/refs/vocabulary.md b/doc/refs/vocabulary.md index 4ab3bb3..b4929b4 100644 --- a/doc/refs/vocabulary.md +++ b/doc/refs/vocabulary.md @@ -2,67 +2,66 @@ This table maps the French game terminology to the English terms used in this codebase (primarily the `store` crate). Where a code identifier exists, it is shown in `monospace`. -| French | English (code) | Notes | -|---|---|---| -| tablier | board | `Board` | -| case / flèche | field | `Field` (1–24, 0 = exit); "flèche" (arrow) and "case" both refer to a field/point | -| demi-case | half-field | A field occupied by exactly one checker | -| dame | checker | `Checker`; a playing piece | -| talon | stack | The starting pile of 15 checkers before they are deployed | -| coin de repos / coin | rest corner / corner | `corner`; field 12 (White) or 13 (Black) | -| bande de départ | starting rail | The side rail where stacks start; holds the pegs and flag | -| bande de sortie | exit rail | Same rail, used as an extra field value during exit | -| petit jan | small jan | Fields 1–6; `is_field_in_small_jan` | -| grand jan | big jan | Fields 7–12 (White's side, opponent's near zone) | -| jan de retour | return jan | Fields 13–18; same fields as opponent's small jan | -| dernier jan / jan de retour | last jan / last quarter | Fields 19–24; where checkers gather before exiting; `last quarter` | -| table des petits jans | small jan table | The board half containing both players' small jans (fields 1–12) | -| table des grands jans | big jan table | The board half containing both players' big jans (fields 13–24) | -| plein (d'un jan) | filled (jan) | All 6 fields of a jan hold ≥ 2 checkers | -| remplir | fill | Scoring event: completing the fill of a jan; `FilledQuarter` | -| conserver | conserve | Scoring event: maintaining a filled jan without breaking it; `FilledQuarter` | -| jan de récompense — battre à vrai | true hit | `TrueHitSmallJan`, `TrueHitBigJan`, `TrueHitOpponentCorner` | -| jan de récompense — battre à faux | false hit | `FalseHitSmallJan`, `FalseHitBigJan` | -| batterie du coin | corner hit | `TrueHitOpponentCorner`; hitting the opponent's empty rest corner | -| jan-qui-ne-peut / impuissance | helpless man | `HelplessMan`; a die value that cannot be played (penalty for opponent) | -| jan de deux tables | two tables jan | `TwoTables` | -| contre-jan de deux tables | contre two tables | `ContreTwoTables` | -| jan de mézéas | mezeas jan | `Mezeas` | -| contre-jan de mézéas | contre mezeas | `ContreMezeas` | -| jan de six tables / jan de trois coups | six tables jan | `SixTables`; also called "three-roll jan" | -| sortie (première) | first player to exit | `FirstPlayerToExit` | -| sortie (nombre sortant) | exit (exact exit) | Moving a checker off the board with an exact die value | -| nombre excédant | overflow number | Die value exceeding the checker's distance to the exit rail | -| nombre défaillant | failing number | A die value that cannot be played within the jan | -| tout d'une | chained move | `chained move`; one checker playing both dice successively | -| repos (case de repos) | rest (resting field) | An intermediate field where a checker pauses in a chained move | -| doublet | double | `is_double`; both dice show the same value | -| dé / dés | die / dice | `Dice` | -| cornet | dice cup | — | -| par puissance | by puissance | `is_move_by_puissance`; taking own corner using opponent's empty corner as virtual step | -| par effet | by effect | `can_take_corner_by_effect`; taking own corner by normal die values | -| d'emblée | simultaneously | Two checkers entering (or leaving) the corner at the same time | -| dédoubler | unstack corner | Using one of the two corner-holding checkers (forbidden for corner exits) | -| trou / jeu | hole | `holes`; 12 points = 1 hole; the primary scoring unit | -| fichet | peg | Physical marker tracking holes won along the board edge | -| jeton | token | Physical marker tracking points within a game (0–12) | -| pavillon | flag | The bredouille marker taken by the second player to score | -| bredouille | bredouille | `can_bredouille`; winning a hole while opponent scored nothing | -| petite bredouille | small bredouille | Winning a round (marqué) with ≥ 6 consecutive holes | -| grande bredouille | big bredouille | `can_big_bredouille`; winning a round with ≥ 12 consecutive holes | -| relevé | new setting | Resetting checkers to their stacks after a hole or exit | -| primauté | first-move privilege | The right to roll first, held by the player who exited or left first | -| s'en aller | leave / go | `Go` event; choosing to start a new setting after winning a hole | -| tenir | stay / hold | Choosing to continue after winning a hole instead of leaving | -| marqué | round | A scoring round in the "partie à écrire" | -| partie ordinaire | ordinary game | First to 12 holes wins | -| partie à écrire | scored game | Multi-round game played for tokens | -| à la chouette | chouette | Three- or four-player format | -| refait | replay | A drawn round (equal holes) that must be replayed | -| consolation | consolation | Bonus tokens paid to the winner and, in 3-player games, the non-playing player | -| postillon | postillon | The first "double bet" in final payment settlement | -| école | school | `schools`; a penalty for a marking error; opponent scores the missed points | -| fausse case | false move | Playing a checker to the wrong field | -| fausse école | false school | Incorrectly claiming or marking a school penalty | -| augmentation d'école | school escalation | Back-and-forth dispute over a school penalty | -| pile de misère | misery pile | A special scoring configuration (not yet implemented in the codebase) | +| French | English (code) | Notes | +| -------------------------------------- | -------------------- | -------------------------------------------------------------------------------------------------------- | +| tablier | board | `Board` | +| case / flèche | field | `Field` (1–24, 0 = exit); "flèche" (arrow) and "case" both refer to a field/point | +| demi-case | half-field | A field occupied by exactly one checker | +| dame | checker | `Checker`; a playing piece | +| talon | stack | The starting pile of 15 checkers before they are deployed | +| coin de repos / coin | rest corner / corner | `corner`; field 12 (White) or 13 (Black) | +| bande de départ | starting rail | The side rail where stacks start; holds the pegs and flag | +| bande de sortie | exit rail | Same rail, used as an extra field value during exit | +| petit jan | small jan | Fields 1–6; `is_field_in_small_jan` | +| grand jan | big jan | Fields 7–12 (White's side, opponent's near zone) | +| jan de retour | return jan | Fields 19–24; same fields as opponent's small jan ; where checkers gather before exiting; `last quarter` | +| table des petits jans | small jan table | The board half containing both players' small jans (fields 1–12) | +| table des grands jans | big jan table | The board half containing both players' big jans (fields 13–24) | +| plein (d'un jan) | filled (jan) | All 6 fields of a jan hold ≥ 2 checkers | +| remplir | fill | Scoring event: completing the fill of a jan; `FilledQuarter` | +| conserver | conserve | Scoring event: maintaining a filled jan without breaking it; `FilledQuarter` | +| jan de récompense — battre à vrai | true hit | `TrueHitSmallJan`, `TrueHitBigJan`, `TrueHitOpponentCorner` | +| jan de récompense — battre à faux | false hit | `FalseHitSmallJan`, `FalseHitBigJan` | +| batterie du coin | corner hit | `TrueHitOpponentCorner`; hitting the opponent's empty rest corner | +| jan-qui-ne-peut / impuissance | helpless man | `HelplessMan`; a die value that cannot be played (penalty for opponent) | +| jan de deux tables | two tables jan | `TwoTables` | +| contre-jan de deux tables | contre two tables | `ContreTwoTables` | +| jan de mézéas | mezeas jan | `Mezeas` | +| contre-jan de mézéas | contre mezeas | `ContreMezeas` | +| jan de six tables / jan de trois coups | six tables jan | `SixTables`; also called "three-roll jan" | +| sortie (première) | first player to exit | `FirstPlayerToExit` | +| sortie (nombre sortant) | exit (exact exit) | Moving a checker off the board with an exact die value | +| nombre excédant | overflow number | Die value exceeding the checker's distance to the exit rail | +| nombre défaillant | failing number | A die value that cannot be played within the jan | +| tout d'une | chained move | `chained move`; one checker playing both dice successively | +| repos (case de repos) | rest (resting field) | An intermediate field where a checker pauses in a chained move | +| doublet | double | `is_double`; both dice show the same value | +| dé / dés | die / dice | `Dice` | +| cornet | dice cup | — | +| par puissance | by puissance | `is_move_by_puissance`; taking own corner using opponent's empty corner as virtual step | +| par effet | by effect | `can_take_corner_by_effect`; taking own corner by normal die values | +| d'emblée | simultaneously | Two checkers entering (or leaving) the corner at the same time | +| dédoubler | unstack corner | Using one of the two corner-holding checkers (forbidden for corner exits) | +| trou / jeu | hole | `holes`; 12 points = 1 hole; the primary scoring unit | +| fichet | peg | Physical marker tracking holes won along the board edge | +| jeton | token | Physical marker tracking points within a game (0–12) | +| pavillon | flag | The bredouille marker taken by the second player to score | +| bredouille | bredouille | `can_bredouille`; winning a hole while opponent scored nothing | +| petite bredouille | small bredouille | Winning a round (marqué) with ≥ 6 consecutive holes | +| grande bredouille | big bredouille | `can_big_bredouille`; winning a round with ≥ 12 consecutive holes | +| relevé | new setting | Resetting checkers to their stacks after a hole or exit | +| primauté | first-move privilege | The right to roll first, held by the player who exited or left first | +| s'en aller | leave / go | `Go` event; choosing to start a new setting after winning a hole | +| tenir | stay / hold | Choosing to continue after winning a hole instead of leaving | +| marqué | round | A scoring round in the "partie à écrire" | +| partie ordinaire | ordinary game | First to 12 holes wins | +| partie à écrire | scored game | Multi-round game played for tokens | +| à la chouette | chouette | Three- or four-player format | +| refait | replay | A drawn round (equal holes) that must be replayed | +| consolation | consolation | Bonus tokens paid to the winner and, in 3-player games, the non-playing player | +| postillon | postillon | The first "double bet" in final payment settlement | +| école | school | `schools`; a penalty for a marking error; opponent scores the missed points | +| fausse case | false move | Playing a checker to the wrong field | +| fausse école | false school | Incorrectly claiming or marking a school penalty | +| augmentation d'école | school escalation | Back-and-forth dispute over a school penalty | +| pile de misère | misery pile | A special scoring configuration (not yet implemented in the codebase) | From 4550b1d66a1ea4e02b2d594ebc317f88007d02e0 Mon Sep 17 00:00:00 2001 From: Henri Bourcereau Date: Fri, 10 Apr 2026 21:07:59 +0200 Subject: [PATCH 3/3] =?UTF-8?q?fix(client=5Fweb):=E2=80=AFcenter=20board?= =?UTF-8?q?=20and=20points=20panels?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client_web/assets/style.css | 53 +++++++++++++++++++++++++------------ 1 file changed, 36 insertions(+), 17 deletions(-) diff --git a/client_web/assets/style.css b/client_web/assets/style.css index ddc024e..655bb1f 100644 --- a/client_web/assets/style.css +++ b/client_web/assets/style.css @@ -249,12 +249,15 @@ body { } /* ── Game container ─────────────────────────────────────────────────── */ +/* No width: 100% — let it size to content (the board wrapper, ~832px). + This keeps the board pinned at the same horizontal position whether or + not the side panel is visible, and aligns the status bar / score panels + with the board rather than with the viewport edge. */ .game-container { display: flex; flex-direction: column; align-items: center; gap: 0.6rem; - width: 100%; } /* ── Language switcher (in-game) ────────────────────────────────────── */ @@ -325,17 +328,25 @@ body { } /* ── Player score panel ─────────────────────────────────────────────── */ +/* Horizontal banner: name on the left, score bars expanding to fill the + board width — no more empty right half on large screens. */ .player-score-panel { background: var(--ui-parchment); border-radius: 5px; - padding: 0.45rem 1rem; + 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 { margin-bottom: 0.35rem; } +.player-score-header { + flex-shrink: 0; + min-width: 90px; +} .player-name { font-family: var(--font-display); @@ -345,25 +356,28 @@ body { letter-spacing: 0.02em; } -.score-bars { display: flex; flex-direction: column; gap: 5px; } +/* Bars sit side-by-side (points | holes) filling remaining width */ +.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: 3.5rem; + width: 3rem; text-align: right; flex-shrink: 0; } /* ── Points bar ─────────────────────────────────────────────────────── */ .score-bar { - width: 140px; + flex: 1; + max-width: 220px; height: 8px; background: rgba(0,0,0,0.1); border-radius: 4px; @@ -424,18 +438,21 @@ body { } /* ── Board + side panel ─────────────────────────────────────────────── */ +/* .board-and-panel is sized to the board wrapper only; the side panel is + positioned absolutely so it floats to the right without pushing the + board and breaking its horizontal alignment. */ .board-and-panel { - display: flex; - flex-direction: row; - align-items: flex-start; - gap: 1rem; + position: relative; } .side-panel { + position: absolute; + left: calc(100% + 1rem); + top: 0; display: flex; flex-direction: column; gap: 0.65rem; - min-width: 160px; + width: 200px; padding-top: 0.15rem; } @@ -785,8 +802,10 @@ body { .board-quarter .field.zone-petit:nth-child(even), .board-quarter .field.zone-grand:nth-child(even) { --fc: var(--field-ivory); } -/* Opponent's grand-jan — cooler: dark teal / silvery-green ivory */ -.board-quarter .field.zone-opponent:nth-child(odd) { --fc: #1e3d32; } +/* Opponent's grand-jan — deep slate-blue / silvery-green ivory. + Previously #1e3d32 was nearly identical to the felt (#1d3d28); now using + a clearly distinguishable cool blue that reads well against the green. */ +.board-quarter .field.zone-opponent:nth-child(odd) { --fc: #1a4f72; } .board-quarter .field.zone-opponent:nth-child(even) { --fc: #e5eadc; } /* Jan de retour — warmer: amber-brown / warm amber ivory */ @@ -872,10 +891,10 @@ body { 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-dernier:nth-child(odd) .field-num { +.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); }