Maquette : Article et Événement en paysage sur la borne (K3, K7, K8)

Une seule passe de design pour les deux écrans. Les trancher séparément
donnerait deux grammaires différentes sur la même dalle, alors qu'ils
posent la même question : ces écrans sont dessinés pour un téléphone tenu
debout, une borne fait 1280 px de large et se lit à un mètre.

Six règles communes, chacune corrigeant un idiome de téléphone qui devient
faux sur une dalle large et fixe : deux colonnes (un ancrage fixe, un flux
qui défile), texte plafonné à ~65 caractères, héros à 26 % au lieu de 52 %,
plus de showModalBottomSheet (il y a la place à côté), carte affichée à sa
taille utile sans « Agrandir », et accents dérivés de
configuration.primaryColor — précédent K4, tablet-app n'a pas de flavor.

Quatre décisions prises le 12/08 :

- Fond clair, aux valeurs réelles de l'app (kBackgroundColor #FFFFFF,
  kBackgroundLight #F3F3F3), pour que la section ne dénote pas.
- Pas de sélecteur de langue : elle est choisie en amont.
- Hors dates, aucune prose : le programme reste, sans pastille EN COURS,
  et les dates du héros disent qu'on est avant ou après.
- Retour automatique après 5 min : adopté mais sorti du périmètre, K8.

Trois états incomplets, tous possibles en configuration. La règle qui les
couvre : le rail média n'existe que s'il a quelque chose à ancrer.

- Sans audio : le dock tombe, l'image reprend la hauteur.
- Sans photo : le rail tombe — une colonne de 38 % avec un seul lecteur
  serait un trou. L'audio passe en tête de la colonne de lecture.
- Texte seul : une colonne centrée, toujours plafonnée. Le vide latéral
  est voulu, c'est lui qui garde le texte lisible.

Quatre vérifications faites dans le code plutôt que supposées :

- Les statistiques sont déjà acquises. section_page_detail émet
  sectionView et sectionLeave avec durée et enveloppe les treize types :
  l'article sera compté dès que son case entre dans le switch.
- tablet-app n'a aucun i18n — pas de l10n/, pas d'AppLocalizations,
  libellés français en dur. C'est ce qui interdit un bandeau « Édition
  passée » et impose d'exprimer l'état hors dates par des dates.
- audioIds est une List<TranslationDTO> : « sans audio » est un état de
  la langue courante, pas de l'article. Un article peut avoir sa version
  française et pas sa néerlandaise.
- isContentTop existe déjà et dit si le texte passe avant le média. En
  paysage il ne veut plus dire « au-dessus » mais « à gauche » : à
  câbler, sinon un réglage déjà offert dans manager-app reste sans effet.

Retiré de la maquette : un repère « 3 / 8 » en pied d'article, inventé
pour occuper la place libérée par le sélecteur de langue. SectionPageDetail
ne reçoit qu'une section et un booléen isFromMenu, jamais la liste de ses
voisines — le repère n'était adossé à aucune donnée.

K8 ajouté au plan et au kanban : le retour à l'accueil s'écrit une seule
fois dans section_page_detail. Bénéfice de bord, il passe par le même
dispose et émet donc un sectionLeave avec sa durée réelle au lieu de
laisser une consultation ouverte jusqu'au visiteur suivant.

Source de vérité dans le repo, artifact pour la diffusion (convention DB0).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Thomas Fransolet 2026-08-12 14:23:39 +02:00
parent f099dbbc5f
commit b5883be088
3 changed files with 997 additions and 3 deletions

View File

@ -0,0 +1,984 @@
<title>Borne en paysage — Article & Événement</title>
<style>
:root {
--ground: #F7F8F9;
--surface: #FFFFFF;
--surface-2: #EDF0F2;
--ink: #16202A;
--ink-2: #4A5966;
--ink-3: #7C8B98;
--line: #DCE2E7;
--line-soft: #E8ECEF;
--accent: #2F4858;
--accent-ink: #FFFFFF;
--accent-soft: #E3EBF0;
--live: #C4622D;
--live-soft: #F6E7DC;
--display: ui-serif, "Iowan Old Style", "Palatino Linotype", Palatino, Georgia, serif;
--body: ui-sans-serif, "Segoe UI", system-ui, -apple-system, sans-serif;
--mono: ui-monospace, "Cascadia Mono", Consolas, "SF Mono", monospace;
--measure: 66ch;
--s1: 0.4rem; --s2: 0.75rem; --s3: 1.15rem;
--s4: 1.9rem; --s5: 3rem; --s6: 4.75rem;
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--ground: #10161C;
--surface: #172029;
--surface-2: #1F2A34;
--ink: #E6ECF1;
--ink-2: #A9B7C2;
--ink-3: #74838F;
--line: #2A3742;
--line-soft: #212D37;
--accent: #7FB0C8;
--accent-ink: #0E141A;
--accent-soft: #1E2C36;
--live: #E08A4F;
--live-soft: #2E2118;
}
}
:root[data-theme="dark"] {
--ground: #10161C;
--surface: #172029;
--surface-2: #1F2A34;
--ink: #E6ECF1;
--ink-2: #A9B7C2;
--ink-3: #74838F;
--line: #2A3742;
--line-soft: #212D37;
--accent: #7FB0C8;
--accent-ink: #0E141A;
--accent-soft: #1E2C36;
--live: #E08A4F;
--live-soft: #2E2118;
}
* { box-sizing: border-box; }
body {
background: var(--ground);
color: var(--ink);
font-family: var(--body);
font-size: 17px;
line-height: 1.65;
margin: 0;
padding: var(--s5) var(--s3) var(--s6);
-webkit-font-smoothing: antialiased;
}
.page { max-width: 68rem; margin: 0 auto; display: flex; flex-direction: column; gap: var(--s6); }
/* ── Header ─────────────────────────────────────────── */
.masthead { display: flex; flex-direction: column; gap: var(--s3); }
.eyebrow {
font-family: var(--mono);
font-size: 0.72rem;
letter-spacing: 0.13em;
text-transform: uppercase;
color: var(--ink-3);
display: flex; flex-wrap: wrap; gap: var(--s2) var(--s3);
}
h1 {
font-family: var(--display);
font-size: clamp(2.1rem, 5.2vw, 3.4rem);
line-height: 1.08;
font-weight: 600;
letter-spacing: -0.015em;
text-wrap: balance;
margin: 0;
}
.standfirst {
font-size: 1.2rem;
line-height: 1.55;
color: var(--ink-2);
max-width: var(--measure);
margin: 0;
text-wrap: pretty;
}
.rule { height: 1px; background: var(--line); border: 0; margin: 0; }
/* ── Sections ───────────────────────────────────────── */
section { display: flex; flex-direction: column; gap: var(--s4); }
h2 {
font-family: var(--display);
font-size: clamp(1.5rem, 3vw, 2rem);
font-weight: 600;
line-height: 1.15;
letter-spacing: -0.01em;
margin: 0;
text-wrap: balance;
}
h3 {
font-size: 1.02rem;
font-weight: 650;
line-height: 1.3;
margin: 0;
letter-spacing: -0.005em;
}
p { margin: 0; max-width: var(--measure); text-wrap: pretty; }
.lede { color: var(--ink-2); }
code {
font-family: var(--mono);
font-size: 0.87em;
background: var(--surface-2);
padding: 0.1em 0.36em;
border-radius: 3px;
color: var(--ink);
}
strong { font-weight: 650; }
/* ── Kiosk mockup frame ─────────────────────────────── */
.kiosk {
background: var(--surface);
border: 1px solid var(--line);
border-radius: 10px;
padding: var(--s2);
box-shadow: 0 1px 2px rgba(22, 32, 42, 0.05);
}
.kiosk-label {
font-family: var(--mono);
font-size: 0.68rem;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--ink-3);
padding: 0 var(--s1) var(--s2);
display: flex; justify-content: space-between; gap: var(--s2);
}
.screen {
aspect-ratio: 16 / 10;
background: var(--surface-2);
border-radius: 4px;
overflow: hidden;
container-type: inline-size;
display: flex; flex-direction: column;
}
/* mockup internals scale with the frame width */
/* La dalle est peinte en clair en dur, quel que soit le thème du lecteur :
tablet-app est clair (kBackgroundColor #FFFFFF, kBackgroundLight #F3F3F3)
et une maquette qui bascule avec le thème du lecteur ne montre plus l'app. */
.screen {
--u: 1cqw;
--surface: #FFFFFF;
--surface-2: #F3F3F3;
--ink: #16202A;
--ink-2: #4A5966;
--ink-3: #7C8B98;
--line: #DCE2E7;
--line-soft: #E8ECEF;
--accent: #2F4858;
--accent-ink: #FFFFFF;
--accent-soft: #E3EBF0;
--live: #C4622D;
--live-soft: #F6E7DC;
background: var(--surface-2);
color: var(--ink);
}
.screen, .screen * { font-size: calc(1.35 * var(--u)); line-height: 1.4; }
.zone {
position: relative;
background: var(--surface);
border: 1px dashed var(--line);
}
.ztag {
position: absolute; top: calc(0.6 * var(--u)); left: calc(0.6 * var(--u));
font-family: var(--mono);
font-size: calc(1.15 * var(--u));
font-weight: 700;
color: var(--accent-ink);
background: var(--accent);
width: calc(2.4 * var(--u)); height: calc(2.4 * var(--u));
display: grid; place-items: center;
border-radius: 2px;
z-index: 3;
}
/* generic mockup atoms */
.bar { background: var(--ink-3); opacity: 0.28; border-radius: 1px; height: calc(1.1 * var(--u)); }
.bar.t { opacity: 0.5; }
.stack { display: flex; flex-direction: column; gap: calc(0.9 * var(--u)); }
.imgblock {
background: linear-gradient(135deg, var(--accent-soft), var(--surface-2));
border: 1px solid var(--line-soft);
display: grid; place-items: center;
color: var(--ink-3);
font-family: var(--mono);
font-size: calc(1.1 * var(--u));
}
/* ── Article mockup ─────────────────────────────────── */
.m-article { display: grid; grid-template-columns: 38fr 62fr; height: 100%; }
.m-media { padding: calc(2 * var(--u)); display: flex; flex-direction: column; gap: calc(1.4 * var(--u)); }
.m-media .imgblock { flex: 1; border-radius: calc(0.6 * var(--u)); }
.m-thumbs { display: grid; grid-template-columns: repeat(4, 1fr); gap: calc(0.8 * var(--u)); height: calc(7 * var(--u)); }
.m-thumbs .imgblock { border-radius: calc(0.4 * var(--u)); font-size: calc(0.9 * var(--u)); }
.m-audio {
background: var(--accent);
color: var(--accent-ink);
border-radius: calc(0.6 * var(--u));
padding: calc(1.2 * var(--u)) calc(1.4 * var(--u));
display: flex; align-items: center; gap: calc(1.2 * var(--u));
height: calc(8.5 * var(--u));
}
.m-audio .play {
width: calc(5 * var(--u)); height: calc(5 * var(--u));
border-radius: 50%;
background: var(--accent-ink); opacity: 0.9;
display: grid; place-items: center;
color: var(--accent);
font-size: calc(1.8 * var(--u));
flex: none;
}
.m-audio .track { flex: 1; height: calc(0.9 * var(--u)); background: currentColor; opacity: 0.35; border-radius: 1px; }
.m-text { padding: calc(2.4 * var(--u)) calc(2.4 * var(--u)) 0; display: flex; flex-direction: column; overflow: hidden; }
.m-title { font-family: var(--display); font-size: calc(3.1 * var(--u)) !important; font-weight: 600; line-height: 1.1; color: var(--ink); }
.m-kicker { font-family: var(--mono); font-size: calc(1 * var(--u)) !important; letter-spacing: 0.1em; text-transform: uppercase; color: var(--accent); }
.m-measure { max-width: calc(52 * var(--u)); }
.m-footer {
border-top: 1px solid var(--line);
padding: calc(1.2 * var(--u)) calc(2.4 * var(--u));
display: flex; align-items: center; justify-content: flex-end;
background: var(--surface-2);
height: calc(8 * var(--u));
flex: none;
}
.btn-back {
background: var(--accent); color: var(--accent-ink);
border-radius: calc(0.4 * var(--u));
padding: calc(0.8 * var(--u)) calc(1.8 * var(--u));
font-size: calc(1.3 * var(--u)) !important;
font-weight: 600;
}
/* ── Event mockup ───────────────────────────────────── */
.m-event { display: flex; flex-direction: column; height: 100%; }
.m-hero {
height: 26%;
background: linear-gradient(135deg, var(--accent), var(--accent-soft));
padding: calc(2 * var(--u)) calc(2.4 * var(--u));
display: flex; align-items: flex-end; justify-content: space-between; gap: calc(2 * var(--u));
flex: none;
position: relative;
}
.m-hero-title { font-family: var(--display); font-size: calc(3 * var(--u)) !important; font-weight: 600; color: #fff; line-height: 1.1; }
.m-hero-date {
background: rgba(255,255,255,0.9); color: var(--accent);
border-radius: calc(2 * var(--u));
padding: calc(0.6 * var(--u)) calc(1.4 * var(--u));
font-size: calc(1.2 * var(--u)) !important;
font-weight: 600;
white-space: nowrap;
}
.m-body { display: grid; grid-template-columns: 44fr 56fr; flex: 1; min-height: 0; }
.m-prog { padding: calc(1.8 * var(--u)); display: flex; flex-direction: column; gap: calc(1 * var(--u)); overflow: hidden; }
.m-sechead {
font-family: var(--mono); font-size: calc(1.05 * var(--u)) !important;
letter-spacing: 0.1em; text-transform: uppercase; color: var(--ink-3);
}
.m-block {
border: 1px solid var(--line);
border-radius: calc(0.5 * var(--u));
padding: calc(1.1 * var(--u)) calc(1.3 * var(--u));
display: flex; align-items: center; gap: calc(1.2 * var(--u));
background: var(--surface);
}
.m-block .time { font-family: var(--mono); font-size: calc(1.15 * var(--u)) !important; color: var(--ink-3); flex: none; font-variant-numeric: tabular-nums; }
.m-block .name { flex: 1; color: var(--ink-2); font-size: calc(1.3 * var(--u)) !important; }
.m-block.live { background: var(--live-soft); border-color: var(--live); }
.m-block.live .name { color: var(--ink); font-weight: 650; }
.m-block.live .time { color: var(--live); font-weight: 700; }
.pill-live {
background: var(--live); color: #fff;
border-radius: calc(2 * var(--u));
padding: calc(0.35 * var(--u)) calc(1 * var(--u));
font-size: calc(1 * var(--u)) !important;
font-weight: 700; letter-spacing: 0.04em;
flex: none;
}
.m-map { position: relative; border-left: 1px solid var(--line); background: var(--surface-2); overflow: hidden; }
.m-map-grid {
position: absolute; inset: 0;
background-image:
linear-gradient(var(--line-soft) 1px, transparent 1px),
linear-gradient(90deg, var(--line-soft) 1px, transparent 1px);
background-size: calc(6 * var(--u)) calc(6 * var(--u));
}
.pin {
position: absolute;
width: calc(2.2 * var(--u)); height: calc(2.2 * var(--u));
border-radius: 50% 50% 50% 0;
transform: rotate(-45deg);
background: var(--accent);
border: 1px solid var(--surface);
}
.pin.live { background: var(--live); width: calc(2.8 * var(--u)); height: calc(2.8 * var(--u)); }
.m-map-cap {
position: absolute; left: calc(1.4 * var(--u)); bottom: calc(1.4 * var(--u));
background: var(--surface); border: 1px solid var(--line);
border-radius: calc(0.4 * var(--u));
padding: calc(0.6 * var(--u)) calc(1.1 * var(--u));
font-family: var(--mono); font-size: calc(1 * var(--u)) !important;
color: var(--ink-2);
}
/* ── Annotations ────────────────────────────────────── */
.notes { display: grid; grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr)); gap: var(--s3); }
/* ── Variants (états incomplets) ────────────────────── */
.variants { display: grid; grid-template-columns: repeat(auto-fit, minmax(17rem, 1fr)); gap: var(--s3); }
.variant { display: flex; flex-direction: column; gap: var(--s2); }
.variant .kiosk { padding: calc(var(--s1) * 1.2); }
.variant h3 { font-size: 0.94rem; }
.variant p { font-size: 0.9rem; color: var(--ink-2); max-width: none; }
.variant .cond {
font-family: var(--mono); font-size: 0.72rem; color: var(--ink-3);
display: block; margin-top: var(--s1);
}
.note {
display: grid;
grid-template-columns: 1.6rem 1fr;
gap: var(--s2);
align-items: start;
}
.note .key {
font-family: var(--mono); font-size: 0.72rem; font-weight: 700;
color: var(--accent-ink); background: var(--accent);
width: 1.6rem; height: 1.6rem;
display: grid; place-items: center; border-radius: 2px;
margin-top: 0.12rem;
}
.note p { font-size: 0.94rem; line-height: 1.55; color: var(--ink-2); max-width: none; }
.note strong { color: var(--ink); }
/* ── Rules list ─────────────────────────────────────── */
.rules { display: flex; flex-direction: column; gap: 0; border-top: 1px solid var(--line); }
.ruleitem {
display: grid;
grid-template-columns: minmax(9rem, 15rem) 1fr;
gap: var(--s2) var(--s4);
padding: var(--s3) 0;
border-bottom: 1px solid var(--line);
align-items: start;
}
.ruleitem h3 { padding-top: 0.1rem; }
.ruleitem p { font-size: 0.96rem; color: var(--ink-2); }
.ruleitem .before {
display: block;
font-family: var(--mono);
font-size: 0.75rem;
color: var(--ink-3);
margin-top: var(--s1);
}
/* ── Callout ────────────────────────────────────────── */
.callout {
background: var(--surface);
border: 1px solid var(--line);
border-left: 3px solid var(--accent);
border-radius: 0 6px 6px 0;
padding: var(--s3);
display: flex; flex-direction: column; gap: var(--s2);
}
.callout h3 { font-size: 0.96rem; }
.callout p { font-size: 0.94rem; color: var(--ink-2); }
footer {
font-family: var(--mono); font-size: 0.72rem;
color: var(--ink-3); letter-spacing: 0.04em;
border-top: 1px solid var(--line);
padding-top: var(--s3);
display: flex; flex-wrap: wrap; gap: var(--s2) var(--s4);
}
@media (max-width: 44rem) {
.ruleitem { grid-template-columns: 1fr; }
body { font-size: 16px; }
}
</style>
<div class="page">
<header class="masthead">
<div class="eyebrow">
<span>tablet-app · lot K</span>
<span>K3 — SectionEvent</span>
<span>K7 — SectionArticle</span>
<span>12 août 2026</span>
</div>
<h1>Deux écrans à faire passer du téléphone debout à la borne couchée</h1>
<p class="standfirst">
Le code se recopie depuis <code>mymuseum-visitapp</code> — ce n'est pas là qu'est le travail.
Ces écrans sont dessinés pour un téléphone tenu à la verticale, à trente centimètres des yeux.
Une borne fait 1280 px de large, elle est fixe, et on la lit debout à un mètre.
Cette page arrête la mise en page des deux écrans <em>ensemble</em>, parce que les trancher
séparément donnerait deux grammaires différentes sur la même dalle.
<strong>Validée le 12 août</strong> — les quatre décisions de fin de page sont prises.
</p>
</header>
<hr class="rule">
<!-- ── RÈGLES COMMUNES ────────────────────────────── -->
<section>
<h2>Ce qui change, pour les deux</h2>
<p class="lede">
Six décisions valables sur toute la borne. Elles ne sortent pas d'une préférence :
chacune corrige un idiome de téléphone qui devient faux sur une dalle large et fixe.
</p>
<div class="rules">
<div class="ruleitem">
<h3>Deux colonnes, jamais une</h3>
<p>
La largeur est la ressource qu'on gagne, et une colonne unique la gaspille intégralement.
Chaque écran se découpe en un <strong>ancrage</strong> qui ne bouge pas et un
<strong>flux</strong> qui défile. Sur l'article, le média ancre et le texte défile ; sur
l'événement, la carte ancre et le programme défile.
<span class="before">mobile — une colonne, tout défile ensemble</span>
</p>
</div>
<div class="ruleitem">
<h3>Le texte reste étroit</h3>
<p>
Élargir la colonne de texte à 1280 px la rend illisible : l'œil perd la ligne au retour.
Le corps de l'article est plafonné autour de <strong>65 caractères</strong> et le reste de
la largeur va au média, pas au texte. C'est la règle qui justifie à elle seule les deux colonnes.
</p>
</div>
<div class="ruleitem">
<h3>Le héros est une bande, pas un écran</h3>
<p>
Sur mobile, l'événement ouvre sur une image qui occupe <strong>52 % de la hauteur</strong>,
et le parallaxe récompense le défilement. Debout devant une borne, personne ne défile pour
découvrir qu'il y a un programme dessous. Le héros tombe à <strong>26 %</strong> : il situe,
il n'accueille pas.
<span class="before">mobile — SliverAppBar, expandedHeight = 0.52 × hauteur</span>
</p>
</div>
<div class="ruleitem">
<h3>Pas de feuille modale</h3>
<p>
<code>showModalBottomSheet</code> est une réponse à l'étroitesse : sur un téléphone il n'y a
pas de place à côté, alors on empile. Sur une borne il y a la place à côté. Le détail d'un
bloc de programme s'ouvre <strong>dans la colonne de droite</strong>, sous la carte, sans
masquer ce qu'on regardait.
<span class="before">mobile — _showBlockDetail() en bottom sheet</span>
</p>
</div>
<div class="ruleitem">
<h3>Rien à agrandir</h3>
<p>
La carte de l'événement est un aperçu de 220 px qu'il faut toucher pour ouvrir une page
pleine. Sur une borne d'accueil, la carte <em>est</em> la raison pour laquelle on s'arrête :
elle est affichée à sa taille utile d'emblée, et <code>EventMapFullPage</code> ne se porte pas.
<span class="before">mobile — aperçu 220 px + bouton « Agrandir »</span>
</p>
</div>
<div class="ruleitem">
<h3>Les couleurs viennent de la configuration</h3>
<p>
<code>mymuseum-visitapp</code> code ses couleurs en dur par flavor client — rouge MDLF, bleu
Fort. <code>tablet-app</code> n'a pas de flavor : un seul APK sert tous les clients et la
charte arrive avec la configuration choisie au pincode. Tout accent dérive de
<code>configuration.primaryColor</code>, <strong>comme l'écran Game livré en K4</strong>.
Le bleu ardoise de cette page est le repli <code>kTestSecondColor</code>, pas une proposition
de charte.
</p>
</div>
</div>
</section>
<!-- ── ARTICLE ────────────────────────────────────── -->
<section>
<h2>Article — le média ancre, le texte défile</h2>
<p class="lede">
Un article porte un corps HTML, souvent un audio, et parfois une série d'images.
Sur mobile les trois s'empilent et l'audio finit en lecteur flottant par-dessus le texte.
En paysage, chacun a sa place et le lecteur audio n'a plus besoin de flotter.
</p>
<div class="kiosk">
<div class="kiosk-label">
<span>Article — 1280 × 800</span>
<span>MDLF · « Ruche 7 »</span>
</div>
<div class="screen">
<div class="m-article">
<div class="zone m-media">
<span class="ztag">A</span>
<div class="imgblock">image principale</div>
<div class="m-thumbs">
<div class="imgblock">2</div>
<div class="imgblock">3</div>
<div class="imgblock">4</div>
<div class="imgblock">5</div>
</div>
<div class="m-audio">
<span class="play"></span>
<span class="track"></span>
<span style="font-family: var(--mono); font-variant-numeric: tabular-nums;">3:12</span>
</div>
</div>
<div style="display: flex; flex-direction: column; min-width: 0;">
<div class="zone m-text" style="flex: 1;">
<span class="ztag">B</span>
<div class="stack m-measure" style="margin-top: calc(2.6 * var(--u));">
<div class="m-kicker">Rucher pédagogique</div>
<div class="m-title">Ruche 7</div>
<div class="stack" style="margin-top: calc(1.4 * var(--u));">
<div class="bar" style="width: 100%;"></div>
<div class="bar" style="width: 97%;"></div>
<div class="bar" style="width: 99%;"></div>
<div class="bar" style="width: 62%;"></div>
<div class="bar t" style="width: 44%; margin-top: calc(1.2 * var(--u));"></div>
<div class="bar" style="width: 98%;"></div>
<div class="bar" style="width: 95%;"></div>
<div class="bar" style="width: 71%;"></div>
</div>
</div>
</div>
<div class="zone m-footer" style="border-style: dashed dashed none none;">
<span class="ztag" style="top: calc(-1.4 * var(--u));">C</span>
<span class="btn-back">Retour</span>
</div>
</div>
</div>
</div>
</div>
<div class="notes">
<div class="note">
<span class="key">A</span>
<p>
<strong>Rail média, 38 %.</strong> L'image principale prend la hauteur disponible, les
autres ressources deviennent une bande de vignettes — c'est <code>SliderImages</code> à plat
plutôt qu'en carrousel, parce qu'une borne n'a pas de raison de cacher quatre images
derrière un geste. <strong>L'audio est docké en bas du rail</strong> : sur mobile,
<code>audio_player_floating</code> flotte au-dessus du texte faute de place. Ici il a une
place, donc il ne flotte plus.
</p>
</div>
<div class="note">
<span class="key">B</span>
<p>
<strong>Colonne de lecture, 62 %, texte plafonné.</strong> Le rendu HTML
(<code>flutter_widget_from_html</code>) se recopie tel quel ; ce qui change est le
<code>maxWidth</code> qui l'entoure. Le titre passe en display, avec l'étiquette de section
au-dessus — sur une borne, un visiteur arrive au milieu d'un parcours et doit savoir
<em></em> il est avant <em>quoi</em> il lit.
</p>
</div>
<div class="note">
<span class="key">C</span>
<p>
<strong>Barre de pied, hauteur de doigt.</strong> Un seul bouton, cible à
<strong>56 px minimum</strong> : on tape debout, parfois avec un enfant au bout du bras.
<strong>Pas de sélecteur de langue</strong> — elle est choisie en amont, la réafficher ici
rouvrirait une décision déjà prise. Et rien d'autre : <code>SectionPageDetail</code> ne
reçoit qu'<em>une</em> section et un booléen <code>isFromMenu</code>, jamais la liste de ses
voisines. Un repère du type « 3 / 8 » n'est donc adossé à aucune donnée — il faudrait
descendre la liste jusqu'ici pour le calculer.
</p>
</div>
</div>
<h2 style="font-size: 1.25rem; margin-top: var(--s3);">Quand il manque l'audio, les photos, ou les deux</h2>
<p class="lede">
Les trois cas sont possibles en configuration, et le rail média de 38 % ne survit pas tel quel
à la disparition de son contenu. La règle qui les couvre tous :
<strong>le rail n'existe que s'il a quelque chose à ancrer</strong>. Sinon la colonne de lecture
se centre — ce qui ne contredit pas la règle des deux colonnes, dont l'objet est d'empêcher une
ligne de texte de s'étirer sur 1280 px, pas d'imposer une colonne vide.
</p>
<div class="variants">
<div class="variant">
<div class="kiosk">
<div class="screen">
<div class="m-article">
<div class="zone m-media">
<div class="imgblock">image principale</div>
<div class="m-thumbs">
<div class="imgblock">2</div>
<div class="imgblock">3</div>
<div class="imgblock">4</div>
<div class="imgblock">5</div>
</div>
</div>
<div style="display: flex; flex-direction: column; min-width: 0;">
<div class="zone m-text" style="flex: 1;">
<div class="stack m-measure" style="margin-top: calc(2.6 * var(--u));">
<div class="m-kicker">Rucher pédagogique</div>
<div class="m-title">Ruche 7</div>
<div class="stack" style="margin-top: calc(1.4 * var(--u));">
<div class="bar" style="width: 100%;"></div>
<div class="bar" style="width: 96%;"></div>
<div class="bar" style="width: 99%;"></div>
<div class="bar" style="width: 58%;"></div>
<div class="bar t" style="width: 44%; margin-top: calc(1.2 * var(--u));"></div>
<div class="bar" style="width: 97%;"></div>
<div class="bar" style="width: 74%;"></div>
</div>
</div>
</div>
<div class="zone m-footer" style="border-style: dashed none none none;">
<span class="btn-back">Retour</span>
</div>
</div>
</div>
</div>
</div>
<h3>Sans audio</h3>
<p>
Le dock disparaît, l'image reprend la hauteur libérée. Aucune autre reprise —
c'est le cas le plus simple des trois.
<span class="cond">audioIds vide pour la langue courante</span>
</p>
</div>
<div class="variant">
<div class="kiosk">
<div class="screen">
<div style="display: flex; flex-direction: column; height: 100%;">
<div class="zone m-text" style="flex: 1; align-items: center;">
<div class="stack m-measure" style="margin-top: calc(3.2 * var(--u)); width: calc(52 * var(--u));">
<div class="m-kicker">Rucher pédagogique</div>
<div class="m-title">Ruche 7</div>
<div class="m-audio" style="margin-top: calc(1.6 * var(--u)); height: calc(7.5 * var(--u));">
<span class="play"></span>
<span class="track"></span>
<span style="font-family: var(--mono);">3:12</span>
</div>
<div class="stack" style="margin-top: calc(1.6 * var(--u));">
<div class="bar" style="width: 100%;"></div>
<div class="bar" style="width: 97%;"></div>
<div class="bar" style="width: 99%;"></div>
<div class="bar" style="width: 63%;"></div>
<div class="bar t" style="width: 44%; margin-top: calc(1.2 * var(--u));"></div>
<div class="bar" style="width: 96%;"></div>
<div class="bar" style="width: 81%;"></div>
</div>
</div>
</div>
<div class="zone m-footer" style="border-style: dashed none none none;">
<span class="btn-back">Retour</span>
</div>
</div>
</div>
</div>
<h3>Sans photo, avec audio</h3>
<p>
Le rail tombe — une colonne de 38 % contenant un seul lecteur serait un trou, pas une mise
en page. L'audio passe <strong>en tête de la colonne de lecture</strong>, à la largeur du
texte, juste sous le titre : c'est là qu'on le cherche quand il est la pièce principale.
<span class="cond">imageSource nul et contents vide</span>
</p>
</div>
<div class="variant">
<div class="kiosk">
<div class="screen">
<div style="display: flex; flex-direction: column; height: 100%;">
<div class="zone m-text" style="flex: 1; align-items: center;">
<div class="stack m-measure" style="margin-top: calc(4.5 * var(--u)); width: calc(52 * var(--u));">
<div class="m-kicker">Rucher pédagogique</div>
<div class="m-title">Ruche 7</div>
<div class="stack" style="margin-top: calc(1.8 * var(--u));">
<div class="bar" style="width: 100%;"></div>
<div class="bar" style="width: 98%;"></div>
<div class="bar" style="width: 96%;"></div>
<div class="bar" style="width: 99%;"></div>
<div class="bar" style="width: 57%;"></div>
<div class="bar t" style="width: 44%; margin-top: calc(1.4 * var(--u));"></div>
<div class="bar" style="width: 97%;"></div>
<div class="bar" style="width: 99%;"></div>
<div class="bar" style="width: 68%;"></div>
</div>
</div>
</div>
<div class="zone m-footer" style="border-style: dashed none none none;">
<span class="btn-back">Retour</span>
</div>
</div>
</div>
</div>
<h3>Texte seul</h3>
<p>
Une seule colonne, centrée, toujours plafonnée à 65 caractères. Le vide de part et d'autre
est <strong>voulu</strong> : c'est ce qui garde le texte lisible. La tentation d'étirer la
ligne pour « remplir » est précisément ce que la règle interdit.
<span class="cond">ni image, ni contents, ni audio</span>
</p>
</div>
</div>
<div class="callout">
<h3>Deux choses vérifiées dans <code>ArticleDTO</code>, pas supposées</h3>
<p>
<strong><code>audioIds</code> est une <code>List&lt;TranslationDTO&gt;</code>, pas un id.</strong>
L'audio est donc <em>par langue</em> : un article peut avoir sa version française et pas sa
néerlandaise. « Sans audio » n'est pas un état de l'article, c'est un état de
<strong>l'article dans la langue courante</strong> — et la borne doit basculer sur la variante
du milieu pour ce visiteur-là seulement.
</p>
<p>
<strong><code>isContentTop</code> existe déjà</strong> et dit si le texte passe avant le média.
En portrait, « avant » veut dire au-dessus. En paysage, il n'y a plus de dessus : le drapeau
devient l'ordre des deux colonnes — média à gauche, ou texte à gauche. À câbler, pas à ignorer,
sinon un réglage déjà offert dans <code>manager-app</code> devient sans effet sur la borne.
</p>
</div>
<div class="callout">
<h3>Les statistiques sont déjà là — vérifié dans le code</h3>
<p>
Rien à câbler. <code>section_page_detail.dart</code> émet <code>sectionView</code> à
l'ouverture et <code>sectionLeave</code> avec la durée au <code>dispose</code>, et il
<strong>enveloppe les treize types</strong> : l'article sera compté dès que son <code>case</code>
entre dans le <code>switch</code>. <code>statisticsService</code> est câblé à sept endroits de
l'app. L'événement <code>articleRead</code> de mymuseum, plus spécifique, n'existe pas ici —
c'est un ajout facultatif, pas un manque.
</p>
<p>
En revanche, le <strong>marquage « déjà lu » par visiteur</strong>
(<code>articleRead</code> en base locale, <code>DatabaseHelper</code>) ne se recopie pas : sur
un appareil partagé, le lecteur suivant n'est pas le précédent.
</p>
</div>
</section>
<!-- ── ÉVÉNEMENT ──────────────────────────────────── -->
<section>
<h2>Événement — la carte ancre, le programme défile</h2>
<p class="lede">
C'est l'écran du cas E du test-plan : la borne d'accueil d'un carnaval ou d'un festival, qui
répond à deux questions et deux seulement — <em>qu'est-ce qui se passe maintenant</em> et
<em>où est-ce que ça se passe</em>. La mise en page les met côte à côte, sans geste entre elles.
</p>
<div class="kiosk">
<div class="kiosk-label">
<span>Événement — 1280 × 800</span>
<span>Cas E · carnaval</span>
</div>
<div class="screen">
<div class="m-event">
<div class="zone m-hero" style="border-style: none none dashed none;">
<span class="ztag">D</span>
<div class="m-hero-title">Carnaval des Légendes</div>
<span class="m-hero-date">14/03/2026 → 16/03/2026</span>
</div>
<div class="m-body">
<div class="zone m-prog">
<span class="ztag">E</span>
<div class="m-sechead" style="margin-top: calc(2.4 * var(--u));">Programme du jour</div>
<div class="m-block">
<span class="time">10:00</span>
<span class="name">Ouverture des portes</span>
</div>
<div class="m-block live">
<span class="time">14:00</span>
<span class="name">Grand cortège — départ place du Marché</span>
<span class="pill-live">EN COURS</span>
</div>
<div class="m-block">
<span class="time">16:30</span>
<span class="name">Remise des prix des chars</span>
</div>
<div class="m-block">
<span class="time">18:00</span>
<span class="name">Brûlage du Bonhomme Hiver</span>
</div>
<div class="m-block">
<span class="time">20:00</span>
<span class="name">Feu d'artifice</span>
</div>
</div>
<div class="zone m-map">
<span class="ztag">F</span>
<div class="m-map-grid"></div>
<div class="pin live" style="left: 44%; top: 46%;"></div>
<div class="pin" style="left: 22%; top: 28%;"></div>
<div class="pin" style="left: 68%; top: 34%;"></div>
<div class="pin" style="left: 58%; top: 66%;"></div>
<div class="pin" style="left: 30%; top: 72%;"></div>
<span class="m-map-cap">5 points · le point orange suit le bloc en cours</span>
</div>
</div>
</div>
</div>
</div>
<div class="notes">
<div class="note">
<span class="key">D</span>
<p>
<strong>Bande héros, 26 %.</strong> Assez pour porter l'image, le titre et les dates ;
pas assez pour repousser le programme sous la ligne de flottaison. Le dégradé de repli
dérive de <code>primaryColor</code> — c'est exactement le calcul déjà écrit dans l'écran
Game de K4, à réutiliser plutôt qu'à refaire.
</p>
</div>
<div class="note">
<span class="key">E</span>
<p>
<strong>Programme, 44 %.</strong> La frise verticale à pastilles et trait de liaison de
mymuseum tombe : elle coûte 56 px de gouttière pour exprimer une chronologie que l'heure
en tête de ligne dit déjà. Le bloc actif garde son fond plein et son pastillage
<strong>« EN COURS »</strong> — c'est l'information la plus demandée de l'écran, elle doit
se voir à trois mètres.
</p>
</div>
<div class="note">
<span class="key">F</span>
<p>
<strong>Carte, 56 %, toujours vive.</strong> Les annotations globales sont dans l'accent,
celles du bloc en cours dans l'orange sémantique — la même paire de couleurs que le
programme, pour que l'œil relie une ligne à un point sans légende. Défilement et zoom
restent actifs ; <code>InteractiveFlag.none</code> était une contrainte d'aperçu.
</p>
</div>
</div>
<div class="callout">
<h3>Le piège à ne pas déplacer</h3>
<p>
Les coordonnées de <code>MapAnnotationDTO.geometry.coordinates</code> sont en
<strong><code>[lng, lat]</code></strong> — GeoJSON. Celles de <code>GuidedStep</code> sont en
<code>[lat, lng]</code>. Les deux ordres sont en production, et une confusion
<strong>ne lève aucune erreur</strong> : elle déplace les points sur une carte qui reste
plausible. La lecture de mymuseum (<code>coords[1]</code> = lat, <code>coords[0]</code> = lng)
se recopie telle quelle, sans passer par un helper venu d'un autre contexte. Le volet parcours
étant exclu de la borne, cet écran ne voit qu'une seule des deux conventions — c'est une raison
de plus de ne pas y introduire l'autre.
</p>
</div>
</section>
<!-- ── QUESTIONS ──────────────────────────────────── -->
<section>
<h2>Tranché le 12 août</h2>
<div class="rules">
<div class="ruleitem">
<h3>Fond clair</h3>
<p>
La nouvelle section ne doit pas dénoter dans l'app. <code>tablet-app</code> est clair —
<code>kBackgroundColor</code> vaut <code>#FFFFFF</code> et <code>kBackgroundLight</code>
<code>#F3F3F3</code> — et les maquettes ci-dessus emploient <strong>ces valeurs-là</strong>,
pas une palette inventée. Le <code>#111111</code> en dur de l'écran Événement de mymuseum ne
se recopie donc pas : il vient d'un téléphone consulté en extérieur.
<span class="before">mymuseum — Scaffold backgroundColor: Color(0xFF111111)</span>
</p>
</div>
<div class="ruleitem">
<h3>Hors dates, aucune prose</h3>
<p>
Le carnaval dure trois jours, la borne tourne les 362 autres. Le programme reste affiché,
<strong>sans pastille « EN COURS »</strong> et avec les blocs en gris — le surlignage est
calculé sur <code>DateTime.now()</code>, il s'éteint tout seul. La bande héros continue de
porter les dates, qui sont <strong>numériques donc sans traduction</strong>, et suffisent à
dire qu'on est avant ou après.
<span class="before">état passé = les mêmes blocs, désaturés — pas un écran différent</span>
</p>
</div>
<div class="ruleitem">
<h3>Et c'est l'i18n qui tranche</h3>
<p>
Ton réflexe sur les traductions était le bon, et il est plus contraignant que prévu :
<strong><code>tablet-app</code> n'a aucun système d'i18n</strong> — pas de <code>l10n/</code>,
pas d'<code>AppLocalizations</code>, les libellés sont en français en dur
(<code>"Ce type n'est pas supporté"</code>). Un bandeau « Édition passée » serait donc
<strong>français pour un visiteur néerlandophone</strong>. Le vrai libellé viendrait soit
d'un champ backend traduit — qui n'existe pas — soit d'un i18n à créer, ce qui n'est pas un
chantier de K3. D'où la règle ci-dessus : on affiche des dates, pas des phrases.
</p>
</div>
<div class="ruleitem">
<h3>Retour à l'accueil après 5 min</h3>
<p>
Adopté, et <strong>écrit une seule fois pour les treize types</strong> :
<code>section_page_detail</code> enveloppe déjà toutes les sections et porte déjà les crochets
<code>initState</code> / <code>dispose</code> des statistiques. Le minuteur d'inactivité va
là, pas dans les deux écrans de cette page. Bénéfice de bord : un retour automatique passe
par le même <code>dispose</code>, donc il émet un <code>sectionLeave</code> avec sa durée
réelle — la statistique reste juste au lieu de compter une consultation ouverte jusqu'au
prochain visiteur.
<span class="before">hors périmètre K3/K7 — à ouvrir comme chantier propre</span>
</p>
</div>
</div>
</section>
<footer>
<span>DOCS/claude design/kiosk-paysage-article-event.html</span>
<span>source de vérité — l'artifact ne sert qu'à la diffusion (DB0)</span>
</footer>
</div>

View File

@ -458,7 +458,7 @@
<div class="stat"><span class="n n-info">2</span><span class="k">Migration v3</span></div>
<div class="stat"><span class="n n-warn">5</span><span class="k">Bugs ouverts</span></div>
<div class="stat"><span class="n">6</span><span class="k">À tester</span></div>
<div class="stat"><span class="n">20</span><span class="k">Planifié</span></div>
<div class="stat"><span class="n">21</span><span class="k">Planifié</span></div>
<div class="stat"><span class="n n-gate">8</span><span class="k">Bascule prod</span></div>
<div class="stat"><span class="n n-good">40</span><span class="k">Fait récemment</span></div>
</section>
@ -626,7 +626,16 @@
<!-- PLANIFIÉ -->
<section class="col" style="--stripe: var(--ink-3)">
<div class="col-head"><h2>Planifié</h2><span class="count">20</span></div>
<div class="col-head"><h2>Planifié</h2><span class="count">21</span></div>
<article class="card" data-area="manager" data-horizon="v1">
<div class="card-meta"><span class="tag">tablet-app</span><span class="flag f-warn">Lot K · K8 — nouveau 12/08</span></div>
<h3>Retour automatique à l'accueil après inactivité</h3>
<p>Un visiteur part sans fermer l'écran ; le suivant trouve l'article du précédent. Délai proposé : <strong>5 minutes</strong>. Sorti de la maquette K3/K7 parce que ce n'est ni l'un ni l'autre — c'est un comportement de borne qui vaut pour <strong>les treize types</strong>.</p>
<p><strong>À écrire une seule fois</strong>, dans <code>section_page_detail</code> : il enveloppe déjà toutes les sections et porte déjà les crochets <code>initState</code>/<code>dispose</code> des statistiques. <strong>Bénéfice de bord</strong> : le retour passant par le même <code>dispose</code>, il émet un <code>sectionLeave</code> avec sa <em>durée réelle</em> au lieu de laisser une consultation ouverte jusqu'au prochain visiteur — la statistique devient juste, en plus de l'écran.</p>
<span class="src">v1-plan.md lot K — K8 · maquette kiosk-paysage</span>
</article>
<div class="stack">
<article class="card" data-area="commercial" data-horizon="v1">

View File

@ -284,7 +284,8 @@ Ordre du §2 de STATUS.md, corrigé par **L12**.
| ~~**K4**~~**2026-08-12** | **Écran Game repris de mymuseum.** Les 6 fichiers de `Screens/Sections/Game/` sont dans `tablet-app/lib/Screens/Game/`, contexte adapté ; `lib/Screens/Puzzle/` supprimé ; `main_view` bascule sur `SectionType.Game` / `GameDTO` / `GamePage`. **Gains** : le puzzle glissant (mélange par mouvements valides, donc toujours soluble), le bouton d'indice, et un dimensionnement qui respecte le ratio de l'image — 508 lignes contre 237 à l'ancien `puzzle_view`.<br>⚠️ **Une décision de rendu à valider à l'œil** : mymuseum code ses couleurs **en dur par flavor client** (`kMainColor0/1/2` — rouge MDLF, bleu Fort). `tablet-app` n'a pas de flavor : un seul APK sert tous les clients et la charte vient de la configuration choisie au pincode. Recopier ces constantes aurait affiché du bleu chez MDLF. Le dégradé et le bouton d'indice sont donc **dérivés de `configuration.primaryColor`** (même parsing que `audio_player`/`loading_common`, repli `kTestSecondColor`, 3 nuances par `Color.lerp`). Plus juste sur le fond, mais **le rendu diffère de la version mobile** : si le dégradé dessiné à la main est préféré, figer les trois couleurs |
| ~~K4 — énoncé d'origine~~ | **`SectionPuzzle``SectionGame` + le type glissant.** Le serveur a renommé et porte `GameTypes { Puzzle, SlidingPuzzle }` ; `tablet-app` a encore un écran `Puzzle/` qui ne connaît que l'ancien. **Reprendre le code de `mymuseum-visitapp/lib/Screens/Sections/Game/`**`game_page.dart` et `sliding_puzzle_piece.dart` existent déjà là-bas, et cette version est **moins buguée** que celle de tablet-app | Récupération, pas réécriture |
| ~~**K5**~~**2026-08-12** | **Les trois flavors de `mymuseum-visitapp` construisent** (`dev`, `mdlf`, `fortsaintheribert`), exit 0, APK à l'appui. **D0 est débloqué.**<br>**L'énoncé était faux : il n'y avait pas de « même rattrapage » à faire.** Sa config Android était **déjà à niveau** — Gradle 8.11.1, AGP 8.9.0, `-Xmx4096M`, `enableJetifier=false`, aucun forçage d'`androidx.lifecycle`. Les dix crans de K1 ont amené **`tablet-app` jusqu'à `mymuseum`** ; le lot supposait la symétrie du problème, elle n'existait pas. Le réflexe du diff annoncé en K1 a donné la réponse en deux `cat`.<br>**Ce qui restait, et qui est fait** : les deux avertissements du build. NDK **27.0.12077973 → 28.2.13676358** (`speech_to_text` le réclame nommément) et Kotlin **2.1.0 → 2.3.10** (seuil 2.2.20 annoncé comme rupture ; c'est la version de `tablet-app`, donc un alignement). Six builds — trois avant, trois après. APK de **382 à 349 Mo**.<br>⚠️ **Un cran de plus existe, délibérément non pris** : Kotlin réglé, Flutter réclame Gradle 8.14.0 et AGP 8.11.1. C'est du **terrain neuf pour les deux repos** (`tablet-app` est à 8.11.1/8.9.1, mêmes avertissements latents) — le faire ici seul **désaligne** les deux apps. À faire d'un bloc sur les deux, ou pas du tout : c'est la dette d'outillage déjà parquée en V2 | Son code était déjà porté sur le nouveau contrat — **c'est ce qui aurait dû mettre la puce à l'oreille** : le repo qui a servi de modèle au portage K2 n'avait pas de raison d'être en retard d'outillage sur celui qui le copiait |
| **K7** | **`SectionArticle` sur le kiosk — ajouté le 2026-08-12**, en recomptant les types couverts. Son `case` est commenté « TODO » dans `main_view.getContent` et `Screens/Article/article_view.dart` fait 1 Ko : un article rend « Ce type n'est pas supporté ». **Contrairement à `SectionEvent` et `SectionParcours`, ce type existait dans Mongo** — c'est donc un vrai retard, pas un type né avec Postgres v3, et il sortira de la migration avec du contenu réel.<br>**Le code est une reprise de `mymuseum-visitapp/lib/Screens/Sections/Article/`**, pas une conception. **Le travail n'est pas là** : il est dans le **passage en paysage**. Les écrans de mymuseum sont pensés pour un téléphone tenu debout ; une borne est large, et un article rendu en colonne unique sur 1280 px de large est illisible. C'est le seul endroit du lot K où il y a une décision de mise en page à prendre, pas une copie à faire — d'où la maquette ci-dessous | **À traiter avec K3** : `SectionEvent` pose exactement la même question de paysage (héros, programme, carte), et la trancher deux fois donnerait deux mises en page différentes sur la même borne. Une seule passe de design pour les deux, même raisonnement d'économie que **L5** et **L15** |
| **K7** | **`SectionArticle` sur le kiosk — ajouté le 2026-08-12**, en recomptant les types couverts. Son `case` est commenté « TODO » dans `main_view.getContent` et `Screens/Article/article_view.dart` fait 1 Ko : un article rend « Ce type n'est pas supporté ». **Contrairement à `SectionEvent` et `SectionParcours`, ce type existait dans Mongo** — c'est donc un vrai retard, pas un type né avec Postgres v3, et il sortira de la migration avec du contenu réel.<br>**Le code est une reprise de `mymuseum-visitapp/lib/Screens/Sections/Article/`**, pas une conception. **Le travail n'est pas là** : il est dans le **passage en paysage**. Les écrans de mymuseum sont pensés pour un téléphone tenu debout ; une borne est large, et un article rendu en colonne unique sur 1280 px de large est illisible. C'est le seul endroit du lot K où il y a une décision de mise en page à prendre, pas une copie à faire.<br>**Maquette faite et validée le 2026-08-12**`DOCS/claude design/kiosk-paysage-article-event.html`, une seule passe pour K3 et K7. Six règles communes (deux colonnes ancrage/flux, texte à 65 caractères, héros à 26 % au lieu de 52 %, plus de `showModalBottomSheet`, carte sans « Agrandir », accents dérivés de `configuration.primaryColor` selon le précédent K4) et quatre décisions tranchées : **fond clair** aux valeurs réelles de l'app (`kBackgroundColor` / `kBackgroundLight`), **pas de sélecteur de langue** (choisie en amont), **hors dates : aucune prose**, et le **retour automatique** sorti du périmètre (ligne suivante).<br>⚠️ **Deux vérifications faites dans le code plutôt que supposées.** Les **statistiques sont déjà acquises** : `section_page_detail.dart:60/72` émet `sectionView` et `sectionLeave` avec durée, et il enveloppe les treize types — l'article sera compté dès que son `case` entre dans le `switch`, sans une ligne à écrire. Et **`tablet-app` n'a aucun i18n** : pas de `l10n/`, pas d'`AppLocalizations`, libellés français en dur. C'est ce qui interdit un bandeau « Édition passée » — il serait français pour un visiteur néerlandophone — et c'est pourquoi l'état hors dates s'exprime par les **dates**, numériques donc sans traduction | **À traiter avec K3** : `SectionEvent` pose exactement la même question de paysage (héros, programme, carte), et la trancher deux fois donnerait deux mises en page différentes sur la même borne. Une seule passe de design pour les deux, même raisonnement d'économie que **L5** et **L15**.<br>**Trois états incomplets couverts par la maquette** : sans audio (le dock tombe, l'image reprend la hauteur), sans photo (le rail tombe, l'audio passe en tête de la colonne de lecture), texte seul (une colonne centrée, toujours plafonnée). ⚠️ **`audioIds` est une `List<TranslationDTO>`** : « sans audio » est un état **de la langue courante**, pas de l'article. Et **`isContentTop` existe déjà** — en paysage il ne veut plus dire « au-dessus » mais « à gauche », à câbler sous peine de rendre sans effet un réglage déjà offert dans `manager-app` |
| **K8** | **Retour automatique à l'accueil après inactivité — ajouté le 2026-08-12**, sorti de la maquette K3/K7. Un visiteur part sans fermer l'écran ; le suivant trouve l'article du précédent. Délai proposé : **5 minutes**.<br>**À écrire une seule fois pour les treize types**, dans `section_page_detail` — il enveloppe déjà toutes les sections et porte déjà les crochets `initState`/`dispose`. Bénéfice de bord : le retour passant par le même `dispose`, il émet un `sectionLeave` **avec sa durée réelle** au lieu de laisser une consultation ouverte jusqu'au prochain visiteur. **La statistique devient juste, en plus de l'écran** | Ce n'est ni K3 ni K7 : c'est un comportement de borne qui vaut pour tous les types. À ne pas glisser dans le port des deux écrans, sinon il sera écrit deux fois puis une troisième |
| **K6** | **Publier les APK à jour**, puis vérifier le renouvellement du parc | **L19**. Les tablettes kiosk sont des appareils que tu gères — mise à jour maîtrisée. **Les téléphones des visiteurs, non** : aucune manœuvre technique ne force une mise à jour, d'où la stratégie de coexistence du lot I |
### Lot J — RGPD & conformité (2 à 3 jours) — juste avant le lot I