# D58. The learner's player: two configurable bars, resources, a glossary, an AI mention and credits (19 Oct 2026)

*Revised on 20 Oct 2026: the founder decided the editor of the player on Lin 44, and the core follows it (the last part of this entry, « Lin 44 »): a spacer item, the « IA » mark, links and images among the resources, the glossary's terms linked to the screens by one operation, a plate for the logo, « Affichage » and « Langue » (D59). What changed is marked where it stands. Revised again on 29 Oct 2026: where the learner is, said by the progress in parts and the outline's button (« Where the learner is », after Lin 44). Revised on 31 Oct 2026: the progress's options, the bar in one piece and its percentage (« The progress's options », schemas 0.1.9).*

The player around the screens had grown by additions (a header with the title, the progress and the outline; a footer with Previous, a counter and Next; the narration's bar slipped into the footer), with settings scattered in `navigation` and `narration`, and the look of a prototype. The founder asked for a clean base with the look of the Lin pages 36 (the module as the Revue shows it) and 27 (the narration's bar and its seek tooltip), the player's chrome configurable by authors in the app (Lin 44, drawn in parallel from the same model), Resources and a Glossary, an « IA et crédits » mention, checks on navigation and regulation, and tools that let agents fit the player to a module's theme without writing CSS for it.

- **One place: `course.json` `player`.** Two bars, `top` and `bottom`, each `{ items[], position }`; an item is `{ id, enabled, options? }` and stands in one bar at most. Items: `logo`, `title`, `progress`, `outline`, `controls`, `resources`, `glossary`, `credits`, `previous`, `next`, and `exit` (Quit, added to the founder's list because D3 wants one in an LMS; never shown on the web). An item shows when it is enabled and has something to show; a bar with nothing to show is removed and the screen takes its room. Whether a bar shows is decided for the course, not per screen (the controls count when some screen is timed), so the room a slide has never changes from one screen to the next. Ordered lists were chosen over flags on each item because the editor drags items within and between bars; three zones per bar (start, centre, end) were weighed and set aside as one more concept to draw and explain: the room left goes to the title or the controls, else before the first of the items that sit at the end of a line (Resources, Glossary, « IA et crédits », Quit, Previous, Next) that follows another item. *Revised on 20 Oct 2026: that rule stays for a bar without a spacer; a bar with one says it (below).*
- **Settings migrated, no migration.** `navigation.showMenu` and `showProgress` became the `outline` and `progress` items, `navigation.footer: "end"` the bottom bar's `position: "page"`, and the course's `narration.seek: "none"` the controls option `seekBar: false`; `narration.seek` keeps `seen` and `free`, how far learners may move (a pedagogical rule like `lockUntilComplete`, read by Continue), and a screen may still say `none`. Before publication the break is clean (the founder's choice): the old settings are `E_PLAYER_SETTING_MOVED`, which says where each went; the samples, the course template, the importer of Storyline and Rise and the agents' rules were updated. The web target's free navigation (D41) shows the outline whatever the course says, as it did.
- **Defaults.** Top: `logo` (disabled until an image is given), `title`, `progress`, `resources`, `glossary`, `credits`, `exit`; bottom: `outline`, `controls`, `previous`, `next`; both bars in the window. *Revised on 20 Oct 2026: a spacer after `progress` and after `controls`; `display` and `language` in neither bar.* It is Lin 36's title bar with its chips on the right and Lin 27's bottom bar, and the arrangement of Storyline's player (title, menu and resources above, the seek bar and navigation below) and of Rise (an outline with progress). The controls show the seek bar, the volume and the transcript, captions off until the learner turns them on, and speeds from 0.75× to 2×: learners who choose their pace stay, and the editor says so beside the option.
- **The look and its tokens.** Lin's sizes at the reference window (bars of 56 and 72 px, 44 px buttons, 20 px icons, pills), in rem so they follow the size classes. Colours and size are theme tokens, `player.top.background`, `player.top.text`, `player.bottom.background`, `player.bottom.text`, `player.accent`, `player.onAccent`, `player.progress`, `player.tooltip.background`, `player.tooltip.text`, `player.radius` and `player.density` (`comfortable` or `compact`), defaulting to the charter: a top bar in the course's colour, a bottom bar on its surface whose text is the course's colour when it reads there, else the text colour. Every pair is checked like the charter's (`E_THEME_CONTRAST`), the cascade is unchanged (D32: the chrome lives in `elabora.base`, a theme may still restyle its hooks), and generators do not see these tokens, so restyling the player never makes a figure out of date.
- **Nothing moves.** Previous and Next unavailable stay in place, greyed (`aria-disabled`); Next keeps the width of its longest label (Next and Continue share one cell); a refusal, a hint, the seek bar's quote and a glossary definition are one tooltip, fixed over the page (WCAG 1.4.13: it stays while hovered, Escape dismisses it), and a refusal is said in the live region too. The in-page Continue of scroll pages says why it waits the same way. On a phone the controls' row is kept on every screen of a course that has one; with the outline's option `label` set to a title (*revised on 29 Oct 2026*, below), its button has one width, the title cut with an ellipsis and whole in the tooltip. The controls work from either bar: from the top bar their popups open downward and the captions stay over the bottom of the screen. Over a modal dialog (a layer, the PDF preview) the tooltip goes into the dialog, and a term clicked there keeps its definition in it, the glossary's panel being under the dialog.
- **The seek tooltip** quotes what is said there, with its speaker, never the time (« Marc : « …et mes collègues ont… » »), on hover as when dragging, from the line's own text so that its punctuation stays. **The play button** that the browser's autoplay policy may ask for is one large circle with « Écouter l'écran »; its click is the gesture the browser waits for; it comes first in the screen for the keyboard.
- **Resources** are files of `assets/` named in `course.json` `resources` and shipped in the package. A PDF opens in a preview over the module, the browser's own viewer in a frame, with « Télécharger » and « Ouvrir dans un nouvel onglet » in its head; another file downloads (`download` link). *Revised on 20 Oct 2026: an image opens in the same preview, and a resource may be a link (below).* Weighed: pdf.js embedded (about 1.4 MB with its worker, a worker file that IIS does not serve by default, and maintained code in every package) and pages rendered at build time (a native renderer, images heavier than the PDF and no text to select); the browser's viewer costs nothing, renders the PDF faithfully and lets learners search, zoom and print. Measured on 19 October 2026: Chrome 154 (headless, as the captures run it) shows the sample's two-page PDF in the frame; `navigator.pdfViewerEnabled` is false where there is no viewer (Android, some Electron webviews), and the preview then shows the file's card with its two buttons; a sandboxed LMS frame may refuse Chromium's viewer, which is why the buttons are always there. *Measured on 20 Oct 2026 in Electron 44 (the app's), after the app's « Lecteur » tab found its preview empty:* Chromium's viewer draws in a frame of a sandboxed, context-isolated window with `webPreferences.plugins` left off, nested in another origin's frame and in a modal dialog alike, and `pdfViewerEnabled` tells the truth there. The empty frame came from the core's local servers (canvas, preview, emulator), which sent `.pdf` as `application/octet-stream`: the browser downloads such a file, leaves the frame on its empty first document and fires no load event. They now send the resources' types (`application/pdf`, the office formats, ZIP, RTF). And since an LMS may still send a PDF as a download (another type, or `Content-Disposition: attachment`), the preview asks the server first (a HEAD request, same origin) and shows the file's card when the answer is not a PDF to show inline; a server that does not answer HEAD gets the card only after 8 s of an empty frame, so that a slow LMS keeps its viewer. `.pdf`, office documents, text, CSV, images, audio, video and ZIP are served by every LMS web server (IIS's default MIME map); another extension is `W_RESOURCE_TYPE` (D7).
- **The glossary** is `course.json` `glossary`: `id`, `term`, `definition`, `variants` (searched too), translated with the course. The player's panel has a search field (accents and case aside; terms first, then definitions that say the words) and the entries in the alphabetical order of the course's language. A text links a word to its entry with a Markdown link, `[passerelle](glossary:gateway)`, compiled to a link to the entry: its definition in the tooltip on hover or focus, the panel opened there on a click. Matching terms in texts automatically was set aside: false matches (a common word that is also a term), forms the matcher misses, and translations that would mark other words; a link is chosen, visible to authors and kept by translators. *Revised on 20 Oct 2026: an operation writes those links for the author, once (below); the player still matches nothing.*
- **« IA et crédits »** says what the course knows by itself: the speakers whose voice is generated (their sidecars, or a voice of a voice tool when the line has no audio yet: it will be generated, D25, D29) and the media blocks marked `generated`, then the author's `credits` and the credits CC-BY voices ask for. The AI Act asks deployers to disclose synthetic audio, images and video that resemble real people (Regulation (EU) 2024/1689, article 50(4)), clearly and at the latest at the first exposure (article 50(5); applicable from 2 August 2026). The Commission's code of practice on AI-generated content (final version of 10 June 2026) recommends an « AI » icon to deployers and a visible mention when there is a screen, and its guidelines on article 50 (C(2026) 5054, 20 July 2026) say that marking files for machines does not replace a mention people perceive. A narrator's voice made from a donor's recording may resemble a real person, so the course says it: the button is an icon (*revised on 20 Oct 2026: « IA », below*), and the first time a generated voice plays its tooltip says « Voix générée par IA » for six seconds; the transcript of a narrated screen keeps the mention and its voices' credits whatever the bars show.
- **Checks**, each with a code and a fix in the catalogue: `W_PLAYER_NO_WAY_BACK` (neither the outline nor Previous; may be deliberate with buttons in the screens, so it can be ignored by the author), `W_PLAYER_END_UNREACHABLE` (Next hidden: the screens reached from the first through `go-to` events must lead to the last, counting an event only where learners can reach its trigger: in slides a step that opens with Next cannot be reached, unless it opens once answered, an event reveals it or a narration plays it; a narration that goes to the next screen by itself does not count, since it stops on phones and for learners who choose « M'arrêter à chaque étape »; the details list the screens), `W_PLAYER_AI_MENTION_HIDDEN` (AI Act, art. 50(4) and (5)), `W_PLAYER_CONTROLS_HIDDEN` (a narration without pause, captions or transcript: WCAG 2.2 1.2.2, 1.4.2 and 2.2.2, EN 301 549 9.1.2.2, 9.1.4.2 and 9.2.2.2, RGAA 4.1 criteria 4.3 and 4.10), `W_PLAYER_TRANSCRIPT_HIDDEN` (WCAG 2.2 1.2.1 and 1.2.3, EN 301 549 9.1.2.1 and 9.1.2.3, RGAA criterion 4.1), `W_PLAYER_EXIT_HIDDEN` (D3), `I_PLAYER_SPEEDS_NONE`, `E_PLAYER_ITEM_DUPLICATE`, `E_GLOSSARY_TERM_UNKNOWN`, `E_DUPLICATE_ENTRY`, and the logo's and resources' files (`E_ASSET_MISSING`); *since 20 Oct 2026* `E_RESOURCE_URL` and `W_PLAYER_LOGO_CONTRAST`.
- **For agents** (D53): one operation reads and changes all of it, `elabora_player` (CLI `elabora player`, the engine's `update_player`): the bars with what shows and why not, the tokens with their values, the findings; changes in one undoable change. The visual-design skill has the recipe (tokens from the charter, the theme's radius, a logo, defaults otherwise) and the course rules one line.
- **Competitors.** Kept from Storyline's player: the menu (outline), resources, glossary, volume, seek bar, captions, speeds, and Quit; from Rise: the outline with what is done and what is not open yet, and the progress. Added: the outline says where the learner is (« Écran 3 sur 12 »); the glossary's terms linked from the texts. Left out: Storyline's notes panel (the transcript is our narrator's notes), its custom tabs and its « light box » slides (layers do it), a search of the whole course (later, with the outline), bookmarks, and a dark mode of the player's own (a dark course is its tokens, as the orbital sample shows).

**Lin 44 (20 Oct 2026).** The founder approved the editor of the player drawn on Lin 44 with these choices; the format, the operation and the player follow them.

- **« Espace flexible », the item `spacer`.** One per bar, always on (it cannot be disabled: `update_player` refuses), moved like the others within its bar. The items before it go to the start of the line, those after it to the end; the narration's controls stretch first (the spacer takes room only on screens without them), so nothing jumps from a narrated screen to one that is not. A bar without a spacer keeps the rule above: courses written before it, and authors who write the lists by hand, see no change, so there is no migration. A second spacer in a bar is `E_PLAYER_ITEM_DUPLICATE`; a bar with only its spacer to show is removed. New defaults: top `logo`, `title`, `progress`, `spacer`, `resources`, `glossary`, `credits`, `exit`; bottom `outline`, `controls`, `spacer`, `previous`, `next`. Enabling the spacer of a bar that has none puts it back at its default place. On a phone the bars wrap and the spacer keeps the end of the line at the end.
- **The « IA » mark.** « IA et crédits » shows « IA » (« AI » in English and Dutch, « KI » in German, « IA » in French, Spanish, Italian and Portuguese) when the course has a generated voice or generated media, the icon « i » otherwise, as the code of practice's icon asks; its name for screen readers stays « IA et crédits », and the first generated voice still says « Voix générée par IA » in the tooltip and the live region.
- **Resources: images and links.** An image (PNG, JPEG, GIF, WebP, SVG, AVIF) opens in the preview the PDFs have, with « Télécharger ». A resource is a file (`asset`) or a link (`url`), one of the two: `{ id, title, description?, url }`, an `https://` address (others are `E_RESOURCE_URL`, left out of the package; `update_player` refuses them), opened in a new tab (`rel="noopener noreferrer"`), with a link's icon, « Lien · » and the address's host under its title and « s’ouvre dans un nouvel onglet » for screen readers. The player itself loads nothing from a link (it never goes on the network); a learner opens it. « Ajouter une ressource » takes any file (a PDF, a spreadsheet, a document): `add_resource` (engine; `@elabora/node` `edit.addResource`; CLI `elabora player --add-resource <file>`; MCP `elabora_player` `addResources`) copies it as it is into `assets/resources/` under a readable name (`-2` when the name is taken by another content; the same content is not copied twice) and adds its entry, in one journaled change; undone, the copy goes with the entry (its bytes stay in the blob store, as a font's do), where a media import leaves its file. Its title is the one given, else the PDF's own, else the file's name. `@elabora/node/sources/pdf-title` `pdfTitle(file)` reads a PDF's own title: the XMP metadata's `dc:title`, else the information dictionary's `/Title`, found from the trailer (in a compressed object stream too); null when the PDF says none, is encrypted, or says one a program wrote (« Untitled », « Microsoft Word - brief.docx », a file name, a form's code). No dependency (D49 reads PDFs as they are). On 25 PDFs of a working computer it gave the titles macOS Spotlight reads (`mdls`), apart from the ones it drops on purpose.
- **« Relier les termes aux écrans ».** The glossary item's option `linkTerms`, on by default. Turned on, it links every term; while it is on, a term added is linked; `elabora_player` `linkTerms` (CLI `--link-terms [ids]`) links on demand. The first time a term or one of its variants is in a screen's texts (screens in the course's order, texts in reading order, whole words), it becomes `[mot](glossary:<id>)`; a screen that already links to the term is left as it is. Every form of every entry is looked for at once, the longest first, whatever the glossary's order (« réseau privé virtuel » before « réseau »); a word written in capitals or with a capital inside (« SI », « VPN », « iPhone ») matches with its case, any other in either case. Only the screens' own Markdown texts: the plan's locked texts and validated questions keep the client's words, and nothing is touched in code blocks and fences (as markdown-it, the player's Markdown, reads them), code spans, maths, links (inline, reference, with nested brackets), bracketed text, link definitions, HTML tags and autolinks, or bare URLs. *Revised after review (20 Oct 2026):* a text the links change would go stale in every translation that had it up to date, and a package of every language would then leave that language out. So the same link is placed in those translations where the term's translation is (the term itself for `translate: false`), and the unit stays up to date; where it is not, the unit is left stale, and the result lists it (`translations: [{ language, linked, stale }]`). `elabora build --languages all` names each translation it leaves out and what it lacks (`W_PACKAGE_LANGUAGE_LEFT_OUT`). *Added on 20 Oct 2026:* removing an entry removes its links in the same change (`update_player` with the glossary less that entry): every link to it in the screens' texts, placed by the operation or written by hand, inline or by reference (its definition goes), becomes its words, read with the same Markdown model; the translations that had those texts up to date lose the same links and stay up to date. The result lists them (`unlinked: [{ screen, term, text, unit }]`, and `translations[].unlinked`). A link left to an entry that is gone would stop the build (`E_GLOSSARY_TERM_UNKNOWN`). It never runs on its own afterwards, so a link the author removed stays removed: no list of removed links is kept, because the only moments it runs are ones the author asked for (turning the option on, adding a term, asking), and each says how many links it placed (the operation's result lists them). Deterministic: the same course gives the same links. A glossary entry with `translate: false` keeps its term and variants in every language (a product name, an acronym); its definition is translated. The pedagogy skill has the recipe « Relever les termes » (the agent proposes entries from the screens, the author keeps them, the operation links them). *Added on 20 Oct 2026:* an entry may say where its term was found, `source: { kind: "source" | "screen", id, at? }` (a file of `sources/`, D49, by its name, or a screen; `at` where in it: « p. 4 »), which the editor shows beside the entry; the recipe asks agents to fill it. It is the author's: never shown to learners, never translated, left out of packages; a screen renamed is followed. A source that is gone is not a finding: `sources/` is outside the course (D49), so the editor says it.
- **The logo's plate.** `logo.options.plate`: the logo on a white plate with rounded corners, for a logo that does not read on its bar. `W_PLAYER_LOGO_CONTRAST` says when an SVG logo does not: the colours its painted shapes and text show, each against the bar's background (`player.top.background` or `player.bottom.background`), the best of them under 3:1 (WCAG's ratio for graphics, 1.4.11, as a reference: a logo is exempt). Only what is painted counts: what `<defs>`, clip paths, masks, patterns, symbols and gradients hold does not, nor a hidden shape. Fill and stroke are inherited, from attributes, simple `<style>` rules and style attributes in CSS's order; black when nothing says a fill; `currentColor` is the SVG's own `color`, black unless it says another (the player shows the logo as an image). When unsure it says nothing: an embedded image, `<use>`, a gradient or pattern paint, transparency, a filter, mask or blend on what is painted, a rule it cannot match (`@media` included), an animation. It costs a reading of the SVG's text, no rendering, bounded (2 MB); a raster logo is not measured (its pixels would need a decoder). The finding carries the ratio as data (`measured`, `limit`, and `details`: the bar, its token, its background, the file), so an editor says it in its own words.
- **« Affichage », the item `display`.** Off in a new module, placed after the bottom bar's spacer when enabled. A menu with the text size in three steps (« Normale », « Grande », « Très grande »: the screen and the layers over it at 115 % and 130 %, by CSS `zoom` on the screen, never on the bars), « Sous-titres plus grands » (captions at 1.75 rem) and the keyboard shortcuts of narrated screens when the course has some; kept for the course where the page keeps progress (`elabora.display:<course id>`). Measured on 20 October 2026 in Chrome on the four samples: the bars never move (same place and height at every size, 1600×900 and 1280×720); scroll pages reflow and grow (medieval-charts at 1600×900: 14,830 px of pages, 16,971 at « Grande », 19,113 at « Très grande »); a slide grows as a whole and scrolls when it no longer fits: at 1600×900, 2 of orbital's 5 slides and 4 of planetary's 9 at « Grande » (at most 103 px to scroll), 4 of 5 and 7 of 9 at « Très grande » (at most 286 px); at 1280×720 a little more (at most 137 px, then 646 px). The learner chose it; the slide's layout itself does not change.
- **« Langue », the item `language`.** Off in a new module, placed after the top bar's spacer; shows in a package of several languages (D59).
- **Not now.** Full screen (the founder's choice; in an LMS it also needs the LMS's frame to allow it). The volume item can be unchecked without a warning (WCAG 1.4.2 asks for a pause or a stop, which Play is). `elabora_player` reports how learners move through the course (`navigation.mode`, `lockUntilComplete`), which the editor shows with the outline and Next, while they stay in `course.json` `navigation`.

**Where the learner is (29 Oct 2026).** The founder removed the eyebrow the opening layout wrote over lesson titles (« Partie 2 · … »): an over-title written by the machine reads as AI design, and learners skip it; where the learner is belongs to the module's interface. Of the ways weighed he kept two, the progress in parts (clickable) and the outline's button, and set aside a part's name that appears and fades when the learner enters it: something that comes and goes draws the eye away from the slide.

- **The progress in parts.** In a course of two sections or more, `progress` is one segment per section, as wide as its share of the screens (the item is a little wider for many parts: 1.75 rem a part, between 11 and 22 rem). A part fills as its screens are completed (its required screens, else all of them); a completed part is full, and the part shown has a stronger line. Each segment is a button: hovered or focused with the keyboard, the one tooltip gives the part's name (« La torréfaction »), and screen readers hear where it stands too (« Partie 2 sur 4 : La torréfaction, en cours, 2 écrans terminés sur 5 »). The founder asked for the name alone on screen: the fill and the stronger line already show where the part stands. The pointer going from part to part, the tooltip follows it at once, as it does along any of the bars' buttons once shown. The segments are one toolbar: Tab stops once, on the part shown, and the arrows, Home and End go from part to part. The share of the whole stays beside it, and the toolbar's name says it (« Progression : 40 % »). A course of one section keeps the bar in one piece (`role=progressbar`). An option to keep the bar in one piece in a course of parts was weighed and left out: the outline already shows learners the sections, the item has the same width either way, and no course has shown a reason to hide them. *Revised on 31 Oct 2026: the founder asked for it (below).*
- **A click goes to the part's first screen**, as a book's chapter or a video's does: the tooltip names where the click leads, whatever the learner did in the part. Going back to where the learner stopped in the part was weighed: it needs a place per part kept with the progress, its result would differ from one learner to the next under the same tooltip, and Next, the outline and the LMS's bookmark already resume. A part whose first screen cannot be opened yet (`navigation.mode: linear`, `lockUntilComplete`) refuses as Next does: greyed in place (`aria-disabled`), it says why in the tooltip and the live region, with Next's own reason when the part comes right after the screen shown, else « Cette partie n'est pas encore disponible ». In the editor's preview (`chrome=live`) every part opens, and the canvases show the progress of a learner at the screen shown who has completed the screens before it.
- **Nothing moves.** The segments' widths are set by the course, from the first screen. Hovered, a line darkens; focused, the ring goes around the line, not around the button's 44 px. Nothing appears or fades unless the learner pointed at it.
- **On a phone** (760 px wide or less) a segment is a few pixels wide: a tap opens the outline instead, at the screen shown, where each screen is a line a finger can reach. A mouse, a pen and the keyboard still go to the part, and so does a tap when the outline is not in the bars. Each segment is as tall as a bar's button, so the target is 44 px high everywhere.
- **The outline's button may name the part shown.** The outline's option `screenTitle` became `label`: `outline` (« Sommaire », the default), `section` (the title of the part shown) or `screen` (the title of the screen shown). One choice among three replaces two booleans that would allow a fourth case with no meaning. With a title, the button keeps one width on every screen, the title cut with an ellipsis and whole in the tooltip, and screen readers hear « Sommaire : La torréfaction ». This is a clean break before publication: the 0.1.5 schemas do not know `screenTitle`.
- **For agents.** The course's rules and the visual-design skill say it: a screen never says where the learner is in its own texts (no eyebrow « Partie 2 », no « Leçon 3 sur 6 »); the progress and the outline do.

**The progress's options (31 Oct 2026, schemas 0.1.9).** The founder asked the Lecteur tab for a choice between the bar in parts and the classic bar, and a checkbox for the percentage. The item `progress` takes options like the others: `parts` (default true; false: one bar in one piece, `role=progressbar`, not a button, in a course of parts too) and `percent` (default true; false: the bar alone, the percentage still said to screen readers, by the bar's value or the toolbar's name). Two booleans and no enum: each says one thing shown or not, as the controls' `seekBar` and `volume` do, and the four cases all have a meaning. The item keeps its width either way, so a bar changed from one to the other moves nothing else. On a phone the percentage is hidden whatever the option says, as before.

*Revisit if:* authors need a third place for items (a side bar, Storyline's sidebar menu), per-screen bars, resources in several languages (a file per translation), a glossary that the author's AI fills from the sources (the recipe « Relever les termes » does it from the screens), logos that do not read and that the check misses (then raster logos measured), an LMS frame sandboxed so that no PDF viewer loads (then pdf.js as an option), learners who look for where they stopped in a part (then a place per part, kept with the progress), or a course whose parts learners should not see (done on 31 Oct 2026: `progress.parts`).
