« Reprendre l'ancienne » relit l'ancienne partie dans le stockage par son identifiant et supprime la nouvelle (GameSession). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1319 lines
28 KiB
CSS
1319 lines
28 KiB
CSS
/* Style de l'application, selon la charte graphique (ADR 0010,
|
|
* docs/identite/charte/) : les couleurs, tailles et rayons sont les
|
|
* variables de tokens.json, sous le même nom, redéfinies pour le thème
|
|
* sombre ; les composants reprennent bundle.css. */
|
|
|
|
/* ------------------------------------------------------------ Police */
|
|
|
|
/* Alegreya Sans (SIL Open Font License 1.1, voir fonts/OFL.txt), réduite
|
|
* aux caractères latins et convertie en woff2 (voir docs/identite/README.md).
|
|
* Embarquée dans le site, donc dans le cache du service worker : elle
|
|
* fonctionne hors ligne. */
|
|
@font-face {
|
|
font-family: "Alegreya Sans";
|
|
src: url("fonts/AlegreyaSans-Regular.woff2") format("woff2");
|
|
font-weight: 400;
|
|
font-style: normal;
|
|
font-display: swap;
|
|
}
|
|
|
|
@font-face {
|
|
font-family: "Alegreya Sans";
|
|
src: url("fonts/AlegreyaSans-Italic.woff2") format("woff2");
|
|
font-weight: 400;
|
|
font-style: italic;
|
|
font-display: swap;
|
|
}
|
|
|
|
@font-face {
|
|
font-family: "Alegreya Sans";
|
|
src: url("fonts/AlegreyaSans-Medium.woff2") format("woff2");
|
|
font-weight: 500;
|
|
font-style: normal;
|
|
font-display: swap;
|
|
}
|
|
|
|
@font-face {
|
|
font-family: "Alegreya Sans";
|
|
src: url("fonts/AlegreyaSans-Bold.woff2") format("woff2");
|
|
font-weight: 700;
|
|
font-style: normal;
|
|
font-display: swap;
|
|
}
|
|
|
|
@font-face {
|
|
font-family: "Alegreya Sans";
|
|
src: url("fonts/AlegreyaSans-ExtraBold.woff2") format("woff2");
|
|
font-weight: 800;
|
|
font-style: normal;
|
|
font-display: swap;
|
|
}
|
|
|
|
/* ------------------------------------------------------------ Couleurs */
|
|
|
|
/* Thème clair par défaut ; thème sombre selon le système, ou forcé par le
|
|
* menu (data-theme sur <html>). Les deux blocs sombres sont identiques
|
|
* (vérifié par app/tests/ui.test.ts). */
|
|
:root {
|
|
--background: #f5efe5;
|
|
--surface: #fffcf6;
|
|
--surface-sunken: #ece3d4;
|
|
--text: #2b1d15;
|
|
--muted: #6b5c50;
|
|
--border: #ded2c0;
|
|
--border-strong: #8e7d6a;
|
|
--accent: #a8432b;
|
|
--on-accent: #ffffff;
|
|
--accent-soft: #f1d9cf;
|
|
--focus: #a8432b;
|
|
--error: #8c1c3a;
|
|
--verdict-good: #2f6b4f;
|
|
--verdict-doubtful: #7e5a0f;
|
|
--verdict-bad: #8c1c3a;
|
|
--brand-noyer: #4a2e1f;
|
|
--brand-ivoire: #f3ead8;
|
|
--brand-laiton: #8f6a1c;
|
|
|
|
--frame: #4a2e1f;
|
|
--bed: #e9dcc4;
|
|
--bar: #3f2619;
|
|
--tray: #3a2418;
|
|
--point-a: #6b4430;
|
|
--point-b: #c9ae88;
|
|
--point-number: #f3ead8;
|
|
--checker-player: #f7f0e2;
|
|
--checker-player-edge: #8c7a62;
|
|
--checker-computer: #2e1c12;
|
|
--checker-computer-edge: #120a05;
|
|
--count-player: #2b1d15;
|
|
--count-computer: #f3ead8;
|
|
--die-player: #f7f0e2;
|
|
--pip-player: #2b1d15;
|
|
--die-computer: #2e1c12;
|
|
--pip-computer: #f3ead8;
|
|
--cube: #f7f0e2;
|
|
--cube-text: #2b1d15;
|
|
--highlight: #a8432b;
|
|
--target: rgba(168, 67, 43, 0.32);
|
|
/* Dernier coup joué (marques et traces) : le laiton, rien d'autre. */
|
|
--last-move: var(--brand-laiton);
|
|
|
|
/* Numéros des flèches. La charte les pose sur le cadre (point-number,
|
|
* ivoire) ; notre plateau les pose sur le fond, où l'ivoire ne se lirait
|
|
* pas : texte secondaire en thème clair (4,7:1 sur bed), point-number en
|
|
* thème sombre (6,3:1). */
|
|
--point-label: var(--muted);
|
|
|
|
--shadow-raised: 0 1px 2px rgba(43, 29, 21, 0.1);
|
|
|
|
--font-sans: "Alegreya Sans", "Gill Sans", "Segoe UI", system-ui, sans-serif;
|
|
|
|
--space-1: 4px;
|
|
--space-2: 8px;
|
|
--space-3: 12px;
|
|
--space-4: 16px;
|
|
--space-6: 24px;
|
|
--space-8: 32px;
|
|
--button-height: 48px;
|
|
--page-width: 40rem;
|
|
--radius-sm: 6px;
|
|
--radius-md: 12px;
|
|
--radius-pill: 999px;
|
|
|
|
/* Mouvement (interaction.md). */
|
|
--duration-instant: 90ms;
|
|
--duration-fast: 160ms;
|
|
--duration-moderate: 240ms;
|
|
--easing-standard: cubic-bezier(0.2, 0, 0, 1);
|
|
--easing-exit: cubic-bezier(0.4, 0, 1, 1);
|
|
|
|
/* Voile sous le menu et les panneaux. */
|
|
--scrim: rgba(43, 29, 21, 0.5);
|
|
|
|
/* Hauteurs fixes de l'écran de jeu en portrait : rien ne bouge sous le
|
|
* doigt (interaction.md, principe 2). */
|
|
--topbar-height: 60px;
|
|
--strip-height: 44px;
|
|
--infoline-height: 44px;
|
|
--prompt-height: 50px;
|
|
--actions-height: 76px;
|
|
|
|
color-scheme: light;
|
|
}
|
|
|
|
@media (prefers-color-scheme: dark) {
|
|
:root:not([data-theme="light"]) {
|
|
--background: #17110d;
|
|
--surface: #221913;
|
|
--surface-sunken: #2c211a;
|
|
--text: #efe5d5;
|
|
--muted: #a99886;
|
|
--border: #3a2d24;
|
|
--border-strong: #7d6b5b;
|
|
--accent: #d0694b;
|
|
--on-accent: #1a110c;
|
|
--accent-soft: #4a2519;
|
|
--focus: #e58a6e;
|
|
--error: #f08fa6;
|
|
--verdict-good: #7ccba4;
|
|
--verdict-doubtful: #e0b04f;
|
|
--verdict-bad: #f08fa6;
|
|
--brand-laiton: #d6a64a;
|
|
|
|
--frame: #2a1a10;
|
|
--bed: #4a3626;
|
|
--bar: #24170e;
|
|
--tray: #22150d;
|
|
--point-a: #3a2519;
|
|
--point-b: #7e6142;
|
|
--point-number: #cdbfa8;
|
|
--checker-player: #ede3cf;
|
|
--checker-player-edge: #8e7c64;
|
|
--checker-computer: #140d09;
|
|
--checker-computer-edge: #a8957d;
|
|
--count-player: #2b1d15;
|
|
--count-computer: #f3ead8;
|
|
--die-player: #ede3cf;
|
|
--pip-player: #2b1d15;
|
|
--die-computer: #140d09;
|
|
--pip-computer: #ede3cf;
|
|
--cube: #ede3cf;
|
|
--cube-text: #2b1d15;
|
|
--highlight: #e07a5a;
|
|
--target: rgba(224, 122, 90, 0.38);
|
|
|
|
--point-label: var(--point-number);
|
|
|
|
--shadow-raised: 0 1px 2px rgba(0, 0, 0, 0.4);
|
|
--scrim: rgba(0, 0, 0, 0.6);
|
|
color-scheme: dark;
|
|
}
|
|
}
|
|
|
|
:root[data-theme="dark"] {
|
|
--background: #17110d;
|
|
--surface: #221913;
|
|
--surface-sunken: #2c211a;
|
|
--text: #efe5d5;
|
|
--muted: #a99886;
|
|
--border: #3a2d24;
|
|
--border-strong: #7d6b5b;
|
|
--accent: #d0694b;
|
|
--on-accent: #1a110c;
|
|
--accent-soft: #4a2519;
|
|
--focus: #e58a6e;
|
|
--error: #f08fa6;
|
|
--verdict-good: #7ccba4;
|
|
--verdict-doubtful: #e0b04f;
|
|
--verdict-bad: #f08fa6;
|
|
--brand-laiton: #d6a64a;
|
|
|
|
--frame: #2a1a10;
|
|
--bed: #4a3626;
|
|
--bar: #24170e;
|
|
--tray: #22150d;
|
|
--point-a: #3a2519;
|
|
--point-b: #7e6142;
|
|
--point-number: #cdbfa8;
|
|
--checker-player: #ede3cf;
|
|
--checker-player-edge: #8e7c64;
|
|
--checker-computer: #140d09;
|
|
--checker-computer-edge: #a8957d;
|
|
--count-player: #2b1d15;
|
|
--count-computer: #f3ead8;
|
|
--die-player: #ede3cf;
|
|
--pip-player: #2b1d15;
|
|
--die-computer: #140d09;
|
|
--pip-computer: #ede3cf;
|
|
--cube: #ede3cf;
|
|
--cube-text: #2b1d15;
|
|
--highlight: #e07a5a;
|
|
--target: rgba(224, 122, 90, 0.38);
|
|
|
|
--point-label: var(--point-number);
|
|
|
|
--shadow-raised: 0 1px 2px rgba(0, 0, 0, 0.4);
|
|
--scrim: rgba(0, 0, 0, 0.6);
|
|
color-scheme: dark;
|
|
}
|
|
|
|
/* -------------------------------------------------------------- Page */
|
|
|
|
* {
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
/* Pas de « tirer pour rafraîchir » ni de rebond qui recharge la page en
|
|
* pleine partie ; pas de texte sélectionné par un appui long. */
|
|
html {
|
|
-webkit-text-size-adjust: 100%;
|
|
text-size-adjust: 100%;
|
|
overscroll-behavior: none;
|
|
}
|
|
|
|
body {
|
|
margin: 0 auto;
|
|
max-width: var(--page-width);
|
|
padding: 0 var(--space-4);
|
|
font-family: var(--font-sans);
|
|
font-size: 17px;
|
|
line-height: 25px;
|
|
color: var(--text);
|
|
background: var(--background);
|
|
overscroll-behavior: none;
|
|
user-select: none;
|
|
-webkit-user-select: none;
|
|
}
|
|
|
|
[hidden] {
|
|
display: none !important;
|
|
}
|
|
|
|
a {
|
|
color: var(--accent);
|
|
text-underline-offset: 3px;
|
|
}
|
|
|
|
/* Anneau de focus clavier, sur tout élément interactif. */
|
|
:focus-visible {
|
|
outline: 3px solid var(--focus);
|
|
outline-offset: 2px;
|
|
}
|
|
|
|
/* L'écran de jeu en portrait : des zones de hauteur fixe, de haut en bas ;
|
|
* la barre d'actions reste collée en bas de l'écran. */
|
|
.screen {
|
|
display: flex;
|
|
flex-direction: column;
|
|
min-height: 100vh;
|
|
min-height: 100dvh;
|
|
}
|
|
|
|
.game {
|
|
display: flex;
|
|
flex: 1;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.spacer {
|
|
flex: 1;
|
|
}
|
|
|
|
/* Logo clair ou sombre selon le thème. */
|
|
.logo-dark {
|
|
display: none;
|
|
}
|
|
|
|
@media (prefers-color-scheme: dark) {
|
|
:root:not([data-theme="light"]) .logo-light { display: none; }
|
|
:root:not([data-theme="light"]) .logo-dark { display: block; }
|
|
}
|
|
|
|
:root[data-theme="dark"] .logo-light { display: none; }
|
|
:root[data-theme="dark"] .logo-dark { display: block; }
|
|
|
|
/* -------------------------------------------------------- Barre haute */
|
|
|
|
.topbar {
|
|
display: flex;
|
|
flex: none;
|
|
gap: var(--space-1);
|
|
align-items: center;
|
|
height: var(--topbar-height);
|
|
}
|
|
|
|
.topbar .brand {
|
|
flex: 1;
|
|
display: flex;
|
|
align-items: center;
|
|
min-width: 0;
|
|
margin: 0;
|
|
}
|
|
|
|
.topbar .brand img {
|
|
width: auto;
|
|
max-width: 100%;
|
|
height: 28px;
|
|
}
|
|
|
|
/* ------------------------------------------------------- Pictogrammes */
|
|
|
|
.icon {
|
|
width: 24px;
|
|
height: 24px;
|
|
fill: none;
|
|
stroke: currentColor;
|
|
stroke-width: 2;
|
|
stroke-linecap: round;
|
|
stroke-linejoin: round;
|
|
flex: none;
|
|
}
|
|
|
|
/* ------------------------------------------------------------ Boutons */
|
|
|
|
/* Ronds comme des pions ; un seul bouton principal (brique) par écran,
|
|
* les autres en contour. */
|
|
button {
|
|
min-height: var(--button-height);
|
|
padding: 0 20px;
|
|
font: inherit;
|
|
font-weight: 700;
|
|
color: var(--on-accent);
|
|
background: var(--accent);
|
|
border: 2px solid var(--accent);
|
|
border-radius: var(--radius-pill);
|
|
cursor: pointer;
|
|
/* Pas de délai ni de zoom sur un double toucher. */
|
|
touch-action: manipulation;
|
|
-webkit-tap-highlight-color: transparent;
|
|
transition: background-color var(--duration-instant) var(--easing-standard),
|
|
filter var(--duration-instant) var(--easing-standard);
|
|
}
|
|
|
|
button.secondary {
|
|
color: var(--text);
|
|
background: transparent;
|
|
border-color: var(--border-strong);
|
|
}
|
|
|
|
@media (hover: hover) {
|
|
button.secondary:hover:not(:disabled) {
|
|
background: var(--surface-sunken);
|
|
}
|
|
}
|
|
|
|
/* Retour visuel au toucher : le bouton descend de 1 px et s'assombrit de
|
|
* 6 %, sans ombre. */
|
|
button:active:not(:disabled) {
|
|
transform: translateY(1px);
|
|
filter: brightness(0.94);
|
|
}
|
|
|
|
button:disabled {
|
|
opacity: 0.45;
|
|
cursor: default;
|
|
}
|
|
|
|
/* Pictogramme seul (ou avec son libellé de 13 px dessous, barre haute). */
|
|
.icon-button {
|
|
display: inline-flex;
|
|
flex-direction: column;
|
|
flex: none;
|
|
align-items: center;
|
|
justify-content: center;
|
|
min-width: 44px;
|
|
min-height: 44px;
|
|
padding: 0;
|
|
color: var(--text);
|
|
background: transparent;
|
|
border: 0;
|
|
border-radius: var(--radius-md);
|
|
}
|
|
|
|
.icon-button.labelled {
|
|
width: 60px;
|
|
height: 52px;
|
|
}
|
|
|
|
.icon-button .label {
|
|
font-size: 13px;
|
|
line-height: 15px;
|
|
font-weight: 500;
|
|
}
|
|
|
|
@media (hover: hover) {
|
|
.icon-button:hover:not(:disabled) {
|
|
background: var(--surface-sunken);
|
|
}
|
|
}
|
|
|
|
/* ------------------------------------------- Bandeaux des joueurs */
|
|
|
|
/* Un bandeau par camp, au-dessus du plateau pour l'ordinateur, dessous pour
|
|
* vous ; celui qui a la main est teinté. */
|
|
.strip {
|
|
display: flex;
|
|
flex: none;
|
|
gap: 10px;
|
|
align-items: center;
|
|
height: var(--strip-height);
|
|
padding: 0 4px 0 12px;
|
|
border-radius: var(--radius-md);
|
|
}
|
|
|
|
.strip.turn {
|
|
background: var(--accent-soft);
|
|
}
|
|
|
|
.strip .who {
|
|
flex: 1;
|
|
min-width: 0;
|
|
overflow: hidden;
|
|
font-weight: 700;
|
|
white-space: nowrap;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
.strip .lead {
|
|
font-size: 15px;
|
|
line-height: 20px;
|
|
color: var(--muted);
|
|
white-space: nowrap;
|
|
}
|
|
|
|
/* Le compte de points : 20 px gras, chiffres alignés ; il se touche pour
|
|
* ouvrir la bulle du glossaire. */
|
|
.strip .count {
|
|
min-width: 56px;
|
|
min-height: 44px;
|
|
padding: 0 8px;
|
|
font-size: 20px;
|
|
line-height: 24px;
|
|
font-weight: 700;
|
|
font-variant-numeric: tabular-nums lining-nums;
|
|
text-align: right;
|
|
color: var(--text);
|
|
background: transparent;
|
|
border: 0;
|
|
border-radius: var(--radius-md);
|
|
}
|
|
|
|
.mini {
|
|
flex: none;
|
|
width: 18px;
|
|
height: 18px;
|
|
border-radius: 50%;
|
|
}
|
|
|
|
.mini-player {
|
|
background: var(--checker-player);
|
|
border: 2px solid var(--checker-player-edge);
|
|
}
|
|
|
|
.mini-computer {
|
|
background: var(--checker-computer);
|
|
border: 2px solid var(--checker-computer-edge);
|
|
}
|
|
|
|
/* La bulle du compte de points, posée sur le plateau, du côté du bandeau
|
|
* touché : elle ne déplace rien. */
|
|
.board-box {
|
|
position: relative;
|
|
flex: none;
|
|
}
|
|
|
|
/* Téléphone en portrait : le plateau, héros de l'écran, prend la largeur
|
|
* presque entière (8 px de marge au lieu de 16). */
|
|
@media (max-width: 40rem) {
|
|
html:not(.landscape) .board-box {
|
|
margin-inline: calc(var(--space-2) - var(--space-4));
|
|
}
|
|
}
|
|
|
|
.bubble {
|
|
position: absolute;
|
|
left: var(--space-2);
|
|
right: var(--space-2);
|
|
z-index: 5;
|
|
margin: 0;
|
|
padding: 10px 14px;
|
|
font-size: 15px;
|
|
line-height: 20px;
|
|
color: var(--text);
|
|
background: var(--surface);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius-md);
|
|
box-shadow: var(--shadow-raised);
|
|
animation: fade-in var(--duration-fast) var(--easing-standard);
|
|
}
|
|
|
|
.bubble[data-side="computer"] { top: var(--space-2); }
|
|
.bubble[data-side="player"] { bottom: var(--space-2); }
|
|
|
|
.bubble p {
|
|
margin: 0 0 var(--space-1);
|
|
}
|
|
|
|
.bubble-term {
|
|
font-weight: 700;
|
|
}
|
|
|
|
.link-button {
|
|
min-height: 44px;
|
|
padding: 0;
|
|
color: var(--accent);
|
|
background: transparent;
|
|
border: 0;
|
|
border-radius: 0;
|
|
text-decoration: underline;
|
|
text-underline-offset: 3px;
|
|
}
|
|
|
|
/* ----------------------------------------------- Sous le plateau */
|
|
|
|
/* Ligne d'information, toujours réservée : ce qui vient de se passer (coup
|
|
* de l'ordinateur, jet d'ouverture, chargement), et « Revoir » au bout. */
|
|
.infoline {
|
|
display: flex;
|
|
flex: none;
|
|
gap: var(--space-2);
|
|
align-items: center;
|
|
height: var(--infoline-height);
|
|
padding-left: 12px;
|
|
}
|
|
|
|
.info {
|
|
display: -webkit-box;
|
|
flex: 1;
|
|
margin: 0;
|
|
overflow: hidden;
|
|
font-size: 15px;
|
|
line-height: 20px;
|
|
color: var(--muted);
|
|
-webkit-box-orient: vertical;
|
|
-webkit-line-clamp: 2;
|
|
line-clamp: 2;
|
|
}
|
|
|
|
.infoline .replay {
|
|
color: var(--accent);
|
|
}
|
|
|
|
/* La consigne du moment : ce qui doit ressortir de l'écran. Texte nu,
|
|
* 17 px gras, sur une hauteur réservée de deux lignes ; rien quand il n'y a
|
|
* rien à dire. La teinte « c'est à vous » est celle du bandeau du joueur. */
|
|
.prompt {
|
|
flex: none;
|
|
height: var(--prompt-height);
|
|
margin: var(--space-2) 0 0;
|
|
padding: 0 12px;
|
|
overflow: hidden;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.prompt.neutral {
|
|
font-weight: 400;
|
|
color: var(--muted);
|
|
}
|
|
|
|
/* L'ordinateur réfléchit : trois points qui apparaissent l'un après
|
|
* l'autre (la seule animation en boucle). */
|
|
.prompt.thinking::after {
|
|
content: "";
|
|
animation: thinking 1.2s steps(4, end) infinite;
|
|
}
|
|
|
|
@keyframes thinking {
|
|
0% { content: ""; }
|
|
25% { content: "."; }
|
|
50% { content: ".."; }
|
|
75% { content: "..."; }
|
|
}
|
|
|
|
/* Barre d'actions collée en bas : l'action secondaire à gauche, la
|
|
* principale à droite, sous le pouce. */
|
|
.actions {
|
|
position: sticky;
|
|
bottom: 0;
|
|
z-index: 2;
|
|
display: grid;
|
|
flex: none;
|
|
grid-template-columns: 1fr 1.6fr;
|
|
gap: var(--space-2);
|
|
height: var(--actions-height);
|
|
padding: var(--space-3) 0 var(--space-4);
|
|
background: var(--background);
|
|
border-top: 1px solid var(--border);
|
|
}
|
|
|
|
.actions .slot {
|
|
display: flex;
|
|
min-width: 0;
|
|
}
|
|
|
|
.actions .slot button {
|
|
flex: 1;
|
|
min-width: 0;
|
|
padding: 0 12px;
|
|
display: inline-flex;
|
|
gap: 6px;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.actions .slot .icon {
|
|
width: 20px;
|
|
height: 20px;
|
|
}
|
|
|
|
.move-hint {
|
|
align-self: center;
|
|
margin: 0;
|
|
font-size: 15px;
|
|
line-height: 20px;
|
|
color: var(--muted);
|
|
text-align: center;
|
|
}
|
|
|
|
/* --------------------------------------------------- Fenêtres */
|
|
|
|
/* Voile sous le menu et les panneaux : il assombrit l'écran derrière. */
|
|
.veil {
|
|
position: fixed;
|
|
inset: 0;
|
|
z-index: 10;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: var(--space-4);
|
|
background: var(--scrim);
|
|
animation: fade-in var(--duration-fast) var(--easing-standard);
|
|
}
|
|
|
|
/* Le menu : une feuille qui monte du bas. */
|
|
.sheet-veil {
|
|
z-index: 11; /* ouvert, le menu passe devant un panneau */
|
|
align-items: flex-end;
|
|
padding: 0;
|
|
}
|
|
|
|
.sheet {
|
|
width: min(100%, var(--page-width));
|
|
max-height: 100%;
|
|
overflow-y: auto;
|
|
padding: var(--space-2) var(--space-4) var(--space-6);
|
|
color: var(--text);
|
|
background: var(--surface);
|
|
border-radius: 20px 20px 0 0;
|
|
box-shadow: 0 -2px 12px rgba(0, 0, 0, 0.2);
|
|
animation: sheet-up var(--duration-moderate) var(--easing-standard);
|
|
}
|
|
|
|
.sheet-head {
|
|
position: relative;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
min-height: 52px;
|
|
}
|
|
|
|
.grip {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 50%;
|
|
width: 40px;
|
|
height: 4px;
|
|
margin-left: -20px;
|
|
background: var(--border-strong);
|
|
border-radius: 2px;
|
|
}
|
|
|
|
.sheet-title {
|
|
margin: 0;
|
|
font-size: 20px;
|
|
line-height: 26px;
|
|
}
|
|
|
|
.sheet .item {
|
|
display: flex;
|
|
gap: 14px;
|
|
align-items: center;
|
|
width: 100%;
|
|
min-height: 52px;
|
|
padding: 0;
|
|
font-weight: 500;
|
|
text-align: left;
|
|
color: var(--text);
|
|
background: transparent;
|
|
border: 0;
|
|
border-bottom: 1px solid var(--border);
|
|
border-radius: 0;
|
|
}
|
|
|
|
.sheet .item:last-child {
|
|
border-bottom: 0;
|
|
}
|
|
|
|
.sheet .item .icon {
|
|
color: var(--muted);
|
|
}
|
|
|
|
.item-value {
|
|
margin-left: auto;
|
|
font-size: 15px;
|
|
color: var(--muted);
|
|
}
|
|
|
|
/* Interrupteur (Vibrations, écran allumé). */
|
|
.switch {
|
|
position: relative;
|
|
flex: none;
|
|
width: 44px;
|
|
height: 26px;
|
|
margin-left: auto;
|
|
background: var(--border-strong);
|
|
border-radius: 13px;
|
|
transition: background-color var(--duration-fast) var(--easing-standard);
|
|
}
|
|
|
|
.switch::after {
|
|
content: "";
|
|
position: absolute;
|
|
top: 3px;
|
|
left: 3px;
|
|
width: 20px;
|
|
height: 20px;
|
|
background: var(--surface);
|
|
border-radius: 50%;
|
|
transition: transform var(--duration-fast) var(--easing-standard);
|
|
}
|
|
|
|
.item[aria-checked="true"] .switch {
|
|
background: var(--accent);
|
|
}
|
|
|
|
.item[aria-checked="true"] .switch::after {
|
|
background: var(--on-accent);
|
|
transform: translateX(18px);
|
|
}
|
|
|
|
/* Panneau centré : confirmation du double, fin de partie, double de
|
|
* l'ordinateur en paysage. */
|
|
.panel {
|
|
width: min(24rem, 100%);
|
|
padding: var(--space-4);
|
|
color: var(--text);
|
|
background: var(--surface);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius-md);
|
|
box-shadow: var(--shadow-raised);
|
|
animation: panel-in var(--duration-moderate) var(--easing-standard);
|
|
}
|
|
|
|
.panel p {
|
|
margin: 0 0 var(--space-4);
|
|
font-size: 20px;
|
|
line-height: 26px;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.panel p:empty {
|
|
display: none;
|
|
}
|
|
|
|
/* Fin de partie : surtitre, titre de 24 px, puis le détail des points. */
|
|
.panel .overline {
|
|
margin: 0 0 var(--space-1);
|
|
font-size: 13px;
|
|
line-height: 16px;
|
|
letter-spacing: 0.08em;
|
|
text-transform: uppercase;
|
|
color: var(--muted);
|
|
}
|
|
|
|
.panel .panel-title {
|
|
margin-bottom: var(--space-1);
|
|
font-size: 24px;
|
|
line-height: 30px;
|
|
}
|
|
|
|
.panel .panel-detail {
|
|
font-size: 17px;
|
|
line-height: 25px;
|
|
font-weight: 400;
|
|
}
|
|
|
|
.panel-actions {
|
|
display: flex;
|
|
gap: var(--space-2);
|
|
}
|
|
|
|
.panel-actions button {
|
|
flex: 1 1 0;
|
|
padding: 0 12px;
|
|
}
|
|
|
|
/* « Nouvelle partie commencée. Reprendre l'ancienne », 10 s, avec sa
|
|
* barre de temps. */
|
|
.banner {
|
|
position: fixed;
|
|
left: 50%;
|
|
bottom: calc(var(--actions-height) + var(--space-2));
|
|
z-index: 9;
|
|
display: flex;
|
|
gap: var(--space-3);
|
|
align-items: center;
|
|
width: min(24rem, calc(100% - 2 * var(--space-4)));
|
|
padding: var(--space-2) var(--space-3) var(--space-3);
|
|
overflow: hidden;
|
|
font-size: 15px;
|
|
line-height: 20px;
|
|
background: var(--surface);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius-md);
|
|
box-shadow: var(--shadow-raised);
|
|
transform: translateX(-50%);
|
|
animation: fade-in var(--duration-fast) var(--easing-standard);
|
|
}
|
|
|
|
.banner span:first-child {
|
|
flex: 1;
|
|
}
|
|
|
|
.banner button {
|
|
flex: none;
|
|
min-height: 44px;
|
|
padding: 0 14px;
|
|
}
|
|
|
|
.banner-timer {
|
|
position: absolute;
|
|
left: 0;
|
|
bottom: 0;
|
|
width: 100%;
|
|
height: 4px;
|
|
background: var(--accent);
|
|
transform-origin: left;
|
|
}
|
|
|
|
.banner-timer.running {
|
|
animation: banner-timer 10s linear forwards;
|
|
}
|
|
|
|
@keyframes banner-timer {
|
|
from { transform: scaleX(1); }
|
|
to { transform: scaleX(0); }
|
|
}
|
|
|
|
@keyframes fade-in {
|
|
from { opacity: 0; }
|
|
to { opacity: 1; }
|
|
}
|
|
|
|
@keyframes sheet-up {
|
|
from { transform: translateY(100%); }
|
|
to { transform: translateY(0); }
|
|
}
|
|
|
|
@keyframes panel-in {
|
|
from { opacity: 0; transform: translateY(8px); }
|
|
to { opacity: 1; transform: translateY(0); }
|
|
}
|
|
|
|
/* Mouvement réduit : tout est instantané, rien ne roule ni ne glisse ; la
|
|
* barre de temps reste visible, immobile. */
|
|
@media (prefers-reduced-motion: reduce) {
|
|
*,
|
|
*::before,
|
|
*::after {
|
|
animation-duration: 0s !important;
|
|
transition-duration: 0s !important;
|
|
}
|
|
|
|
.prompt.thinking::after {
|
|
animation: none;
|
|
content: "…";
|
|
}
|
|
|
|
.banner-timer.running {
|
|
animation: none;
|
|
}
|
|
|
|
button:active:not(:disabled) {
|
|
transform: none;
|
|
}
|
|
}
|
|
|
|
/* ------------------------------------------------------------ Plateau */
|
|
|
|
.board {
|
|
display: block;
|
|
width: 100%;
|
|
height: auto;
|
|
aspect-ratio: 640 / 520;
|
|
user-select: none;
|
|
-webkit-user-select: none;
|
|
touch-action: manipulation;
|
|
-webkit-tap-highlight-color: transparent;
|
|
font-family: var(--font-sans);
|
|
/* Alegreya Sans dessine par défaut des chiffres elzéviriens (de hauteurs
|
|
* variables) : sur le plateau, des chiffres alignés se lisent mieux. */
|
|
font-variant-numeric: tabular-nums lining-nums;
|
|
}
|
|
|
|
.board-frame { fill: var(--frame); }
|
|
.board-bed { fill: var(--bed); }
|
|
.board-bar { fill: var(--bar); }
|
|
.board-tray { fill: var(--tray); }
|
|
.point-a { fill: var(--point-a); }
|
|
.point-b { fill: var(--point-b); }
|
|
|
|
/* Numéros des flèches : leur taille est une mesure du dessin (attribut
|
|
* font-size), calculée pour faire au moins 12 px à l'écran (geometry.ts). */
|
|
.point-number {
|
|
fill: var(--point-label);
|
|
font-weight: 500;
|
|
text-anchor: middle;
|
|
}
|
|
|
|
.checker {
|
|
stroke-width: 2;
|
|
}
|
|
|
|
.checker-player {
|
|
fill: var(--checker-player);
|
|
stroke: var(--checker-player-edge);
|
|
}
|
|
|
|
.checker-computer {
|
|
fill: var(--checker-computer);
|
|
stroke: var(--checker-computer-edge);
|
|
}
|
|
|
|
/* Filet intérieur des pions (bundle.css), prêt pour le jour où le plateau
|
|
* le dessine. */
|
|
.checker-ring {
|
|
fill: none;
|
|
stroke-width: 1.5;
|
|
}
|
|
|
|
.ring-player { stroke: var(--checker-player-edge); }
|
|
.ring-computer { stroke: var(--checker-computer-edge); }
|
|
|
|
.checker.movable {
|
|
stroke: var(--highlight);
|
|
stroke-width: 3;
|
|
}
|
|
|
|
.checker.selected {
|
|
stroke: var(--highlight);
|
|
stroke-width: 6;
|
|
}
|
|
|
|
/* Tailles des chiffres du plateau : attribut font-size, calculé par
|
|
* geometry.ts (13 px au moins à l'écran). */
|
|
.checker-count {
|
|
font-weight: 700;
|
|
text-anchor: middle;
|
|
}
|
|
|
|
.count-player { fill: var(--count-player); }
|
|
.count-computer { fill: var(--count-computer); }
|
|
|
|
.target-zone { fill: var(--target); }
|
|
/* Le dernier coup de l'ordinateur (couleur laiton, charte) : une flèche par
|
|
* demi-coup, les pions arrivés et frappés entourés, un pion fantôme là où
|
|
* un pion est parti. */
|
|
/* Épaisseur des flèches : attribut stroke-width (2,5 px à l'écran,
|
|
* geometry.ts) ; elles passent sous les pions. */
|
|
.trace-arrow {
|
|
stroke: var(--last-move);
|
|
stroke-linecap: round;
|
|
opacity: 0.85;
|
|
}
|
|
|
|
.trace-head { fill: var(--last-move); }
|
|
|
|
/* Où le pion touché se posera : un anneau pointillé, comme les fantômes du
|
|
* coup de l'ordinateur, en brique. */
|
|
.landing {
|
|
fill: none;
|
|
stroke: var(--highlight);
|
|
stroke-width: 3;
|
|
stroke-dasharray: 6 5;
|
|
}
|
|
|
|
.trace-landed,
|
|
.trace-hit {
|
|
fill: none;
|
|
stroke: var(--last-move);
|
|
stroke-width: 4;
|
|
}
|
|
|
|
.trace-hit { stroke-dasharray: 6 4; }
|
|
|
|
.trace-ghost {
|
|
fill: none;
|
|
stroke: var(--last-move);
|
|
stroke-width: 2.5;
|
|
stroke-dasharray: 5 4;
|
|
}
|
|
|
|
.die-player { fill: var(--die-player); stroke: var(--checker-player-edge); stroke-width: 1.5; }
|
|
.die-computer { fill: var(--die-computer); stroke: var(--checker-computer-edge); stroke-width: 1.5; }
|
|
.pip-player { fill: var(--pip-player); }
|
|
.pip-computer { fill: var(--pip-computer); }
|
|
|
|
/* Dés du joueur en attente d'être lancés, et videau quand il peut doubler :
|
|
* ils se touchent comme les boutons. */
|
|
.die-waiting rect {
|
|
stroke: var(--highlight);
|
|
stroke-width: 2.5;
|
|
stroke-dasharray: 5 4;
|
|
}
|
|
|
|
.cube.can-double .cube-face {
|
|
stroke: var(--highlight);
|
|
stroke-width: 3;
|
|
}
|
|
|
|
/* Jet sans coup possible : dés estompés et barrés. */
|
|
.die-blocked { opacity: 0.5; }
|
|
|
|
.die-strike {
|
|
stroke: var(--text);
|
|
stroke-width: 3;
|
|
stroke-linecap: round;
|
|
}
|
|
|
|
/* Dé déjà utilisé par un demi-coup du coup en cours : estompé, sans être
|
|
* barré (il n'est pas perdu : « Annuler » le rend). */
|
|
.die-used { opacity: 0.35; }
|
|
|
|
.cube-face { fill: var(--cube); stroke: var(--checker-player-edge); stroke-width: 2; }
|
|
|
|
.cube-value {
|
|
fill: var(--cube-text);
|
|
font-weight: 800;
|
|
text-anchor: middle;
|
|
}
|
|
|
|
.hit {
|
|
fill: transparent;
|
|
cursor: pointer;
|
|
}
|
|
|
|
/* Paysage : numéros sur le cadre (ivoire), comptes de points en pastilles
|
|
* aux bouts de la barre, consigne dans la bande, commandes à gauche. */
|
|
.point-number.on-frame { fill: var(--point-number); }
|
|
|
|
.pip-chip { stroke-width: 1.5; }
|
|
.chip-player { fill: var(--checker-player); stroke: var(--checker-player-edge); }
|
|
.chip-computer { fill: var(--checker-computer); stroke: var(--checker-computer-edge); }
|
|
|
|
.pip-chip-text {
|
|
font-weight: 700;
|
|
text-anchor: middle;
|
|
}
|
|
|
|
.chip-text-player { fill: var(--pip-player); }
|
|
.chip-text-computer { fill: var(--pip-computer); }
|
|
|
|
.band-pill { fill: var(--accent-soft); }
|
|
|
|
.band-text {
|
|
fill: var(--text);
|
|
font-weight: 700;
|
|
text-anchor: middle;
|
|
}
|
|
|
|
.control-circle {
|
|
fill: none;
|
|
stroke: var(--point-number);
|
|
stroke-width: 2;
|
|
}
|
|
|
|
.control-icon {
|
|
fill: none;
|
|
stroke: var(--point-number);
|
|
stroke-width: 2px;
|
|
stroke-linecap: round;
|
|
stroke-linejoin: round;
|
|
vector-effect: non-scaling-stroke;
|
|
}
|
|
|
|
.control.disabled { opacity: 0.35; }
|
|
|
|
/* ------------------------------------------------------- Mode paysage */
|
|
|
|
/* Téléphone tourné (ADR 0012) : le plateau est l'écran, sur la hauteur
|
|
* réelle (barre d'adresse comprise), cadre noyer jusqu'aux bords. La
|
|
* classe vient de app.ts (rotation du téléphone, ou repli du plein écran). */
|
|
html.landscape,
|
|
html.landscape body {
|
|
height: 100%;
|
|
overflow: hidden;
|
|
}
|
|
|
|
html.landscape body {
|
|
max-width: none;
|
|
padding: 0;
|
|
background: var(--frame);
|
|
}
|
|
|
|
html.landscape .topbar,
|
|
html.landscape .strip,
|
|
html.landscape .infoline,
|
|
html.landscape .prompt,
|
|
html.landscape .spacer,
|
|
html.landscape .actions {
|
|
display: none;
|
|
}
|
|
|
|
html.landscape .board-box {
|
|
position: fixed;
|
|
inset: 0;
|
|
}
|
|
|
|
html.landscape .board {
|
|
width: 100%;
|
|
height: 100%;
|
|
aspect-ratio: auto;
|
|
}
|
|
|
|
html.landscape .banner {
|
|
bottom: var(--space-2);
|
|
}
|
|
|
|
/* L'aide reste une page qu'on fait défiler. */
|
|
html.landscape.help-open,
|
|
html.landscape.help-open body {
|
|
height: auto;
|
|
overflow: visible;
|
|
}
|
|
|
|
html.landscape.help-open body {
|
|
padding: 0 var(--space-4);
|
|
background: var(--background);
|
|
}
|
|
|
|
/* Repli du plein écran : le paysage ne peut pas être verrouillé, l'écran
|
|
* entier tourne d'un quart de tour dans le portrait. Les touchers suivent
|
|
* la rotation ; le plateau et les fenêtres sont posés dans #screen. */
|
|
html.rotated .screen {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100vh;
|
|
width: 100dvh;
|
|
height: 100vw;
|
|
min-height: 0;
|
|
overflow: hidden;
|
|
transform-origin: top left;
|
|
transform: rotate(90deg) translateY(-100%);
|
|
}
|
|
|
|
html.rotated .board-box,
|
|
html.rotated .veil {
|
|
position: absolute;
|
|
}
|
|
|
|
html.rotated .banner {
|
|
position: absolute;
|
|
}
|
|
|
|
/* ------------------------------------------------------- Mes parties */
|
|
|
|
/* La feuille « Mes parties » (app/src/ui/games-view.ts) : une partie par
|
|
* ligne, ses actions dessous, ses branches dépliables. */
|
|
.games-list,
|
|
.games-branches {
|
|
margin: 0;
|
|
padding: 0;
|
|
list-style: none;
|
|
}
|
|
|
|
.games-item {
|
|
padding: var(--space-2) 0 var(--space-4);
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
|
|
.games-item:last-child {
|
|
border-bottom: 0;
|
|
}
|
|
|
|
.games-item p {
|
|
margin: 0 0 var(--space-2);
|
|
font-weight: 500;
|
|
}
|
|
|
|
.games-actions {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: var(--space-2);
|
|
}
|
|
|
|
.games-branch {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: var(--space-2);
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding-top: var(--space-2);
|
|
color: var(--muted);
|
|
}
|
|
|
|
/* --------------------------------------------------------------- Aide */
|
|
|
|
.help {
|
|
max-width: var(--page-width);
|
|
margin: 0 auto;
|
|
}
|
|
|
|
.help h2 {
|
|
margin: var(--space-8) 0 var(--space-2);
|
|
font-size: 24px;
|
|
line-height: 30px;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.help h3 {
|
|
margin: var(--space-6) 0 var(--space-1);
|
|
font-size: 20px;
|
|
line-height: 26px;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.help p {
|
|
margin: var(--space-2) 0;
|
|
}
|
|
|
|
.steps {
|
|
margin: var(--space-2) 0;
|
|
padding-left: var(--space-6);
|
|
}
|
|
|
|
.steps li + li {
|
|
margin-top: var(--space-1);
|
|
}
|
|
|
|
.steps li::marker {
|
|
color: var(--muted);
|
|
}
|
|
|
|
/* Glossaire : une liste de définitions, sur une carte. */
|
|
.glossary {
|
|
margin: var(--space-3) 0 0;
|
|
padding: var(--space-1) var(--space-4);
|
|
background: var(--surface);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius-md);
|
|
box-shadow: var(--shadow-raised);
|
|
}
|
|
|
|
.glossary dt {
|
|
padding-top: var(--space-3);
|
|
font-weight: 700;
|
|
}
|
|
|
|
.glossary dd {
|
|
margin: 0;
|
|
padding-bottom: var(--space-3);
|
|
}
|
|
|
|
.glossary dd + dt {
|
|
border-top: 1px solid var(--border);
|
|
}
|
|
|
|
.about {
|
|
margin-top: var(--space-8);
|
|
font-size: 15px;
|
|
line-height: 20px;
|
|
color: var(--muted);
|
|
}
|
|
|
|
.help {
|
|
user-select: text;
|
|
-webkit-user-select: text;
|
|
}
|