Recette 1g : textes corrigés, boutons en bas, barre haute en paysage
Leçon 1 §2 (flèche 7 vide, 5 déjà bloqué), leçon 2 « Faire des points », flèches 7 à 12, reformulations des leçons 3 et 4. Écran des leçons et du tutoriel : « Retour » et « Continuer » en bas en portrait, « Revoir » sur la ligne du titre, ligne « Leçon n · … » gardée en paysage. Barre haute visible hors de l'écran de jeu en paysage (défaut antérieur à 1g), avec un contrôle dans la recette automatique. Rappel des règles haut de 44 px ; « Aucune leçon terminée sur 5 · … » sur l'accueil. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
5d0f7a9616
commit
351482fa9e
@@ -41,7 +41,9 @@ export const HOME_TEXTS = {
|
||||
rulesContinue: (step: number) => `Reprendre à l'étape ${step}`,
|
||||
rulesAgain: "Revoir",
|
||||
progress: (played: number, won: number) =>
|
||||
`${plural(played, "partie jouée", "parties jouées")} · ${plural(won, "gagnée", "gagnées")}`,
|
||||
played === 0
|
||||
? "aucune partie jouée"
|
||||
: `${plural(played, "partie jouée", "parties jouées")} · ${plural(won, "gagnée", "gagnées")}`,
|
||||
footer: "Né à l'Atelier du Huit, à Cugnaux. Logiciel libre, sans publicité ni mise.",
|
||||
|
||||
// Installation
|
||||
|
||||
@@ -290,10 +290,11 @@ export function createHomeView(options: HomeViewOptions): HomeView {
|
||||
if (state.lessons !== null) {
|
||||
parts.push(LESSON_TEXTS.homeSummary(state.lessons.finished, state.lessons.total));
|
||||
}
|
||||
if (state.progress !== null && state.progress.played > 0) {
|
||||
if (state.progress !== null) {
|
||||
parts.push(HOME_TEXTS.progress(state.progress.played, state.progress.won));
|
||||
}
|
||||
progress.textContent = parts.join(" · ");
|
||||
const line = parts.join(" · ");
|
||||
progress.textContent = line.charAt(0).toUpperCase() + line.slice(1);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -53,5 +53,7 @@ export const LESSON_TEXTS = {
|
||||
homeAllDoneDetail: "Vous pouvez les revoir quand vous voulez.",
|
||||
allLessons: "Toutes les leçons",
|
||||
homeSummary: (finished: number, total: number) =>
|
||||
`${plural(finished, "leçon terminée", "leçons terminées")} sur ${total}`,
|
||||
finished === 0
|
||||
? `Aucune leçon terminée sur ${total}`
|
||||
: `${plural(finished, "leçon terminée", "leçons terminées")} sur ${total}`,
|
||||
} as const;
|
||||
|
||||
@@ -86,9 +86,10 @@ export function createLessonsView(section: HTMLElement): LessonsView {
|
||||
const head = element(section, "div", { className: "lessons-intro" });
|
||||
element(head, "h2", { text: LESSON_TEXTS.listTitle });
|
||||
element(head, "p", { className: "muted", text: LESSON_TEXTS.listLead });
|
||||
const rules = element(section, "p", { className: "lessons-rules caption" });
|
||||
rules.append(`${LESSON_TEXTS.rulesReminder} · `);
|
||||
const review = element(rules, "a", { id: "lessons-rules", text: LESSON_TEXTS.rulesReview });
|
||||
// Le rappel des règles : toute la ligne est le lien, haute de 44 px.
|
||||
const review = element(section, "a", { id: "lessons-rules", className: "lessons-rules" });
|
||||
review.append(`${LESSON_TEXTS.rulesReminder} · `);
|
||||
element(review, "span", { text: LESSON_TEXTS.rulesReview });
|
||||
review.href = "#/tutoriel/1";
|
||||
review.setAttribute("aria-label", LESSON_TEXTS.rulesReviewLabel);
|
||||
const status = element(section, "p", { id: "lessons-status", className: "muted" });
|
||||
|
||||
@@ -287,14 +287,16 @@ export function createTutorialView(options: TutorialViewOptions): TutorialView {
|
||||
skip.textContent = course.texts.quit;
|
||||
skip.setAttribute("aria-label", course.texts.quitLabel);
|
||||
const overline = element(section, "p", { id: idOf("overline"), className: "overline" });
|
||||
const title = element(section, "h2", { id: idOf("title"), className: "tutorial-title" });
|
||||
// Le titre, et « Revoir » à sa droite (en portrait comme en paysage).
|
||||
const titleRow = element(section, "div", { className: "tutorial-title-row" });
|
||||
const title = element(titleRow, "h2", { id: idOf("title"), className: "tutorial-title" });
|
||||
const boardBox = element(section, "div", { className: "tutorial-board-box" });
|
||||
const svg = document.createElementNS(SVG_NS, "svg");
|
||||
svg.id = idOf("board");
|
||||
svg.setAttribute("class", "board tutorial-board");
|
||||
svg.setAttribute("role", "img");
|
||||
boardBox.append(svg);
|
||||
const replay = element(boardBox, "button", {
|
||||
const replay = element(titleRow, "button", {
|
||||
id: idOf("replay"),
|
||||
className: "icon-button tutorial-replay",
|
||||
});
|
||||
|
||||
@@ -106,7 +106,7 @@ test("cinq leçons, de deux à quatre exercices chacune, avec un titre et une fi
|
||||
lessons.map((l) => l.title),
|
||||
[
|
||||
"Les ouvertures",
|
||||
"Construire ses cases",
|
||||
"Faire des points",
|
||||
"Frapper ou ne pas frapper",
|
||||
"La course et le compte de points",
|
||||
"Sortir sans se faire frapper",
|
||||
@@ -217,7 +217,9 @@ test("chaque coup attendu est le meilleur coup du moteur à 2 plis, avec plus de
|
||||
assert.equal(report.length, 16);
|
||||
});
|
||||
|
||||
test("chaque coup illustré est le meilleur coup du moteur à 2 plis", async () => {
|
||||
// Couvre aussi le « nettement devant tous les autres » de la leçon 1 (étape
|
||||
// 1) : chaque coup illustré a plus de MIN_GAP d'avance sur le deuxième.
|
||||
test("chaque coup illustré est le meilleur coup du moteur à 2 plis, avec plus de 0,04 d'avance", async () => {
|
||||
const engine = await loadNodeEngine();
|
||||
let illustrated = 0;
|
||||
for (const [name, screen] of allScreens()) {
|
||||
|
||||
@@ -27,9 +27,9 @@ Vous avez 4 et 2. Faites un point dans votre jan intérieur.
|
||||
|
||||
- position: 4HPwATDgc/ABMA
|
||||
|
||||
La flèche 7 ouvre votre [jan extérieur](jan-exterieur.md), juste à côté de la barre. Avec la flèche 6, déjà fermée par vos cinq pions, elle forme deux points côte à côte.
|
||||
La flèche 7 ouvre votre [jan extérieur](jan-exterieur.md), juste à côté de la barre. Fermée, elle formerait avec la flèche 6, où sont déjà vos cinq pions, deux points côte à côte.
|
||||
|
||||
Les deux pions noirs de votre flèche 1 doivent passer par là : depuis la flèche 1, un 5 les mène sur la flèche 6 et un 6 sur la flèche 7.
|
||||
Les deux pions noirs de votre flèche 1 doivent passer par là : depuis la flèche 1, un 6 les mène sur la flèche 7, et un 5 est déjà bloqué par votre point 6.
|
||||
|
||||
### À vous
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Construire ses cases
|
||||
# Faire des points
|
||||
|
||||
Leçon 2 d'Ustagammon : construire ses cases et faire des points. Texte sous
|
||||
Leçon 2 d'Ustagammon : construire son jeu en faisant des points. Texte sous
|
||||
licence CC BY-SA 4.0. Format : voir README.md.
|
||||
|
||||
## 1. Deux pions font un point
|
||||
@@ -27,7 +27,7 @@ Même position, vous avez 6 et 2. Votre pion de la flèche 5 est seul : couvrez-
|
||||
|
||||
- position: 4HPwATDQZ+EBMA
|
||||
|
||||
Un pion posé sur les flèches 7 à 11, dans votre [jan extérieur](jan-exterieur.md), peut entrer dans votre jan intérieur avec un seul dé.
|
||||
Un pion posé sur les flèches 7 à 12, dans votre [jan extérieur](jan-exterieur.md), peut entrer dans votre jan intérieur avec un seul dé.
|
||||
|
||||
Ici, un pion blanc est seul sur la flèche 5, un autre sur la flèche 9.
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ sous licence CC BY-SA 4.0. Format : voir README.md.
|
||||
|
||||
Un pion noir est seul sur votre flèche 7 : c'est un [blot](blot.md). Avec 6 et 1, je joue 13/7* 8/7. L'astérisque marque le pion frappé : il part sur la [barre](barre.md).
|
||||
|
||||
Mes deux pions restent ensemble sur la flèche 7 : le point est fait, aucun pion noir ne peut plus s'y poser.
|
||||
Les deux pions blancs restent ensemble sur la flèche 7 : le point est fait, aucun pion noir ne peut plus s'y poser.
|
||||
|
||||
### À vous
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ Vous avez 5 et 2.
|
||||
|
||||
Ici, les pions de l'ordinateur ont 81 flèches à parcourir, les vôtres 110 : il a 29 points d'avance. Son dernier pion est seul sur votre flèche 9.
|
||||
|
||||
Frappé, ce pion part sur la barre, et le compte de l'ordinateur remonte : de la flèche 16 de son côté jusqu'à la barre, 9 de plus.
|
||||
Frappé, ce pion part sur la barre, et le compte de l'ordinateur remonte : pour l'ordinateur, ce pion compte 16 ; sur la barre, il compte 25 : 9 de plus.
|
||||
|
||||
### À vous
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# language: fr
|
||||
Fonctionnalité: Progresser avec les leçons
|
||||
|
||||
Cinq leçons courtes, sur le vrai plateau : les ouvertures, construire ses
|
||||
cases, frapper ou non, la course et le compte de points, sortir sans se
|
||||
Cinq leçons courtes, sur le vrai plateau : les ouvertures, faire des
|
||||
points, frapper ou non, la course et le compte de points, sortir sans se
|
||||
faire frapper. Chacune explique une idée en quelques écrans, puis vous
|
||||
fait jouer le coup du maître. GNU Backgammon vérifie chaque coup.
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
||||
import { chromium, firefox } from "playwright";
|
||||
import { hiddenUnderActionBar, tutorialWidthProblems, tutorialWidths } from "./recette.mjs";
|
||||
import { hiddenUnderActionBar, missingTopbar, tutorialWidthProblems, tutorialWidths } from "./recette.mjs";
|
||||
|
||||
const [base, browserName, shots, updateBase, updateDir] = process.argv.slice(2);
|
||||
const browserType = { chromium, firefox }[browserName];
|
||||
@@ -117,6 +117,8 @@ async function capture(page, name, settle = async () => {}, sizes = SIZES) {
|
||||
await settle();
|
||||
const hidden = await hiddenUnderActionBar(page);
|
||||
check(hidden.length === 0, `${name} ${width}x${height} : caché sous la barre d'action : ${hidden.join(", ")}`);
|
||||
const topbar = await missingTopbar(page);
|
||||
check(topbar === null, `${name} : ${topbar}`);
|
||||
await page.screenshot({ path: `${dir}/${name}-${width}x${height}.png`, fullPage: true });
|
||||
captures++;
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
|
||||
import { mkdirSync } from "node:fs";
|
||||
import { chromium, firefox } from "playwright";
|
||||
import { hiddenUnderActionBar, tallButtons } from "./recette.mjs";
|
||||
import { hiddenUnderActionBar, missingTopbar, tallButtons } from "./recette.mjs";
|
||||
|
||||
const [base, browserName, shots] = process.argv.slice(2);
|
||||
const browserType = { chromium, firefox }[browserName];
|
||||
@@ -68,6 +68,8 @@ async function capture(page, name, settle = async () => {}) {
|
||||
await settle();
|
||||
const hidden = await hiddenUnderActionBar(page);
|
||||
check(hidden.length === 0, `${name} ${width}x${height} : caché sous la barre d'action : ${hidden.join(", ")}`);
|
||||
const topbar = await missingTopbar(page);
|
||||
check(topbar === null, `${name} : ${topbar}`);
|
||||
const tall = await tallButtons(page);
|
||||
check(tall.length === 0, `${name} ${width}x${height} : boutons étirés : ${tall.join(", ")}`);
|
||||
await page.screenshot({ path: `${dir}/${name}-${width}x${height}.png`, fullPage: true });
|
||||
@@ -155,7 +157,8 @@ try {
|
||||
await until(page, "carte de la leçon", () => {
|
||||
const card = document.getElementById("home-lesson");
|
||||
return location.hash === "#/accueil" && !card.hidden && !card.closest("[hidden]") &&
|
||||
card.textContent.includes("Leçon 1") && !document.getElementById("home-new").disabled;
|
||||
card.textContent.includes("Leçon 1") && !document.getElementById("home-new").disabled &&
|
||||
document.getElementById("home-progress").textContent.includes(" · ");
|
||||
});
|
||||
const home = await page.evaluate(() => ({
|
||||
go: document.getElementById("home-lesson-go").getAttribute("href"),
|
||||
@@ -170,7 +173,7 @@ try {
|
||||
}));
|
||||
check(home.go === "#/lecons/1/1" && home.goText === "Commencer" && home.goClass.includes("secondary") &&
|
||||
home.all === "#/lecons" && home.title === "Les ouvertures" && home.state === "À commencer · 4 exercices" &&
|
||||
home.summary.startsWith("0 leçon terminée sur 5") && home.primaries === 1,
|
||||
home.summary.startsWith("Aucune leçon terminée sur 5 · ") && home.primaries === 1,
|
||||
`carte de l'accueil : ${JSON.stringify(home)}`);
|
||||
await capture(page, "01-accueil-carte", async () => {
|
||||
await page.locator("#home-lesson").scrollIntoViewIfNeeded();
|
||||
@@ -188,6 +191,8 @@ try {
|
||||
[...document.querySelectorAll("#lessons a.button:not(.secondary)")].filter((el) => !el.closest("[hidden]")).length);
|
||||
check(primaries === 1, `liste : ${primaries} actions principales`);
|
||||
check(await page.getAttribute("#lessons-rules", "href") === "#/tutoriel/1", "liste : rappel des règles");
|
||||
const rulesHeight = await page.evaluate(() => document.getElementById("lessons-rules").getBoundingClientRect().height);
|
||||
check(rulesHeight >= 44, `liste : rappel des règles haut de ${rulesHeight} px`);
|
||||
await capture(page, "02-liste");
|
||||
log("liste : cinq leçons, la première proposée, une seule action principale, rappel des règles");
|
||||
|
||||
@@ -208,6 +213,18 @@ try {
|
||||
explain.segments === 4 && explain.skip === "#/lecons" && explain.back === "#/lecons" &&
|
||||
explain.next === "#/lecons/1/1/a-vous" && explain.topbar === "none" &&
|
||||
explain.glossary.includes("#/glossaire/jan-interieur"), `explication : ${JSON.stringify(explain)}`);
|
||||
// « Revoir » sur la ligne du titre ; « Retour » et « Continuer » en bas
|
||||
// de l'écran, sous le pouce (recette 1g).
|
||||
const layout = await page.evaluate(() => {
|
||||
const title = document.getElementById("lesson-title").getBoundingClientRect();
|
||||
const replay = document.getElementById("lesson-replay").getBoundingClientRect();
|
||||
const next = document.getElementById("lesson-next").getBoundingClientRect();
|
||||
return { titleTop: title.top, titleBottom: title.bottom, replayTop: replay.top,
|
||||
replayBottom: replay.bottom, nextBottom: next.bottom, height: innerHeight };
|
||||
});
|
||||
check(layout.replayTop < layout.titleBottom && layout.replayBottom > layout.titleTop,
|
||||
`« Revoir » hors de la ligne du titre : ${JSON.stringify(layout)}`);
|
||||
check(layout.nextBottom > layout.height - 64, `« Continuer » loin du bas : ${JSON.stringify(layout)}`);
|
||||
check(await page.locator("#lesson-board .trace-arrow").count() > 0, "le coup illustré n'a pas laissé de traces");
|
||||
await capture(page, "03-explication");
|
||||
log("explication : coup illustré 8/5 6/5 joué tout seul, barre de quatre étapes, liens du glossaire");
|
||||
|
||||
@@ -99,6 +99,27 @@ export async function hiddenUnderActionBar(target) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* La barre haute (Aide, Plein écran, Menu) hors de l'écran de jeu : sur une
|
||||
* page (accueil, leçons, aide…) qui n'a pas son propre en-tête, elle doit
|
||||
* rester visible, en paysage comme en portrait ; sans elle, le menu est
|
||||
* inaccessible (recette 1g). Renvoie un problème décrit, ou null.
|
||||
*/
|
||||
export async function missingTopbar(target) {
|
||||
return target.evaluate(() => {
|
||||
const root = document.documentElement;
|
||||
if (!root.classList.contains("page-open") || root.classList.contains("bare")) {
|
||||
return null;
|
||||
}
|
||||
const bar = document.querySelector(".topbar");
|
||||
const r = bar?.getBoundingClientRect();
|
||||
const shown = bar !== null && getComputedStyle(bar).display !== "none" && r.height > 0;
|
||||
return shown
|
||||
? null
|
||||
: `barre haute absente sur ${root.dataset.screen} (${innerWidth}x${innerHeight})`;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Les largeurs d'un écran du tutoriel, en px CSS : le plateau, la colonne
|
||||
* de texte, et la largeur de la page (scrollWidth) face à celle de l'écran.
|
||||
|
||||
+73
-25
@@ -1201,6 +1201,13 @@ html.landscape.page-open body {
|
||||
background: var(--background);
|
||||
}
|
||||
|
||||
/* Hors de l'écran de jeu, la barre haute (Aide, Plein écran, Menu) reste
|
||||
* en paysage : seul le plateau en paysage s'en passe. Les écrans qui ont
|
||||
* leur propre en-tête (bare) la masquent toujours. */
|
||||
html.landscape.page-open:not(.bare) .topbar {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
/* 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. */
|
||||
@@ -1876,17 +1883,20 @@ html.bare .topbar {
|
||||
.lessons-rules {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: fit-content;
|
||||
min-height: 44px;
|
||||
margin-top: var(--space-2);
|
||||
font-size: 15px;
|
||||
line-height: 20px;
|
||||
color: var(--muted);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.lessons-rules a {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 44px;
|
||||
.lessons-rules span {
|
||||
margin-left: var(--space-1);
|
||||
font-weight: 700;
|
||||
color: var(--accent);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
#lessons-status:empty {
|
||||
@@ -2136,8 +2146,28 @@ html.bare .topbar {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.tutorial-title {
|
||||
/* En portrait, l'écran tient au moins la hauteur de l'écran : « Retour »
|
||||
* et « Continuer » restent en bas, sous le pouce. */
|
||||
.tutorial {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
min-height: 100vh;
|
||||
min-height: 100dvh;
|
||||
}
|
||||
|
||||
/* Le titre, et « Revoir » sur sa ligne, à droite. */
|
||||
.tutorial-title-row {
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
align-items: center;
|
||||
margin: var(--space-1) 0 var(--space-3);
|
||||
}
|
||||
|
||||
.tutorial-title {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
line-height: 30px;
|
||||
}
|
||||
@@ -2152,16 +2182,13 @@ html.bare .topbar {
|
||||
}
|
||||
}
|
||||
|
||||
.tutorial-board-box .tutorial-replay {
|
||||
position: absolute;
|
||||
right: var(--space-1);
|
||||
bottom: calc(-44px - var(--space-1));
|
||||
.tutorial-replay {
|
||||
flex: none;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.tutorial-text {
|
||||
margin-top: var(--space-3);
|
||||
padding-right: 52px;
|
||||
}
|
||||
|
||||
.tutorial-text p {
|
||||
@@ -2214,7 +2241,8 @@ html.bare .topbar {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1.4fr;
|
||||
gap: var(--space-2);
|
||||
margin-top: var(--space-6);
|
||||
margin-top: auto;
|
||||
padding-top: var(--space-6);
|
||||
}
|
||||
|
||||
.tutorial-nav .button {
|
||||
@@ -2281,16 +2309,46 @@ html.landscape .tutorial-head {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
html.landscape .tutorial-title {
|
||||
html.landscape .tutorial-title-row {
|
||||
grid-row: 2;
|
||||
margin: 0 0 var(--space-1);
|
||||
padding-right: calc(44px + var(--space-4));
|
||||
padding-right: calc(var(--space-2) + env(safe-area-inset-right));
|
||||
}
|
||||
|
||||
html.landscape .tutorial-title {
|
||||
font-size: 20px;
|
||||
line-height: 26px;
|
||||
}
|
||||
|
||||
/* La boîte du plateau s'efface : le plateau et « Revoir » se placent
|
||||
* eux-mêmes dans la grille. */
|
||||
/* Une leçon garde en paysage sa ligne « Leçon 1 · 1 sur 4 · à vous »
|
||||
* au-dessus du titre (recette 1g) : une rangée de plus dans la grille. */
|
||||
html.landscape #lesson {
|
||||
grid-template-rows: auto auto auto minmax(0, 1fr) auto auto;
|
||||
}
|
||||
|
||||
html.landscape #lesson .overline {
|
||||
display: block;
|
||||
grid-row: 2;
|
||||
}
|
||||
|
||||
html.landscape #lesson .tutorial-title-row {
|
||||
grid-row: 3;
|
||||
}
|
||||
|
||||
html.landscape #lesson .tutorial-body {
|
||||
grid-row: 4;
|
||||
}
|
||||
|
||||
html.landscape #lesson .tutorial-cube {
|
||||
grid-row: 5;
|
||||
}
|
||||
|
||||
html.landscape #lesson .tutorial-nav {
|
||||
grid-row: 6;
|
||||
}
|
||||
|
||||
/* La boîte du plateau s'efface : le plateau se place lui-même dans la
|
||||
* grille. */
|
||||
html.landscape .tutorial-board-box {
|
||||
display: contents;
|
||||
}
|
||||
@@ -2305,16 +2363,6 @@ html.landscape .tutorial-board {
|
||||
background: var(--frame);
|
||||
}
|
||||
|
||||
html.landscape .tutorial-board-box .tutorial-replay {
|
||||
position: static;
|
||||
grid-row: 2;
|
||||
grid-column: 2;
|
||||
justify-self: end;
|
||||
align-self: start;
|
||||
margin: -9px calc(var(--space-2) + env(safe-area-inset-right)) 0 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
html.landscape .tutorial-body {
|
||||
grid-row: 3;
|
||||
min-height: 0;
|
||||
|
||||
Reference in New Issue
Block a user