Lab Skills on the Academy Lesson Row — Design

Date: 2026-08-03 Status: Design approved except one open item (level wording, see §9) Mockup: mockups/lab-skill-row-mockups.html (self-contained, opens with no internet) Decided by: Mario, via chip interview + six mockup iterations on 2026-08-03


1. What this is

Mario's ask was "we need to add the lab skills the user will learn per lesson or unit per ase module." That split two ways and only one half turned out to be real work:

Placement, in Mario's words: "it should be on the lesson box, to the right side, next to the practice, test, start buttons." So the passport is not a separate page. The Academy lesson list itself becomes the roadmap, one row per lesson, each carrying that lesson's lab state.

Scope: current module only (not a career-wide record across B2 to B6).


2. The lesson row, final

Rendered in simulator.html, inside academySelectUnit, around line 7698. Current action group (.atp-actions) holds: Practice icon (.atp-tp), Test icon (.atp-exam), then the open button (.atp-open). Final order after this change:

[Practice] [Test]  [ LESSON / Start ]  [ LAB / Not attempted ]

Both text buttons become two-line pills sharing one geometry block:

Top line small label, LAB or LESSON, 8px, weight 900, letter-spacing 1.3px, opacity .68
Bottom line the value, 11px, weight 900, uppercase
Padding 4px 13px 5px (cut from the original 7px so two lines cost as little height as possible)
Radius 20px, matching today's .atp-open
Icon wrench on LAB; play on Start/Continue, redo arrow on Review

Both pills measure exactly 32px tall. They share one CSS rule for geometry rather than two, so they cannot drift apart when either is edited later.

Why explicit pixel sizes, not em

The surrounding row CSS is written in em, which compounds. The difficulty-modal sizing failed twice for exactly this reason (see CLAUDE.md: .diff-btn span matched both the wrapper and the inner span). The two pill text lines are therefore fixed px with line-height: 1. Do not convert them to em.


3. The six LAB states

Three earned levels are metals; everything below is a flat outline. A metal always means the student earned something.

State Treatment Meaning (see §9, wording not final)
Not attempted grey outline #c3ccd4 / text #59646f Has not started this lab
Ungraded dashed purple outline #7d5fb3 / text #5b3f8c Finished the lab, waiting on instructor sign-off
Exposed amber outline #e8961b / text #9c5f08 Performed the skill with help
Practiced copper, filled metallic Performed the skill on their own
Proficient warm stainless, filled metallic Performed to standard, without help
Mastered gold, filled metallic Can do it to standard every time, and teach it

The metal recipe

All three metals use one shared .metal class carrying a diagonal gloss sweep. The sweep fires ON HOVER ONLY, once per hover — it never loops (Mario, 2026-08-03).

.metal::after{content:"";position:absolute;top:0;left:-70%;width:45%;height:100%;
  background:linear-gradient(100deg,transparent,rgba(255,255,255,.85),transparent);
  transform:skewX(-18deg);pointer-events:none}
@media(hover:hover){.metal:hover::after{animation:shine .85s ease-out}}
@keyframes shine{from{left:-70%}to{left:130%}}
@media(prefers-reduced-motion:reduce){.metal::after{display:none}}

The pill needs position:relative; overflow:hidden for the sweep to clip correctly.

Three things this buys, all deliberate:

Verified in-browser: default state has animation-name: none with the sweep parked off-screen at -95px, the hover rule resolves to 0.85s ease-out ... 1 normal ... shine (iteration count 1), and the word infinite appears nowhere in the stylesheet. Exact gradients for copper, steel B and gold are in the mockup and should be copied verbatim.

All metals are pure CSS, no image, so they stay crisp at any zoom and add zero network requests.

Rejected along the way, with the reason


4. The repetition count

Shown as xN at the right end of the LAB top line, using space that was already empty. It costs zero width and zero height: every pill is still 138px wide (154px for "Not attempted") and 32px tall with the count present, including the two-digit x10.

Rule: the count shows from the very first rating. A rated lab always carries at least x1. The two states with no rating yet, Ungraded and Not attempted, carry no count, because there is nothing to count and x0 would read as an error.

Practice targets, shown in the legend only

Level Target
Exposed about 1 run of the lab
Practiced about 3 runs
Proficient about 5 runs
Mastered about 10 runs

These are a guide, not a promotion rule. This matters and was raised with Mario explicitly:

Practical note flagged to Mario: at 10 ratings for Mastered across 424 labs, Mastered becomes rare. That may be intended, since a gold pill should mean something, but 10 is the number to revisit if Mastered should be reachable within a semester.


5. The skill card (what the LAB pill opens)

Opens that lesson's own skill, not a separate passport page. Content, per Mario's choice "show the standard, hide the scored questions":

Always visible

Hidden until the lab has been done

After rating, everything unlocks, including which rubric lines they missed, their level, the date, and the rep count.


6. The unit legend (Legend B)

Sits at the bottom of every unit's lesson list, after the rows, inside a bento card per CLAUDE.md rule 15 (accent top border, title, short description above the content).

Layout: a vertical list, real pill on the left, meaning on the right. 9 rows total.

  1. Six rows, one per state, each showing the real pill at its typical rep count (Exposed x1, Practiced x3, Proficient x5, Mastered x10; Not attempted and Ungraded carry none) with the meaning and the "usually takes about N runs" line beneath.
  2. A divider, then three rows explaining the rest of the row: the Practice icon, the Test icon, and what xN on the LAB button means.

Using the real pills in the legend rather than swatches is deliberate. A student matches what is on the row to what is in the key without translating anything.

Rejected: a grid layout (three across, more compact but harder to scan), and a collapsed "What do these mean?" bar (saves room but a first-timer has to notice and tap it).


7. Data

Read at runtime

lab_grades/{studentUid}one document per student holding every lab they have ever done, so the whole passport costs a single Firestore read. This matches the caching discipline in CLAUDE.md; do not add a per-lesson read.

lab_grades/{studentUid} = {
  "<deckId>": {                       // e.g. "b5-l04"
    score: 4, total: 5, pct: 80,
    lines: [true,true,false,true,true],   // index-aligned to the deck's rubric lines
    level: "proficient", levelNum: 3, levelLabel: "Proficient",
    reps: 3,                              // true count, may exceed attempts[].length
    first: <ms epoch>,
    attempts: [ ... ],                    // capped at 12: first + latest 11
    lessonTitle: "...", lesson: "Lesson L04", module: "B5",
    by: "<admin email>", at: <ms epoch>
  }, ...
}

Field names are an external contract (the STC Collision Portal Phase 2 consumes them). Keep lines[] index-aligned or old grades silently mislabel.

State derivation for the pill:

Condition Pill state
no entry for this deckId Not attempted
entry exists, no levelNum Ungraded
levelNum 1 to 4 Exposed / Practiced / Proficient / Mastered

Read for the skill text

The skill content lives in each lesson's labassignment block, inside the deck files and Firestore. The menu cannot load 95 decks to paint a unit. It needs a small generated lookup, following the existing _deck-versions.js pattern, keyed by deckId and carrying at minimum the SKILL line (and ideally the procedure, standard, rubric and safety for the card).

functions/scripts/academy-build/build-lab-skills-doc.js already harvests exactly this content for the human-readable doc, so the generator should be extended rather than rewritten. Re-run it after any lesson change, same rule as build-deck-versions.js.

Precedent for the menu-side lookup: window.ACADEMY_LESSON_META (time, slides, ASE tasks).


8. Measured constraints (verified in-browser, not estimated)

Check Result
LAB pill height, all six states 32.0px, identical
LESSON pill height 32.0px, identical to LAB
LAB pill width 138px, except "Not attempted" at 154px
Effect of adding xN none, 138px either way, including x10
Lesson row height 75.8px, unchanged from the single-line version
Phone rows whose buttons wrap at 375px 0 of 6
Horizontal overflow, page and 375px pane none

Why the row height does not grow: the time chip and ASE task chip under the lesson title already make the row taller than any pill, so the pill growing from 28px to 32px is absorbed.


9. OPEN — needs Mario before build

The four level definitions. These are my draft of his ladder, and students will read them as the definition of their own skill level. They are his grading language, not mine:

Two smaller ones, both with a defensible default if he does not care:


10. Implementation notes for the build session


11. Explicitly out of scope