diff --git a/apps/cadcraft/src/workspace.rs b/apps/cadcraft/src/workspace.rs
--- a/apps/cadcraft/src/workspace.rs
+++ b/apps/cadcraft/src/workspace.rs
@@ -47,12 +47,19 @@ fn snapshot(ui: &UiState) -> Result<Value, String> {
     if ui.history_lines > 12 {
         return Err("historyLines must be between 0 and 12".into());
     }
+    if ui.ribbon_tab.len() > 64 {
+        return Err("ribbonTab exceeds the workspace size limit".into());
+    }
     Ok(json!({
         "docking": ui.docking,
         "dockingVersion": cadcraft_ui_egui::docking::LAYOUT_VERSION,
         "showToolsets": ui.show_toolsets,
         "showPalettes": ui.show_palettes,
         "showToolbar": ui.show_toolbar,
+        "showRibbon": ui.show_ribbon,
+        "ribbonTab": ui.ribbon_tab,
+        "ribbonMinimized": ui.ribbon_minimized,
+        "pcLayoutApplied": ui.pc_layout_applied,
         "showFileTabs": ui.show_file_tabs,
         "showStatusBar": ui.show_status_bar,
         "showCommandLine": ui.show_command_line,
@@ -255,6 +262,21 @@ mod tests {
         fs::remove_dir_all(directory).unwrap();
     }
 
+    #[test]
+    fn the_ribbon_survives_native_workspace_file_reload() {
+        let directory = temp_dir();
+        let path = directory.join("workspace.json");
+        let (mut first, _, _) = Workspace::at(Some(path.clone()));
+        let ui = UiState { show_ribbon: true, ribbon_tab: "annotate".into(), ribbon_minimized: true, pc_layout_applied: true, ..Default::default() };
+        first.save_if_changed(&ui, Instant::now()).unwrap();
+        let (_, restored, error) = Workspace::at(Some(path.clone()));
+        assert!(error.is_none(), "{error:?}");
+        let restored = restored.unwrap();
+        assert!(restored.show_ribbon && restored.ribbon_minimized && restored.pc_layout_applied);
+        assert_eq!(restored.ribbon_tab, "annotate");
+        fs::remove_dir_all(directory).unwrap();
+    }
+
     #[test]
     fn dock_columns_floating_and_hidden_panels_survive_native_workspace_file_reload() {
         use cadcraft_ui_egui::docking::{LAYOUT_VERSION, Panel, permissions, reveal_panel};
diff --git a/crates/ui-egui/src/chrome.rs b/crates/ui-egui/src/chrome.rs
--- a/crates/ui-egui/src/chrome.rs
+++ b/crates/ui-egui/src/chrome.rs
@@ -61,13 +61,60 @@ fn title_left(left: f32, width: f32, text_w: f32) -> f32 {
     (left + (width - text_w) / 2.0).max(left + MAC_CONTENT_LEFT)
 }
 
+/// The Windows layout's quick access tool bar (title row, with the Ribbon).
+const QAT: &[(Icon, &str, &str, Option<&str>)] = &[
+    (Icon::New, "new", "New Drawing", Some("Cmd+N")),
+    (Icon::Open, "open", "Open", Some("Cmd+O")),
+    (Icon::Save, "qsave", "Save", Some("Cmd+S")),
+    (Icon::SaveAs, "saveas", "Save As", None),
+    (Icon::Plot, "plot", "Plot", Some("Cmd+P")),
+    (Icon::Undo, "u", "Undo", Some("Cmd+Z")),
+    (Icon::Redo, "redo", "Redo", Some("Cmd+Shift+Z")),
+];
+const QAT_SIZE: f32 = 22.0;
+
+/// Draw the quick access tool bar from `x` on the row centred at `cy`; returns its right edge.
+fn quick_access(ui: &mut egui::Ui, x: f32, cy: f32, clicked: &mut Option<&'static str>) -> f32 {
+    let t = Tokens::get();
+    let mac = ui.ctx().os().is_mac();
+    let mut x = x;
+    for (i, (icon, cmd, label, shortcut)) in QAT.iter().enumerate() {
+        if i == 5 {
+            x += 8.0;
+        }
+        let br = Rect::from_min_size(pos2(x, cy - QAT_SIZE / 2.0), vec2(QAT_SIZE, QAT_SIZE));
+        let resp = ui.interact(br, ui.scope_id().with(("qat", *cmd)), Sense::click());
+        if resp.hovered() {
+            ui.painter().rect_filled(br, 3.0, t.control_hover.gamma_multiply(0.6));
+        }
+        icons::paint(ui.painter(), br.shrink(3.0), *icon, false);
+        let tip = match shortcut {
+            Some(sc) => format!("{} ({})", crate::i18n::t(label), crate::menus::shortcut_label(sc, mac)),
+            None => crate::i18n::t(label).to_string(),
+        };
+        icons::describe_control(ui, &resp, &tip, None, false);
+        if resp.on_hover_text(tip).clicked() {
+            *clicked = Some(cmd);
+        }
+        x += QAT_SIZE + 4.0;
+    }
+    x
+}
+
 /// Title row (with the integrated macOS title bar) and the Tool Bar.
 pub fn title_and_toolbar(app: &mut CadApp, ui: &mut egui::Ui) {
     let t = Tokens::get();
     let title_h = if app.integrated_titlebar { MAC_TITLE_BAR } else { 0.0 };
+    // With the Ribbon and no integrated title bar, the quick access tool bar gets its own row.
+    let qat_h = if app.ui.show_ribbon && !app.integrated_titlebar { 28.0 } else { 0.0 };
     let tb_h = if app.ui.show_toolbar { 34.0 } else { 0.0 };
-    egui::Panel::top("cc_title_toolbar").exact_size(title_h + tb_h).frame(egui::Frame::NONE.fill(t.chrome)).show(ui, |ui| {
+    egui::Panel::top("cc_title_toolbar").exact_size(title_h + qat_h + tb_h).frame(egui::Frame::NONE.fill(t.chrome)).show(ui, |ui| {
         let r = ui.max_rect();
+        let mut qat_clicked = None;
+        if qat_h > 0.0 {
+            quick_access(ui, r.left() + 10.0, r.top() + qat_h / 2.0, &mut qat_clicked);
+        }
+        let r = Rect::from_min_max(pos2(r.left(), r.top() + qat_h), r.max);
         if title_h > 0.0 {
             // The empty row moves the window; it goes first so later widgets take their own clicks.
             craft_window::title_bar::empty_area(ui, Rect::from_min_size(r.min, vec2(r.width(), title_h)));
@@ -78,8 +125,16 @@ pub fn title_and_toolbar(app: &mut CadApp, ui: &mut egui::Ui) {
             // Centred on the traffic lights' centre line (the row's middle): with the system font
             // the capitals' middle is the text box's middle, so no optical nudge.
             let galley = ui.painter().layout_no_wrap(title, egui::FontId::proportional(13.0), t.text_dim);
-            let x = title_left(r.left(), r.width(), galley.size().x);
+            let qat_room = if app.ui.show_ribbon { QAT.len() as f32 * (QAT_SIZE + 4.0) + 24.0 } else { 0.0 };
+            let x = title_left(r.left(), r.width(), galley.size().x).max(r.left() + MAC_CONTENT_LEFT + qat_room);
             ui.painter().galley(pos2(x, r.top() + (title_h - galley.size().y) / 2.0), galley, t.text_dim);
+            // The Windows layout keeps its quick access tool bar in the title row.
+            if app.ui.show_ribbon {
+                quick_access(ui, r.left() + MAC_CONTENT_LEFT + 4.0, r.top() + title_h / 2.0, &mut qat_clicked);
+            }
+        }
+        if let Some(c) = qat_clicked {
+            app.start(c);
         }
         if tb_h > 0.0 {
             let row = Rect::from_min_size(pos2(r.left(), r.top() + title_h), vec2(r.width(), tb_h));
diff --git a/crates/ui-egui/src/docking.rs b/crates/ui-egui/src/docking.rs
--- a/crates/ui-egui/src/docking.rs
+++ b/crates/ui-egui/src/docking.rs
@@ -109,6 +109,21 @@ pub fn default_workspace() -> Workspace {
     })
 }
 
+/// The Windows layout: the Properties palette docked on the left of the drawing, no Tool Sets
+/// (the Ribbon holds the tools) and no Layers palette (the Ribbon's Layers panel holds the layer
+/// list).
+pub fn pc_workspace() -> Workspace {
+    Workspace::new(Layout {
+        root: Some(split(
+            SplitAxis::Horizontal,
+            SplitSize::FixedFirst(PALETTES_WIDTH),
+            dock(Side::Start, PALETTES_WIDTH, &[Panel::Properties]),
+            tabs(Panel::Canvas),
+        )),
+        floating: Vec::new(),
+    })
+}
+
 /// The dock a panel belongs in: Tool Sets on the left, the palettes on the right.
 fn home_side(panel: Panel) -> (Side, f32) {
     if panel == Panel::ToolSets { (Side::Start, TOOL_SETS_WIDTH) } else { (Side::End, PALETTES_WIDTH) }
@@ -288,7 +303,7 @@ fn sync(workspace: &mut Workspace, app: &crate::CadApp) -> Result<(), String> {
     Ok(())
 }
 
-fn publish_visibility(app: &mut crate::CadApp, workspace: &Workspace) {
+pub(crate) fn publish_visibility(app: &mut crate::CadApp, workspace: &Workspace) {
     app.ui.show_toolsets = workspace.layout.contains(&Panel::ToolSets);
     app.ui.show_palettes = workspace.layout.contains(&Panel::Layers) || workspace.layout.contains(&Panel::Properties);
 }
diff --git a/crates/ui-egui/src/i18n.rs b/crates/ui-egui/src/i18n.rs
--- a/crates/ui-egui/src/i18n.rs
+++ b/crates/ui-egui/src/i18n.rs
@@ -290,6 +290,12 @@ pub fn tr<'a>(language: &str, source: &'a str) -> &'a str {
     catalog(language).and_then(|c| c.get(&("", source))).copied().unwrap_or(source)
 }
 
+/// A Ribbon label: the catalog's `ribbon` row (the wording Windows CAD ribbons use in that
+/// language), else the ordinary translation.
+pub fn ribbon(source: &str) -> &str {
+    CURRENT.with(|l| catalog(l.get()).and_then(|c| c.get(&("ribbon", source))).copied().unwrap_or_else(|| tr(l.get(), source)))
+}
+
 pub fn t(source: &str) -> &str {
     CURRENT.with(|l| tr(l.get(), source))
 }
diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv
--- a/crates/ui-egui/src/i18n/fr.tsv
+++ b/crates/ui-egui/src/i18n/fr.tsv
@@ -1015,3 +1015,177 @@
 	Check for Updates…	Rechercher des mises à jour…
 	CADCraft is up to date.	CADCraft est à jour.
 	Could not check for updates.	Impossible de rechercher les mises à jour.
+# Ribbon labels (context `ribbon`): the French wording drafting users know from Windows CAD ribbons (observed black-box, 2026-10-13).
+ribbon	Home	Début
+ribbon	Tab: Insert	Insertion
+ribbon	Insert	Insérer
+ribbon	Annotate	Annoter
+ribbon	Parametric	Paramétrique
+ribbon	View	Vue
+ribbon	Manage	Gérer
+ribbon	Output	Sortie
+ribbon	Collaborate	Collaborer
+ribbon	Draw	Dessin
+ribbon	Modify	Modification
+ribbon	Annotation	Annotation
+ribbon	Layers	Calques
+ribbon	Block	Bloc
+ribbon	Properties	Propriétés
+ribbon	Groups	Groupes
+ribbon	Utilities	Utilitaires
+ribbon	Clipboard	Presse-papiers
+ribbon	Block Definition	Définition de bloc
+ribbon	Reference	Référence
+ribbon	Import	Importer
+ribbon	Data	Données
+ribbon	Linking & Extraction	Liaison et extraction
+ribbon	Location	Position
+ribbon	Text	Texte
+ribbon	Dimensions	Cotes
+ribbon	Centerlines	Traits d'axe
+ribbon	Leaders	Lignes de repère
+ribbon	Tables	Tableaux
+ribbon	Markup	Annotation
+ribbon	Annotation Scaling	Echelle d'annotation
+ribbon	Geometric	Géométrique
+ribbon	Dimensional	Dimensionnelle
+ribbon	Viewport Tools	Outils de fenêtre
+ribbon	Named Views	Vues existantes
+ribbon	Model Viewports	Fenêtres d'espace objet
+ribbon	Review	Vérifier
+ribbon	History	Historique
+ribbon	Palettes	Palettes
+ribbon	Interface	Interface
+ribbon	Customization	Personnalisation
+ribbon	Applications	Applications
+ribbon	Cleanup	Nettoyer
+ribbon	Plot	Traceur
+ribbon	Export to DWF/PDF	Exporter au format DWF/PDF
+ribbon	Share	Partager
+ribbon	Compare	Comparer
+ribbon	Line	Ligne
+ribbon	Polyline	Polyligne
+ribbon	Circle	Cercle
+ribbon	Arc	Arc
+ribbon	Rectangle	Rectangle
+ribbon	Ellipse	Ellipse
+ribbon	Hatch	Hachures
+ribbon	Move	Déplacer
+ribbon	Copy	Copier
+ribbon	Stretch	Etirer
+ribbon	Rotate	Rotation
+ribbon	Mirror	Miroir
+ribbon	Scale	Echelle
+ribbon	Trim	Ajuster
+ribbon	Fillet	Raccord
+ribbon	Array	Réseau
+ribbon	Erase	Effacer
+ribbon	Explode	Décomposer
+ribbon	Offset	Décaler
+ribbon	Dimension	Cote
+ribbon	Linear	Linéaire
+ribbon	Leader	Ligne de repère
+ribbon	Table	Tableau
+ribbon	Layer Properties	Propriétés des calques
+ribbon	Make Current	Rendre courant
+ribbon	Match Layer	Faire correspondre le calque
+ribbon	Copy to Layer	Copier le calque
+ribbon	Create	Créer
+ribbon	Edit	Modifier
+ribbon	Edit Attributes	Modifier attributs
+ribbon	Match Properties	Copier les propriétés
+ribbon	Group	Grouper
+ribbon	Measure	Mesurer
+ribbon	Paste	Coller
+ribbon	Edit Attribute	Editer les attributs
+ribbon	Retain Attribute Display	Conserver l'affichage des attributs
+ribbon	Create Block	Créer un bloc
+ribbon	Define Attributes	Définir des attributs
+ribbon	Manage Attributes	Gérer les attributs
+ribbon	Block Editor	Editeur de blocs
+ribbon	Attach	Attacher
+ribbon	Clip	Délimiter
+ribbon	Adjust	Ajuster
+ribbon	External References	Références externes
+ribbon	Frames vary	Variation de cadres
+ribbon	Snap to Underlays	Accrochage aux fonds de plan
+ribbon	PDF Import	Importer en PDF
+ribbon	Recognize SHX Text	Reconnaître le texte SHX
+ribbon	Recognition Settings	Paramètres de reconnaissance
+ribbon	Combine Text	Combiner le texte
+ribbon	Field	Champ
+ribbon	Update Fields	Mettre à jour les champs
+ribbon	OLE Object	Objet OLE
+ribbon	Hyperlink	Hyperlien
+ribbon	Data Link	Liaison de données
+ribbon	Set Location	Définir l'emplacement
+ribbon	Multiline Text	Texte multiligne
+ribbon	Quick	Rapide
+ribbon	Continue	Continuer
+ribbon	Center Mark	Marque centrale
+ribbon	Centerline	Trait d'axe
+ribbon	Multileader	Ligne de repère multiple
+ribbon	Add Leader	Ajouter une ligne de repère
+ribbon	Remove Leader	Supprimer une ligne de repère
+ribbon	Link Data	Liaison de données
+ribbon	Wipeout	Masque
+ribbon	Revision Cloud	Nuage de révision
+ribbon	Add Current Scale	Ajouter échelle courante
+ribbon	Scale List	Liste d'échelles
+ribbon	Auto Constrain	Contrainte auto
+ribbon	Show/Hide	Afficher/Masquer
+ribbon	Show All	Tout afficher
+ribbon	Hide All	Tout masquer
+ribbon	Delete Constraints	Supprimer les contraintes
+ribbon	Parameters Manager	Gestionnaire des paramètres
+ribbon	UCS Icon	Icône du SCU
+ribbon	Navigation Cube	Cube de navigation
+ribbon	Navigation Bar	Barre de navigation
+ribbon	New View	Nouvelle vue
+ribbon	View Manager	Gestionnaire des vues
+ribbon	Viewport Configuration	Configuration de la fenêtre
+ribbon	Named	Nommé
+ribbon	Join	Joindre
+ribbon	Restore	Restaurer
+ribbon	DWG Compare	Comparaison de fichiers DWG
+ribbon	Activity Insights	Informations sur l'activité
+ribbon	Tool Palettes	Palettes d'outils
+ribbon	Blocks	Blocs
+ribbon	Count	Quantité
+ribbon	Sheet Set Manager	Gestionnaire du jeu de feuilles
+ribbon	Switch Windows	Basculer les fenêtres
+ribbon	File Tabs	Onglets Fichier
+ribbon	Layout Tabs	Onglets de présentation
+ribbon	Tile Horizontally	Mosaïque horizontale
+ribbon	Tile Vertically	Mosaïque verticale
+ribbon	Cascade	Cascade
+ribbon	User Interface	Interface utilisateur
+ribbon	Export	Exporter
+ribbon	Edit Aliases	Modifier les alias
+ribbon	Load Application	Charger une application
+ribbon	Run Script	Exécuter le script
+ribbon	Find Non-Purgeable Items	Rechercher des éléments ne pouvant pas être purgés
+ribbon	Purge	Purger
+ribbon	Overkill	Epurer
+ribbon	Audit	Contrôler
+ribbon	Batch Plot	Tracé par lot
+ribbon	Preview	Aperçu
+ribbon	Page Setup Manager	Gestionnaire des mises en page
+ribbon	View Details	Afficher les détails
+ribbon	Plotter Manager	Gestionnaire de traçage
+ribbon	Share Drawing	Partager le dessin
+ribbon	Shared Views	Vues partagées
+ribbon	Off	Désactiver
+ribbon	Isolate	Isoler
+ribbon	Freeze	Geler
+ribbon	Lock	Verrouiller
+ribbon	Unlock	Déverrouiller
+ribbon	Previous	Précédent
+ribbon	Turn All Layers On	Activer tous les calques
+ribbon	Unisolate	Annuler l'isolement
+ribbon	Thaw All Layers	Libérer tous les calques
+ribbon	Quick Select	Sélection rapide
+ribbon	Command Line	Ligne de commande
+	Ribbon	Ruban
+	Windows Layout (Ribbon)	Disposition Windows (ruban)
+	Mac Layout (Tool Sets)	Disposition Mac (palettes)
diff --git a/crates/ui-egui/src/i18n/tests.rs b/crates/ui-egui/src/i18n/tests.rs
--- a/crates/ui-egui/src/i18n/tests.rs
+++ b/crates/ui-egui/src/i18n/tests.rs
@@ -20,7 +20,7 @@ fn catalog_is_valid_and_preserves_placeholders_and_ellipses() {
         let (catalog, errors) = parse(language.source);
         assert!(errors.is_empty(), "{}: {errors:?}", language.code);
         for ((context, key), value) in catalog {
-            assert!(["", "menu", "@plural"].contains(&context), "unknown context: {context}");
+            assert!(["", "menu", "ribbon", "@plural"].contains(&context), "unknown context: {context}");
             let forms: Vec<_> = if context == "@plural" { value.split('|').collect() } else { vec![value] };
             if context == "@plural" {
                 assert_eq!(forms.len(), language.plural_forms, "{}: {key}", language.code);
diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv
--- a/crates/ui-egui/src/i18n/uk.tsv
+++ b/crates/ui-egui/src/i18n/uk.tsv
@@ -1371,3 +1371,7 @@
 	Check for Updates…	Перевірити оновлення…
 	CADCraft is up to date.	Встановлено найновішу версію CADCraft.
 	Could not check for updates.	Не вдалося перевірити оновлення.
+	Ribbon	Стрічка
+	Windows Layout (Ribbon)	Розкладка Windows (стрічка)
+	Mac Layout (Tool Sets)	Розкладка Mac (набори інструментів)
+	Ribbon Layer	Шар стрічки
diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs
--- a/crates/ui-egui/src/lib.rs
+++ b/crates/ui-egui/src/lib.rs
@@ -46,6 +46,7 @@ pub mod palettes;
 pub mod parametric;
 pub mod plotstyles;
 pub mod quick;
+pub mod ribbon;
 pub mod shortcut_menus;
 pub mod theme;
 #[cfg(not(target_arch = "wasm32"))]
@@ -97,6 +98,15 @@ pub struct UiState {
     /// menu puts them back.
     pub hidden_tools: Vec<String>,
     pub properties_all: bool,
+    /// The Ribbon above the drawing (the Windows layout).
+    pub show_ribbon: bool,
+    /// The Ribbon tab shown (`ribbon::TABS` ids).
+    pub ribbon_tab: String,
+    /// Ribbon reduced to its tab row (double-click a tab).
+    pub ribbon_minimized: bool,
+    /// The desktop app has set up the Windows layout once (Ribbon, Properties docked
+    /// left, no Tool Sets); later choices of layout are the user's (`ui.layout.pc` / `.mac`).
+    pub pc_layout_applied: bool,
     /// Show an in-window menu bar (platforms without a native menu bar, or on request).
     pub in_window_menu: bool,
     pub start_tab: bool,
@@ -178,6 +188,10 @@ impl Default for UiState {
             collapsed_groups: Vec::new(),
             hidden_tools: Vec::new(),
             properties_all: true,
+            show_ribbon: false,
+            ribbon_tab: "home".into(),
+            ribbon_minimized: false,
+            pc_layout_applied: false,
             in_window_menu: !cfg!(target_os = "macos"),
             start_tab: false,
             dialog: None,
@@ -817,9 +831,13 @@ impl CadApp {
         control::begin_frame(ui.ctx());
         let t = theme::Tokens::get();
         chrome::title_and_toolbar(self, ui);
+        // Top panels stack in call order: the menu bar sits above the Ribbon, as on Windows.
         if self.ui.in_window_menu {
             menus::menu_bar(self, ui);
         }
+        if self.ui.show_ribbon {
+            ribbon::ribbon(self, ui);
+        }
         if self.ui.show_status_bar {
             chrome::status_bar(self, ui);
         }
@@ -827,7 +845,8 @@ impl CadApp {
             chrome::file_tabs(self, ui);
         }
         let has_doc = !self.session.docs.is_empty() && !self.ui.start_tab;
-        if has_doc && !self.ui.show_toolsets {
+        // With the Ribbon the tools are in it: no collapsed Tool Sets strip.
+        if has_doc && !self.ui.show_toolsets && !self.ui.show_ribbon {
             palettes::toolsets_strip(self, ui);
         }
         egui::CentralPanel::default().frame(egui::Frame::NONE.fill(t.canvas)).show(ui, |ui| {
diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs
--- a/crates/ui-egui/src/menus.rs
+++ b/crates/ui-egui/src/menus.rs
@@ -35,6 +35,9 @@ pub const UI_COMMANDS: &[(&str, &str, &[&str], Option<&str>)] = &[
     ("ui.animations.off", "Off", &["View", "Accessibility", "Animation Effects", "Off"], None),
     ("ui.animations", "Animation Effects", &[], None),
     ("ui.toggle.toolbar", "Tool Bar", &["Window", "Tool Bar"], None),
+    ("ui.toggle.ribbon", "Ribbon", &["Window", "Ribbon"], None),
+    ("ui.layout.pc", "Windows Layout (Ribbon)", &["Window", "Windows Layout (Ribbon)"], None),
+    ("ui.layout.mac", "Mac Layout (Tool Sets)", &["Window", "Mac Layout (Tool Sets)"], None),
     ("ui.toggle.filetabs", "File Tab", &["Window", "File Tab"], None),
     ("ui.toggle.statusbar", "Status Bar", &["Window", "Status Bar"], None),
     ("ui.toggle.cmdline", "Command Line", &["Window", "Command Line"], Some("Cmd+9")),
@@ -243,6 +246,18 @@ fn run_ui_command_inner(app: &mut CadApp, id: &str, params: &Value) -> Option<Re
             toggle(&mut app.ui.show_toolbar, params);
             Ok(Value::Null)
         }
+        "ui.toggle.ribbon" => {
+            toggle(&mut app.ui.show_ribbon, params);
+            Ok(Value::Null)
+        }
+        "ui.layout.pc" => {
+            crate::ribbon::apply_pc_layout(app);
+            Ok(Value::Null)
+        }
+        "ui.layout.mac" => {
+            crate::ribbon::apply_mac_layout(app);
+            Ok(Value::Null)
+        }
         "ui.toggle.filetabs" => {
             toggle(&mut app.ui.show_file_tabs, params);
             Ok(Value::Null)
diff --git a/crates/ui-egui/src/ribbon.rs b/crates/ui-egui/src/ribbon.rs
new file mode 100644
--- /dev/null
+++ b/crates/ui-egui/src/ribbon.rs
@@ -0,0 +1,1290 @@
+//! The Ribbon: a Windows-style tabbed command bar, with tabs, panels and buttons where 2D
+//! drafting users on Windows expect them (observed black-box). Every button runs a command (`app.start`); a command
+//! CADCraft doesn't have yet shows greyed out in its place, so positions stay where users expect
+//! them. Icons are CADCraft's own (`icons.rs`); a command without one shows its initial.
+//!
+//! Panels that don't fit the window collapse from the right into a single button that opens the
+//! panel's commands as a menu, as Windows CAD ribbons do.
+
+use egui::{Align2, Color32, FontId, Rect, Sense, Stroke, pos2, vec2};
+use serde_json::json;
+
+use crate::CadApp;
+use crate::icons::{self, Icon};
+use crate::theme::Tokens;
+
+/// One ribbon command: id (as `app.start` takes it), label (English source, translated by the
+/// interface catalog) and icon.
+#[derive(Clone, Copy)]
+pub struct Cmd {
+    pub id: &'static str,
+    pub label: &'static str,
+    pub icon: Option<Icon>,
+}
+
+const fn c(id: &'static str, label: &'static str, icon: Icon) -> Cmd {
+    Cmd { id, label, icon: Some(icon) }
+}
+const fn n(id: &'static str, label: &'static str) -> Cmd {
+    Cmd { id, label, icon: None }
+}
+
+/// A current-value list in a panel (layer, colour, styles…).
+#[derive(Clone, Copy, PartialEq, Eq, Debug)]
+pub enum Widget {
+    Layer,
+    Color,
+    Lineweight,
+    Linetype,
+    TextStyle,
+    DimStyle,
+    MleaderStyle,
+    TableStyle,
+    NamedView,
+}
+
+/// A column of a panel.
+pub enum Cell {
+    /// Large button (icon above, label below); `variants` open from its lower half (▾).
+    Big(Cmd, &'static [Cmd]),
+    /// Up to three small buttons (icon, label to the right), each with optional variants.
+    Small(&'static [(Cmd, &'static [Cmd])]),
+    /// Icon-only buttons, three per column.
+    Icons(&'static [Cmd]),
+    /// Current-value lists stacked in one column of the given width.
+    Lists(&'static [Widget], f32),
+    /// A list on the first row and icon buttons on the two rows under it (the Layers panel).
+    ListIcons(Widget, f32, &'static [Cmd]),
+}
+
+pub struct PanelDef {
+    pub title: &'static str,
+    pub cells: &'static [Cell],
+    /// Commands under the panel title's ▾ (the expanded panel).
+    pub more: &'static [Cmd],
+}
+
+pub struct TabDef {
+    pub id: &'static str,
+    pub title: &'static str,
+    pub panels: &'static [PanelDef],
+}
+
+// ------------------------------------------------------------------------------------------
+// Tabs (the Windows 2D drafting layout; 3D tabs are left out on purpose)
+// ------------------------------------------------------------------------------------------
+
+const CIRCLES: &[Cmd] = &[
+    c("circle", "Center, Radius", Icon::Circle),
+    n("circle.cd", "Center, Diameter"),
+    n("circle.2p", "2-Point"),
+    n("circle.3p", "3-Point"),
+    n("circle.ttr", "Tan, Tan, Radius"),
+    n("circle.ttt", "Tan, Tan, Tan"),
+];
+const ARCS: &[Cmd] = &[
+    c("arc", "3-Point", Icon::Arc),
+    n("arc.sce", "Start, Center, End"),
+    n("arc.sca", "Start, Center, Angle"),
+    n("arc.scl", "Start, Center, Length"),
+    n("arc.sea", "Start, End, Angle"),
+    n("arc.sed", "Start, End, Direction"),
+    n("arc.ser", "Start, End, Radius"),
+    n("arc.cse", "Center, Start, End"),
+    n("arc.csa", "Center, Start, Angle"),
+    n("arc.csl", "Center, Start, Length"),
+    n("arc.continue", "Continue"),
+];
+const RECTS: &[Cmd] = &[c("rectang", "Rectangle", Icon::Rectangle), c("polygon", "Polygon", Icon::Polygon)];
+const ELLIPSES: &[Cmd] = &[c("ellipse", "Center", Icon::Ellipse), n("ellipse.axis", "Axis, End"), n("ellipse.arc", "Elliptical Arc")];
+const HATCHES: &[Cmd] = &[c("hatch", "Hatch", Icon::Hatch), c("gradient", "Gradient", Icon::Gradient), c("boundary", "Boundary", Icon::Boundary)];
+const TRIMS: &[Cmd] = &[c("trim", "Trim", Icon::Trim), c("extend", "Extend", Icon::Extend)];
+const FILLETS: &[Cmd] = &[c("fillet", "Fillet", Icon::Fillet), c("chamfer", "Chamfer", Icon::Chamfer), n("blend", "Blend Curves")];
+const ARRAYS: &[Cmd] =
+    &[c("arrayrect", "Rectangular Array", Icon::ArrayRect), n("arraypath", "Path Array"), c("arraypolar", "Polar Array", Icon::ArrayPolar)];
+const TEXTS: &[Cmd] = &[c("mtext", "Multiline Text", Icon::MText), c("text", "Single Line", Icon::Text)];
+const DIMS: &[Cmd] = &[
+    c("dimlinear", "Linear", Icon::DimLinear),
+    c("dimaligned", "Aligned", Icon::DimAligned),
+    c("dimangular", "Angular", Icon::DimAngular),
+    c("dimarc", "Arc Length", Icon::DimArc),
+    c("dimradius", "Radius", Icon::DimRadius),
+    c("dimdiameter", "Diameter", Icon::DimDiameter),
+    n("dimjogged", "Jogged"),
+    c("dimordinate", "Ordinate", Icon::DimOrdinate),
+];
+const LEADERS: &[Cmd] = &[
+    c("mleader", "Multileader", Icon::MLeader),
+    c("mleaderedit.add", "Add Leader", Icon::AddLeader),
+    c("mleaderedit.remove", "Remove Leader", Icon::RemoveLeader),
+];
+const INSERTS: &[Cmd] = &[c("insert", "Insert", Icon::Insert), c("ui.dialog.blocks", "Blocks Palette", Icon::Blocks)];
+const ATT_EDITS: &[Cmd] = &[n("eattedit", "Single"), n("-attedit", "Multiple"), n("attsync", "Synchronize")];
+const MEASURES: &[Cmd] = &[
+    c("measuregeom", "Distance", Icon::Distance),
+    n("measuregeom.radius", "Radius"),
+    n("measuregeom.angle", "Angle"),
+    c("area", "Area", Icon::Area),
+];
+const PASTES: &[Cmd] =
+    &[n("pasteclip", "Paste"), n("pasteblock", "Paste as Block"), n("pasteorig", "Paste to Original Coordinates"), n("pastespec", "Paste Special")];
+const ATT_DISPLAY: &[Cmd] =
+    &[n("attdisp.normal", "Retain Attribute Display"), n("attdisp.on", "Display All Attributes"), n("attdisp.off", "Hide All Attributes")];
+const BLOCK_CREATE: &[Cmd] = &[c("block", "Create Block", Icon::BlockCreate), n("wblock", "Write Block")];
+const PDF_IMPORTS: &[Cmd] = &[n("pdfimport", "PDF Import"), n("import", "Import")];
+const REVCLOUDS: &[Cmd] = &[
+    c("revcloud", "Rectangular", Icon::RevCloud),
+    n("revcloud.polygonal", "Polygonal"),
+    n("revcloud.freehand", "Freehand"),
+    n("revcloud.object", "Object"),
+];
+const VPORT_CONFIGS: &[Cmd] =
+    &[n("vports.1", "Single"), n("vports.2", "Two"), n("vports.3", "Three"), n("vports.4", "Four"), n("vports", "Named...")];
+const EXPORTS: &[Cmd] = &[n("exportpdf", "PDF"), n("exportdwf", "DWF"), n("exportdwfx", "DWFx"), n("export", "Other Formats")];
+const CONTINUES: &[Cmd] = &[c("dimcontinue", "Continue", Icon::DimContinue), c("dimbaseline", "Baseline", Icon::DimBaseline)];
+
+const NONE: &[Cmd] = &[];
+
+pub const TABS: &[TabDef] = &[
+    TabDef {
+        id: "home",
+        title: "Home",
+        panels: &[
+            PanelDef {
+                title: "Draw",
+                cells: &[
+                    Cell::Big(c("line", "Line", Icon::Line), NONE),
+                    Cell::Big(c("pline", "Polyline", Icon::Polyline), NONE),
+                    Cell::Big(c("circle", "Circle", Icon::Circle), CIRCLES),
+                    Cell::Big(c("arc", "Arc", Icon::Arc), ARCS),
+                    Cell::Small(&[
+                        (c("rectang", "Rectangle", Icon::Rectangle), RECTS),
+                        (c("ellipse", "Ellipse", Icon::Ellipse), ELLIPSES),
+                        (c("hatch", "Hatch", Icon::Hatch), HATCHES),
+                    ]),
+                ],
+                more: &[
+                    c("spline", "Spline Fit", Icon::Spline),
+                    c("xline", "Construction Line", Icon::XLine),
+                    c("ray", "Ray", Icon::Ray),
+                    c("point", "Multiple Points", Icon::Point),
+                    n("divide", "Divide"),
+                    n("measure", "Measure"),
+                    c("polygon", "Polygon", Icon::Polygon),
+                    c("donut", "Donut", Icon::Donut),
+                    c("region", "Region", Icon::Region),
+                    c("wipeout", "Wipeout", Icon::Wipeout),
+                    c("revcloud", "Revision Cloud", Icon::RevCloud),
+                    n("3dpoly", "3D Polyline"),
+                ],
+            },
+            PanelDef {
+                title: "Modify",
+                cells: &[
+                    Cell::Small(&[
+                        (c("move", "Move", Icon::Move), NONE),
+                        (c("copy", "Copy", Icon::Copy), NONE),
+                        (c("stretch", "Stretch", Icon::Stretch), NONE),
+                    ]),
+                    Cell::Small(&[
+                        (c("rotate", "Rotate", Icon::Rotate), NONE),
+                        (c("mirror", "Mirror", Icon::Mirror), NONE),
+                        (c("scale", "Scale", Icon::Scale), NONE),
+                    ]),
+                    Cell::Small(&[
+                        (c("trim", "Trim", Icon::Trim), TRIMS),
+                        (c("fillet", "Fillet", Icon::Fillet), FILLETS),
+                        (c("arrayrect", "Array", Icon::ArrayRect), ARRAYS),
+                    ]),
+                    Cell::Icons(&[c("erase", "Erase", Icon::Erase), c("explode", "Explode", Icon::Explode), c("offset", "Offset", Icon::Offset)]),
+                ],
+                more: &[
+                    c("break", "Break", Icon::Break),
+                    n("breakatpoint", "Break at Point"),
+                    c("join", "Join", Icon::Join),
+                    c("lengthen", "Lengthen", Icon::Lengthen),
+                    c("chamfer", "Chamfer", Icon::Chamfer),
+                    n("align", "Align"),
+                    n("reverse", "Reverse"),
+                    n("pedit", "Edit Polyline"),
+                    n("splinedit", "Edit Spline"),
+                    n("hatchedit", "Edit Hatch"),
+                    n("setbylayer", "Set to ByLayer"),
+                    n("overkill", "Delete Duplicate Objects"),
+                    n("draworder", "Draw Order"),
+                    n("flatten", "Flatten"),
+                ],
+            },
+            PanelDef {
+                title: "Annotation",
+                cells: &[
+                    Cell::Big(c("mtext", "Text", Icon::MText), TEXTS),
+                    Cell::Big(c("dim", "Dimension", Icon::DimLinear), NONE),
+                    Cell::Small(&[
+                        (c("dimlinear", "Linear", Icon::DimLinear), DIMS),
+                        (c("mleader", "Leader", Icon::MLeader), LEADERS),
+                        (c("table", "Table", Icon::Table), NONE),
+                    ]),
+                ],
+                more: &[
+                    n("style", "Text Style"),
+                    n("dimstyle", "Dimension Style"),
+                    n("mleaderstyle", "Multileader Style"),
+                    n("tablestyle", "Table Style"),
+                ],
+            },
+            PanelDef {
+                title: "Layers",
+                cells: &[
+                    Cell::Big(c("ui.dialog.layers", "Layer Properties", Icon::LayerProps), NONE),
+                    Cell::ListIcons(
+                        Widget::Layer,
+                        168.0,
+                        &[
+                            c("layoff", "Off", Icon::LayerOff),
+                            c("layiso", "Isolate", Icon::LayerIso),
+                            c("layfrz", "Freeze", Icon::LayerFreeze),
+                            c("laylck", "Lock", Icon::LayerLock),
+                            n("layon", "Turn All Layers On"),
+                            n("layuniso", "Unisolate"),
+                            n("laythw", "Thaw All Layers"),
+                            c("layulk", "Unlock", Icon::Unlock),
+                            c("layerp", "Previous", Icon::LayerPrev),
+                        ],
+                    ),
+                    Cell::Small(&[
+                        (c("laymcur", "Make Current", Icon::MakeCurrent), NONE),
+                        (c("laymch", "Match Layer", Icon::LayerMatch), NONE),
+                        (n("copytolayer", "Copy to Layer"), NONE),
+                    ]),
+                ],
+                more: &[
+                    n("layon", "Turn All Layers On"),
+                    n("laythw", "Thaw All Layers"),
+                    n("layuniso", "Unisolate"),
+                    n("laywalk", "Layer Walk"),
+                    n("laymrg", "Merge"),
+                    n("laydel", "Delete"),
+                    n("layvpi", "Isolate to Current Viewport"),
+                    n("layerstate", "Layer States"),
+                ],
+            },
+            PanelDef {
+                title: "Block",
+                cells: &[
+                    Cell::Big(c("insert", "Insert", Icon::Insert), INSERTS),
+                    Cell::Small(&[
+                        (c("block", "Create", Icon::BlockCreate), NONE),
+                        (c("bedit", "Edit", Icon::BlockEdit), NONE),
+                        (n("eattedit", "Edit Attributes"), ATT_EDITS),
+                    ]),
+                ],
+                more: &[c("attdef", "Define Attributes", Icon::AttDef), n("battman", "Manage Attributes"), n("attsync", "Synchronize Attributes")],
+            },
+            PanelDef {
+                title: "Properties",
+                cells: &[
+                    Cell::Big(c("matchprop", "Match Properties", Icon::MatchProp), NONE),
+                    Cell::Lists(&[Widget::Color, Widget::Lineweight, Widget::Linetype], 140.0),
+                ],
+                more: &[n("transparency", "Transparency"), c("list", "List", Icon::List), n("setbylayer", "Set to ByLayer")],
+            },
+            PanelDef {
+                title: "Groups",
+                cells: &[
+                    Cell::Big(n("group", "Group"), NONE),
+                    Cell::Icons(&[n("ungroup", "Ungroup"), n("groupedit", "Group Edit"), n("pickstyle", "Group Selection On/Off")]),
+                ],
+                more: NONE,
+            },
+            PanelDef {
+                title: "Utilities",
+                cells: &[
+                    Cell::Big(c("measuregeom", "Measure", Icon::Measure), MEASURES),
+                    Cell::Icons(&[
+                        c("ui.dialog.qselect", "Quick Select", Icon::QuickSelect),
+                        n("ui.panel.quickcalc", "QuickCalc"),
+                        n("id", "ID Point"),
+                    ]),
+                ],
+                more: &[n("selectall", "Select All"), n("count", "Count")],
+            },
+            PanelDef {
+                title: "Clipboard",
+                cells: &[
+                    Cell::Big(n("pasteclip", "Paste"), PASTES),
+                    Cell::Icons(&[n("copyclip", "Copy Clip"), n("cutclip", "Cut"), n("copybase", "Copy with Base Point")]),
+                ],
+                more: NONE,
+            },
+        ],
+    },
+    TabDef {
+        id: "insert",
+        title: "Insert",
+        panels: &[
+            PanelDef {
+                title: "Block",
+                cells: &[
+                    Cell::Big(c("insert", "Insert", Icon::Insert), INSERTS),
+                    Cell::Small(&[(n("eattedit", "Edit Attribute"), ATT_EDITS), (n("attdisp", "Retain Attribute Display"), ATT_DISPLAY)]),
+                ],
+                more: NONE,
+            },
+            PanelDef {
+                title: "Block Definition",
+                cells: &[
+                    Cell::Big(c("block", "Create Block", Icon::BlockCreate), BLOCK_CREATE),
+                    Cell::Big(c("attdef", "Define Attributes", Icon::AttDef), NONE),
+                    Cell::Big(n("battman", "Manage Attributes"), NONE),
+                    Cell::Big(c("bedit", "Block Editor", Icon::BlockEdit), NONE),
+                ],
+                more: NONE,
+            },
+            PanelDef {
+                title: "Reference",
+                cells: &[
+                    Cell::Big(c("attach", "Attach", Icon::Attach), NONE),
+                    Cell::Big(n("clip", "Clip"), NONE),
+                    Cell::Big(n("adjust", "Adjust"), NONE),
+                    Cell::Small(&[
+                        (n("xref", "External References"), NONE),
+                        (n("frame", "Frames vary"), NONE),
+                        (n("uosnap", "Snap to Underlays"), NONE),
+                    ]),
+                ],
+                more: &[n("xclip", "Clip Xref"), n("xbind", "Bind"), n("imageattach", "Attach Image"), n("pdfattach", "Attach PDF")],
+            },
+            PanelDef {
+                title: "Import",
+                cells: &[
+                    Cell::Big(n("pdfimport", "PDF Import"), PDF_IMPORTS),
+                    Cell::Small(&[(n("pdfshxtext", "Recognize SHX Text"), NONE), (n("pdfshxtextoptions", "Recognition Settings"), NONE)]),
+                    Cell::Big(n("txt2mtxt", "Combine Text"), NONE),
+                ],
+                more: NONE,
+            },
+            PanelDef {
+                title: "Data",
+                cells: &[
+                    Cell::Big(n("field", "Field"), NONE),
+                    Cell::Small(&[
+                        (n("updatefield", "Update Fields"), NONE),
+                        (n("insertobj", "OLE Object"), NONE),
+                        (n("hyperlink", "Hyperlink"), NONE),
+                    ]),
+                ],
+                more: NONE,
+            },
+            PanelDef {
+                title: "Linking & Extraction",
+                cells: &[
+                    Cell::Big(n("datalink", "Data Link"), NONE),
+                    Cell::Icons(&[n("dataextraction", "Extract Data"), n("datalinkupdate", "Update Data Links")]),
+                ],
+                more: NONE,
+            },
+            PanelDef { title: "Location", cells: &[Cell::Big(n("geographiclocation", "Set Location"), NONE)], more: NONE },
+        ],
+    },
+    TabDef {
+        id: "annotate",
+        title: "Annotate",
+        panels: &[
+            PanelDef {
+                title: "Text",
+                cells: &[
+                    Cell::Big(c("mtext", "Multiline Text", Icon::MText), TEXTS),
+                    Cell::Lists(&[Widget::TextStyle], 170.0),
+                    Cell::Icons(&[n("spell", "Check Spelling"), c("find", "Find Text", Icon::Search), n("scaletext", "Scale Text")]),
+                ],
+                more: &[n("justifytext", "Justify"), n("textalign", "Text Align"), n("style", "Text Style")],
+            },
+            PanelDef {
+                title: "Dimensions",
+                cells: &[
+                    Cell::Big(c("dim", "Dimension", Icon::DimLinear), NONE),
+                    Cell::Lists(&[Widget::DimStyle], 170.0),
+                    Cell::Small(&[
+                        (c("dimlinear", "Linear", Icon::DimLinear), DIMS),
+                        (c("qdim", "Quick", Icon::QDim), NONE),
+                        (c("dimcontinue", "Continue", Icon::DimContinue), CONTINUES),
+                    ]),
+                    Cell::Icons(&[
+                        n("dimbreak", "Break"),
+                        n("dimspace", "Adjust Space"),
+                        n("diminspect", "Inspect"),
+                        n("dimjogline", "Jog Line"),
+                        n("dimreassociate", "Reassociate"),
+                        n("dimtedit", "Text Angle"),
+                    ]),
+                ],
+                more: &[n("dimstyle", "Dimension Style"), n("dimoverride", "Override"), n("dimedit.oblique", "Oblique"), n("tolerance", "Tolerance")],
+            },
+            PanelDef {
+                title: "Centerlines",
+                cells: &[Cell::Big(n("centermark", "Center Mark"), NONE), Cell::Big(n("centerline", "Centerline"), NONE)],
+                more: NONE,
+            },
+            PanelDef {
+                title: "Leaders",
+                cells: &[
+                    Cell::Big(c("mleader", "Multileader", Icon::MLeader), NONE),
+                    Cell::Lists(&[Widget::MleaderStyle], 160.0),
+                    Cell::Small(&[
+                        (c("mleaderedit.add", "Add Leader", Icon::AddLeader), NONE),
+                        (c("mleaderedit.remove", "Remove Leader", Icon::RemoveLeader), NONE),
+                    ]),
+                    Cell::Icons(&[c("mleaderalign", "Align", Icon::AlignLeaders), n("mleadercollect", "Collect")]),
+                ],
+                more: &[n("mleaderstyle", "Multileader Style")],
+            },
+            PanelDef {
+                title: "Tables",
+                cells: &[
+                    Cell::Big(c("table", "Table", Icon::Table), NONE),
+                    Cell::Lists(&[Widget::TableStyle], 150.0),
+                    Cell::Small(&[(n("datalink", "Link Data"), NONE), (n("dataextraction", "Extract Data"), NONE)]),
+                ],
+                more: &[n("tablestyle", "Table Style"), n("tableexport", "Export")],
+            },
+            PanelDef {
+                title: "Markup",
+                cells: &[
+                    Cell::Big(c("wipeout", "Wipeout", Icon::Wipeout), NONE),
+                    Cell::Big(c("revcloud", "Revision Cloud", Icon::RevCloud), REVCLOUDS),
+                ],
+                more: NONE,
+            },
+            PanelDef {
+                title: "Annotation Scaling",
+                cells: &[
+                    Cell::Big(c("objectscale", "Add Current Scale", Icon::Annotation), NONE),
+                    Cell::Small(&[(n("scalelistedit", "Scale List"), NONE), (n("annoupdate", "Sync Scale Positions"), NONE)]),
+                ],
+                more: NONE,
+            },
+        ],
+    },
+    TabDef {
+        id: "parametric",
+        title: "Parametric",
+        panels: &[
+            PanelDef {
+                title: "Geometric",
+                cells: &[
+                    Cell::Big(c("autoconstrain", "Auto Constrain", Icon::Constraint), NONE),
+                    Cell::Icons(&[
+                        c("gccoincident", "Coincident", Icon::Coincident),
+                        c("gcparallel", "Parallel", Icon::Parallel),
+                        c("gcperpendicular", "Perpendicular", Icon::Perpendicular),
+                    ]),
+                    Cell::Icons(&[
+                        c("gchorizontal", "Horizontal", Icon::Horizontal),
+                        c("gcvertical", "Vertical", Icon::Vertical),
+                        c("gctangent", "Tangent", Icon::Tangent),
+                    ]),
+                    Cell::Icons(&[
+                        c("gcconcentric", "Concentric", Icon::Concentric),
+                        c("gcequal", "Equal", Icon::Equal),
+                        c("gcfix", "Fix", Icon::Fix),
+                    ]),
+                    Cell::Small(&[
+                        (n("constraintbar", "Show/Hide"), NONE),
+                        (n("constraintbar.showall", "Show All"), NONE),
+                        (n("constraintbar.hideall", "Hide All"), NONE),
+                    ]),
+                ],
+                more: NONE,
+            },
+            PanelDef {
+                title: "Dimensional",
+                cells: &[
+                    Cell::Small(&[(n("dclinear", "Linear"), NONE), (n("dcaligned", "Aligned"), NONE), (n("dcangular", "Angular"), NONE)]),
+                    Cell::Small(&[(n("dcradius", "Radius"), NONE), (n("dcdiameter", "Diameter"), NONE), (n("dcconvert", "Convert"), NONE)]),
+                    Cell::Small(&[
+                        (n("dcdisplay", "Show/Hide"), NONE),
+                        (n("dcdisplay.showall", "Show All"), NONE),
+                        (n("dcdisplay.hideall", "Hide All"), NONE),
+                    ]),
+                ],
+                more: NONE,
+            },
+            PanelDef {
+                title: "Manage",
+                cells: &[Cell::Big(n("delconstraint", "Delete Constraints"), NONE), Cell::Big(n("ui.dialog.parameters", "Parameters Manager"), NONE)],
+                more: NONE,
+            },
+        ],
+    },
+    TabDef {
+        id: "view",
+        title: "View",
+        panels: &[
+            PanelDef {
+                title: "Viewport Tools",
+                cells: &[
+                    Cell::Big(n("ui.toggle.ucsicon", "UCS Icon"), NONE),
+                    Cell::Big(n("ui.toggle.viewcube", "Navigation Cube"), NONE),
+                    Cell::Big(n("navbar", "Navigation Bar"), NONE),
+                ],
+                more: NONE,
+            },
+            PanelDef {
+                title: "Named Views",
+                cells: &[Cell::Lists(&[Widget::NamedView], 170.0), Cell::Small(&[(n("view", "New View"), NONE), (n("view", "View Manager"), NONE)])],
+                more: NONE,
+            },
+            PanelDef {
+                title: "Model Viewports",
+                cells: &[
+                    Cell::Big(n("vports", "Viewport Configuration"), VPORT_CONFIGS),
+                    Cell::Small(&[(n("vports", "Named"), NONE), (n("-vports.join", "Join"), NONE), (n("vports.1", "Restore"), NONE)]),
+                ],
+                more: NONE,
+            },
+            PanelDef { title: "Review", cells: &[Cell::Big(n("compare", "DWG Compare"), NONE)], more: NONE },
+            PanelDef { title: "History", cells: &[Cell::Big(n("activityinsights", "Activity Insights"), NONE)], more: NONE },
+            PanelDef {
+                title: "Palettes",
+                cells: &[
+                    Cell::Big(n("toolpalettes", "Tool Palettes"), NONE),
+                    Cell::Big(c("ui.panel.properties", "Properties", Icon::Properties), NONE),
+                    Cell::Big(c("ui.dialog.blocks", "Blocks", Icon::Blocks), NONE),
+                    Cell::Big(n("count", "Count"), NONE),
+                    Cell::Big(n("sheetset", "Sheet Set Manager"), NONE),
+                    Cell::Icons(&[
+                        n("ui.toggle.cmdline", "Command Line"),
+                        c("ui.panel.layers", "Layers", Icon::Layers),
+                        n("ui.panel.quickcalc", "QuickCalc"),
+                    ]),
+                    Cell::Icons(&[n("xref", "External References"), n("ui.toggle.history", "Text Window"), n("markupimport", "Markup Import")]),
+                ],
+                more: NONE,
+            },
+            PanelDef {
+                title: "Interface",
+                cells: &[
+                    Cell::Big(c("ui.switcher", "Switch Windows", Icon::Switcher), NONE),
+                    Cell::Big(n("ui.toggle.filetabs", "File Tabs"), NONE),
+                    Cell::Big(n("ui.toggle.layouttabs", "Layout Tabs"), NONE),
+                    Cell::Small(&[
+                        (n("ui.tile.horizontal", "Tile Horizontally"), NONE),
+                        (n("ui.tile.vertical", "Tile Vertically"), NONE),
+                        (n("ui.cascade", "Cascade"), NONE),
+                    ]),
+                ],
+                more: NONE,
+            },
+        ],
+    },
+    TabDef {
+        id: "manage",
+        title: "Manage",
+        panels: &[
+            PanelDef {
+                title: "Customization",
+                cells: &[
+                    Cell::Big(n("cui", "User Interface"), NONE),
+                    Cell::Big(n("toolpalettes", "Tool Palettes"), NONE),
+                    Cell::Small(&[(n("cuiimport", "Import"), NONE), (n("cuiexport", "Export"), NONE), (n("aliasedit", "Edit Aliases"), NONE)]),
+                ],
+                more: NONE,
+            },
+            PanelDef {
+                title: "Applications",
+                cells: &[Cell::Big(n("appload", "Load Application"), NONE), Cell::Big(n("script", "Run Script"), NONE)],
+                more: NONE,
+            },
+            PanelDef {
+                title: "Cleanup",
+                cells: &[
+                    Cell::Big(n("purge.find", "Find Non-Purgeable Items"), NONE),
+                    Cell::Small(&[(n("purge", "Purge"), NONE), (n("overkill", "Overkill"), NONE), (n("audit", "Audit"), NONE)]),
+                ],
+                more: NONE,
+            },
+        ],
+    },
+    TabDef {
+        id: "output",
+        title: "Output",
+        panels: &[
+            PanelDef {
+                title: "Plot",
+                cells: &[
+                    Cell::Big(c("plot", "Plot", Icon::Plot), NONE),
+                    Cell::Big(c("publish", "Batch Plot", Icon::Publish), NONE),
+                    Cell::Big(c("preview", "Preview", Icon::Preview), NONE),
+                    Cell::Small(&[
+                        (c("pagesetup", "Page Setup Manager", Icon::PageSetup), NONE),
+                        (n("viewplotdetails", "View Details"), NONE),
+                        (n("plottermanager", "Plotter Manager"), NONE),
+                    ]),
+                ],
+                more: NONE,
+            },
+            PanelDef { title: "Export to DWF/PDF", cells: &[Cell::Big(c("exportpdf", "Export", Icon::Export), EXPORTS)], more: NONE },
+        ],
+    },
+    TabDef {
+        id: "collaborate",
+        title: "Collaborate",
+        panels: &[
+            PanelDef {
+                title: "Share",
+                cells: &[Cell::Big(c("share", "Share Drawing", Icon::Share), NONE), Cell::Big(n("sharedviews", "Shared Views"), NONE)],
+                more: NONE,
+            },
+            PanelDef { title: "Compare", cells: &[Cell::Big(n("compare", "DWG Compare"), NONE)], more: NONE },
+        ],
+    },
+];
+
+// ------------------------------------------------------------------------------------------
+// Layout
+// ------------------------------------------------------------------------------------------
+
+/// Height of the tab row.
+pub const TAB_ROW: f32 = 24.0;
+/// Height of the panel body (three small-button rows) and of the panel title row under it.
+const BODY: f32 = 72.0;
+const TITLE: f32 = 18.0;
+/// Whole ribbon height when expanded.
+pub const HEIGHT: f32 = TAB_ROW + BODY + TITLE + 4.0;
+
+const ROW: f32 = BODY / 3.0;
+const BIG_ICON: f32 = 30.0;
+const SMALL_ICON: f32 = 16.0;
+const PAD: f32 = 3.0;
+
+/// Ribbon text is a little smaller than the palettes' body text.
+fn font() -> FontId {
+    FontId::proportional(11.0)
+}
+
+/// A tab's title: the catalog's `Tab: <title>` row when the tab is named differently from a
+/// button with the same English word (Insert tab, Insert button), else the plain label.
+fn tab_label(title: &str) -> String {
+    let key = format!("Tab: {title}");
+    let tab = crate::i18n::ribbon(&key);
+    if tab == key { crate::i18n::ribbon(title).to_string() } else { tab.to_string() }
+}
+
+/// Whether the command exists (else its button is greyed out).
+pub fn known(id: &str) -> bool {
+    cadcraft_engine::find_command(id).is_some() || crate::menus::is_ui_command(id)
+}
+
+fn text_w(ui: &egui::Ui, s: &str) -> f32 {
+    ui.painter().layout_no_wrap(crate::i18n::ribbon(s).to_string(), font(), Color32::WHITE).size().x
+}
+
+/// A big button's label in up to two lines (split at the space nearest the middle).
+fn two_lines(label: &str) -> (String, Option<String>) {
+    let s = crate::i18n::ribbon(label);
+    if s.chars().count() <= 10 || !s.contains(' ') {
+        return (s.to_string(), None);
+    }
+    let mid = s.len() / 2;
+    let split = s.char_indices().filter(|(_, ch)| *ch == ' ').map(|(i, _)| i).min_by_key(|i| i.abs_diff(mid)).unwrap_or(mid);
+    let (a, b) = s.split_at(split);
+    (a.to_string(), Some(b.trim_start().to_string()))
+}
+
+fn cell_width(ui: &egui::Ui, cell: &Cell) -> f32 {
+    match cell {
+        Cell::Big(cmd, variants) => {
+            let (a, b) = two_lines(cmd.label);
+            let w = |s: &str| ui.painter().layout_no_wrap(s.to_string(), font(), Color32::WHITE).size().x;
+            let lw = w(&a).max(b.as_deref().map_or(0.0, w)) + if variants.is_empty() { 0.0 } else { 10.0 };
+            lw.max(BIG_ICON + 4.0) + 2.0 * PAD
+        }
+        Cell::Small(rows) => {
+            rows.iter().map(|(cmd, v)| SMALL_ICON + 5.0 + text_w(ui, cmd.label) + if v.is_empty() { 2.0 } else { 13.0 }).fold(0.0, f32::max)
+                + 2.0 * PAD
+        }
+        Cell::Icons(cmds) => (cmds.len().div_ceil(3) as f32) * (SMALL_ICON + 8.0) + PAD,
+        Cell::Lists(_, w) | Cell::ListIcons(_, w, _) => *w + 2.0 * PAD,
+    }
+}
+
+fn panel_width(ui: &egui::Ui, panel: &PanelDef) -> f32 {
+    let cells: f32 = panel.cells.iter().map(|c| cell_width(ui, c)).sum();
+    cells.max(text_w(ui, panel.title) + if panel.more.is_empty() { 12.0 } else { 24.0 }) + PAD
+}
+
+/// Width of a collapsed panel (a single button with the panel's title).
+fn collapsed_width(ui: &egui::Ui, panel: &PanelDef) -> f32 {
+    text_w(ui, panel.title).max(BIG_ICON) + 4.0 * PAD + 10.0
+}
+
+/// Which panels are shown in full: all of them while they fit, then the rightmost ones collapse
+/// first, as Windows CAD ribbons do when the window narrows.
+pub fn fit(full: &[f32], collapsed: &[f32], room: f32) -> Vec<bool> {
+    let mut expanded = vec![true; full.len()];
+    let total = |e: &[bool]| -> f32 {
+        e.iter().enumerate().map(|(i, x)| if *x { full.get(i).copied().unwrap_or(0.0) } else { collapsed.get(i).copied().unwrap_or(0.0) }).sum()
+    };
+    let mut i = full.len();
+    while total(&expanded) > room && i > 0 {
+        i -= 1;
+        if let Some(e) = expanded.get_mut(i) {
+            *e = false;
+        }
+    }
+    expanded
+}
+
+// ------------------------------------------------------------------------------------------
+// Drawing
+// ------------------------------------------------------------------------------------------
+
+/// What a click in the ribbon asks for.
+enum Action {
+    Start(&'static str),
+    Tab(&'static str),
+    ToggleMinimized,
+    Layer(String),
+    LayerToggle(String, &'static str, bool),
+}
+
+fn hover_fill(ui: &egui::Ui, r: Rect, resp: &egui::Response) {
+    if resp.hovered() {
+        ui.painter().rect_filled(r, 3.0, Tokens::get().control_hover.gamma_multiply(0.6));
+    }
+}
+
+fn paint_icon(p: &egui::Painter, r: Rect, cmd: &Cmd, dim: bool) {
+    match cmd.icon {
+        Some(icon) => icons::paint(p, r, icon, dim),
+        None => {
+            let t = Tokens::get();
+            let col = if dim { t.text_faint } else { t.icon };
+            p.rect_stroke(r.shrink(r.width() * 0.08), 2.0, Stroke::new(1.0, col), egui::StrokeKind::Inside);
+            let initial: String = crate::i18n::ribbon(cmd.label).chars().next().map(|ch| ch.to_uppercase().collect()).unwrap_or_default();
+            p.text(r.center(), Align2::CENTER_CENTER, initial, FontId::proportional(r.height() * 0.55), col);
+        }
+    }
+}
+
+fn tooltip(cmd: &Cmd, enabled: bool) -> String {
+    let label = crate::i18n::ribbon(cmd.label);
+    if enabled { format!("{label} ({})", cmd.id.to_uppercase()) } else { format!("{label} — {}", crate::tl!("not available yet")) }
+}
+
+/// A menu of command variants (or a collapsed panel's commands).
+fn variant_menu(ui: &mut egui::Ui, items: &[Cmd], out: &mut Option<Action>) {
+    for v in items {
+        let enabled = known(v.id);
+        let resp = ui.add_enabled(enabled, egui::Button::new(crate::i18n::ribbon(v.label)));
+        crate::control::record_widget(ui, &resp, v.label, "button", None);
+        if resp.clicked() {
+            *out = Some(Action::Start(v.id));
+            ui.close();
+        }
+    }
+}
+
+fn big(ui: &mut egui::Ui, r: Rect, cmd: &Cmd, variants: &'static [Cmd], out: &mut Option<Action>) {
+    let t = Tokens::get();
+    let enabled = known(cmd.id);
+    let top = Rect::from_min_max(r.min, pos2(r.right(), r.top() + BIG_ICON + 8.0));
+    let resp = ui.interact(if variants.is_empty() { r } else { top }, ui.scope_id().with(("rb_big", cmd.id, cmd.label)), Sense::click());
+    hover_fill(ui, if variants.is_empty() { r } else { top }, &resp);
+    paint_icon(ui.painter(), Rect::from_center_size(pos2(r.center().x, r.top() + 4.0 + BIG_ICON / 2.0), vec2(BIG_ICON, BIG_ICON)), cmd, !enabled);
+    let (a, b) = two_lines(cmd.label);
+    let col = if enabled { t.text } else { t.text_faint };
+    let y = r.top() + BIG_ICON + 10.0;
+    ui.painter().text(pos2(r.center().x, y), Align2::CENTER_TOP, &a, font(), col);
+    if let Some(b) = &b {
+        ui.painter().text(pos2(r.center().x, y + 13.0), Align2::CENTER_TOP, b, font(), col);
+    }
+    icons::describe_control(ui, &resp, cmd.label, None, false);
+    if resp.on_hover_text(tooltip(cmd, enabled)).clicked() && enabled {
+        *out = Some(Action::Start(cmd.id));
+    }
+    if !variants.is_empty() {
+        let low = Rect::from_min_max(pos2(r.left(), top.bottom()), r.max);
+        let vr = ui.interact(low, ui.scope_id().with(("rb_bigv", cmd.id, cmd.label)), Sense::click());
+        hover_fill(ui, low, &vr);
+        let line_y = y + if b.is_some() { 26.0 } else { 13.0 };
+        icons::paint(ui.painter(), Rect::from_center_size(pos2(r.center().x, line_y + 4.0), vec2(9.0, 9.0)), Icon::ChevronDown, false);
+        egui::Popup::menu(&vr).show(|ui| variant_menu(ui, variants, out));
+    }
+}
+
+fn small(ui: &mut egui::Ui, r: Rect, cmd: &Cmd, variants: &'static [Cmd], out: &mut Option<Action>) {
+    let t = Tokens::get();
+    let enabled = known(cmd.id);
+    let main = if variants.is_empty() { r } else { Rect::from_min_max(r.min, pos2(r.right() - 12.0, r.bottom())) };
+    let resp = ui.interact(main, ui.scope_id().with(("rb_small", cmd.id, cmd.label, r.top() as i32)), Sense::click());
+    hover_fill(ui, main, &resp);
+    paint_icon(
+        ui.painter(),
+        Rect::from_center_size(pos2(r.left() + 2.0 + SMALL_ICON / 2.0, r.center().y), vec2(SMALL_ICON, SMALL_ICON)),
+        cmd,
+        !enabled,
+    );
+    ui.painter().text(
+        pos2(r.left() + SMALL_ICON + 6.0, r.center().y),
+        Align2::LEFT_CENTER,
+        crate::i18n::ribbon(cmd.label),
+        font(),
+        if enabled { t.text } else { t.text_faint },
+    );
+    icons::describe_control(ui, &resp, cmd.label, None, false);
+    if resp.on_hover_text(tooltip(cmd, enabled)).clicked() && enabled {
+        *out = Some(Action::Start(cmd.id));
+    }
+    if !variants.is_empty() {
+        let vr_rect = Rect::from_min_max(pos2(r.right() - 12.0, r.top()), r.max);
+        let vr = ui.interact(vr_rect, ui.scope_id().with(("rb_smallv", cmd.id, cmd.label, r.top() as i32)), Sense::click());
+        hover_fill(ui, vr_rect, &vr);
+        icons::paint(ui.painter(), Rect::from_center_size(vr_rect.center(), vec2(8.0, 8.0)), Icon::ChevronDown, false);
+        egui::Popup::menu(&vr).show(|ui| variant_menu(ui, variants, out));
+    }
+}
+
+fn icon_button(ui: &mut egui::Ui, r: Rect, cmd: &Cmd, out: &mut Option<Action>) {
+    let enabled = known(cmd.id);
+    let resp = ui.interact(r, ui.scope_id().with(("rb_icon", cmd.id, cmd.label)), Sense::click());
+    hover_fill(ui, r, &resp);
+    paint_icon(ui.painter(), Rect::from_center_size(r.center(), vec2(SMALL_ICON, SMALL_ICON)), cmd, !enabled);
+    icons::describe_control(ui, &resp, cmd.label, None, false);
+    if resp.on_hover_text(tooltip(cmd, enabled)).clicked() && enabled {
+        *out = Some(Action::Start(cmd.id));
+    }
+}
+
+/// The current value a list shows, and the command its button opens.
+fn widget_value(app: &CadApp, w: Widget) -> (String, &'static str) {
+    let Ok(d) = app.session.doc() else { return (String::new(), "ui.noop") };
+    let h = &d.header;
+    let get = |name: &str, default: &str| {
+        let v = h.str(name, default);
+        if v.is_empty() { default.to_string() } else { v }
+    };
+    match w {
+        Widget::Layer => {
+            let selection = app.session.selection();
+            let value = if selection.is_empty() {
+                h.str("CLAYER", "0")
+            } else {
+                let mut names = selection.iter().filter_map(|handle| d.entity(*handle).map(|entity| entity.common.layer.as_str()));
+                names.next().filter(|first| names.all(|name| name.eq_ignore_ascii_case(first))).unwrap_or_default().to_string()
+            };
+            (value, "ui.dialog.layers")
+        }
+        Widget::Color => (color_name(h.i64("CECOLOR", 256)), "color"),
+        Widget::Lineweight => (lineweight_name(h.i64("CELWEIGHT", -1)), "lweight"),
+        Widget::Linetype => (by_name(&get("CELTYPE", "ByLayer")), "linetype"),
+        Widget::TextStyle => (get("TEXTSTYLE", "Standard"), "style"),
+        Widget::DimStyle => (get("DIMSTYLE", "Standard"), "dimstyle"),
+        Widget::MleaderStyle => (get("CMLEADERSTYLE", "Standard"), "mleaderstyle"),
+        Widget::TableStyle => (get("CTABLESTYLE", "Standard"), "tablestyle"),
+        Widget::NamedView => (crate::tl!("Unsaved View").to_string(), "view"),
+    }
+}
+
+/// ByLayer/ByBlock in the interface language; other names as they are.
+fn by_name(name: &str) -> String {
+    if name.eq_ignore_ascii_case("ByLayer") {
+        crate::tl!("ByLayer").to_string()
+    } else if name.eq_ignore_ascii_case("ByBlock") {
+        crate::tl!("ByBlock").to_string()
+    } else {
+        name.to_string()
+    }
+}
+
+/// The usual CAD name for a colour index (CECOLOR).
+pub fn color_name(aci: i64) -> String {
+    match aci {
+        256 => crate::tl!("ByLayer").to_string(),
+        0 => crate::tl!("ByBlock").to_string(),
+        1 => crate::tl!("Red").to_string(),
+        2 => crate::tl!("Yellow").to_string(),
+        3 => crate::tl!("Green").to_string(),
+        4 => crate::tl!("Cyan").to_string(),
+        5 => crate::tl!("Blue").to_string(),
+        6 => crate::tl!("Magenta").to_string(),
+        7 => crate::tl!("White").to_string(),
+        n => format!("{} {n}", crate::tl!("Color")),
+    }
+}
+
+/// The usual CAD name for a lineweight value (CELWEIGHT, hundredths of a millimetre).
+pub fn lineweight_name(v: i64) -> String {
+    match v {
+        -1 => crate::tl!("ByLayer").to_string(),
+        -2 => crate::tl!("ByBlock").to_string(),
+        -3 => crate::tl!("Default").to_string(),
+        n if n >= 0 => format!("{:.2} mm", n as f64 / 100.0),
+        _ => crate::tl!("ByLayer").to_string(),
+    }
+}
+
+fn list(ui: &mut egui::Ui, app: &CadApp, r: Rect, w: Widget, out: &mut Option<Action>) {
+    let t = Tokens::get();
+    let (value, cmd) = widget_value(app, w);
+    let resp = ui.interact(r, ui.scope_id().with(("rb_list", w as u8)), Sense::click());
+    if w == Widget::Layer {
+        resp.widget_info(|| {
+            let mut info = egui::WidgetInfo::labeled(egui::Role::ComboBox, ui.is_enabled(), crate::tl!("Ribbon Layer"));
+            info.current_text_value = Some(value.clone());
+            info
+        });
+    }
+    ui.painter().rect_filled(r, 2.0, if resp.hovered() { t.control_hover } else { t.control });
+    let mut x = r.left() + 5.0;
+    if w == Widget::Layer
+        && let Ok(d) = app.session.doc()
+        && let Some(l) = d.layers.iter().find(|l| l.name.eq_ignore_ascii_case(&value))
+    {
+        for icon in [
+            if l.on { Icon::Bulb } else { Icon::BulbOff },
+            if l.frozen { Icon::Snowflake } else { Icon::Sun },
+            if l.locked { Icon::Lock } else { Icon::Unlock },
+        ] {
+            icons::paint(ui.painter(), Rect::from_min_size(pos2(x, r.center().y - 7.0), vec2(14.0, 14.0)), icon, false);
+            x += 16.0;
+        }
+        crate::layers::swatch(ui.painter(), Rect::from_min_size(pos2(x + 1.0, r.center().y - 5.0), vec2(10.0, 10.0)), l.color);
+        x += 16.0;
+    }
+    let galley = ui.painter().layout_no_wrap(value.clone(), font(), t.text);
+    let room = (r.right() - 16.0 - x).max(0.0);
+    let clip = Rect::from_min_max(pos2(x, r.top()), pos2(x + room, r.bottom()));
+    ui.painter().with_clip_rect(clip).galley(pos2(x, r.center().y - galley.size().y / 2.0), galley, t.text);
+    icons::paint(ui.painter(), Rect::from_center_size(pos2(r.right() - 8.0, r.center().y), vec2(9.0, 9.0)), Icon::ChevronDown, false);
+    resp.clone().on_hover_text(&value);
+    if w == Widget::Layer {
+        egui::Popup::from_toggle_button_response(&resp)
+            .width(r.width().max(220.0))
+            .close_behavior(egui::PopupCloseBehavior::CloseOnClickOutside)
+            .show(|ui| {
+                let selection = app.session.selection();
+                let editable = app.session.doc().is_ok_and(|doc| {
+                    selection
+                        .iter()
+                        .all(|handle| doc.entity(*handle).is_some_and(|entity| doc.layer(&entity.common.layer).is_none_or(|layer| !layer.locked)))
+                });
+                let current = app.session.doc().map(|doc| doc.header.str("CLAYER", "0")).unwrap_or_default();
+                let layers: Vec<(String, bool, bool, bool, cadcraft_color::Color)> = app
+                    .session
+                    .doc()
+                    .map(|d| d.layers.iter().map(|l| (l.name.clone(), l.on, l.frozen, l.locked, l.color)).collect())
+                    .unwrap_or_default();
+                egui::ScrollArea::vertical().max_height(420.0).show(ui, |ui| {
+                    for (name, on, frozen, locked, color) in &layers {
+                        ui.horizontal(|ui| {
+                            if icons::button(ui, if *on { Icon::Bulb } else { Icon::BulbOff }, 18.0, "On/Off", false).clicked() {
+                                *out = Some(Action::LayerToggle(name.clone(), "on", !*on));
+                            }
+                            if ui
+                                .add_enabled_ui(!name.eq_ignore_ascii_case(&current), |ui| {
+                                    icons::button(ui, if *frozen { Icon::Snowflake } else { Icon::Sun }, 18.0, "Freeze/Thaw", false)
+                                })
+                                .inner
+                                .clicked()
+                            {
+                                *out = Some(Action::LayerToggle(name.clone(), "frozen", !*frozen));
+                            }
+                            if icons::button(ui, if *locked { Icon::Lock } else { Icon::Unlock }, 18.0, "Lock/Unlock", false).clicked() {
+                                *out = Some(Action::LayerToggle(name.clone(), "locked", !*locked));
+                            }
+                            let (sr, _) = ui.allocate_exact_size(vec2(12.0, 12.0), Sense::hover());
+                            crate::layers::swatch(ui.painter(), sr, *color);
+                            if ui.add_enabled_ui(editable, |ui| ui.selectable_label(name.eq_ignore_ascii_case(&value), name)).inner.clicked() {
+                                *out = Some(Action::Layer(name.clone()));
+                                ui.close();
+                            }
+                        });
+                    }
+                });
+            });
+    } else if resp.clicked() && known(cmd) {
+        *out = Some(Action::Start(cmd));
+    }
+}
+
+fn draw_cell(ui: &mut egui::Ui, app: &CadApp, cell: &Cell, r: Rect, out: &mut Option<Action>) {
+    match cell {
+        Cell::Big(cmd, v) => big(ui, r.shrink2(vec2(PAD / 2.0, 0.0)), cmd, v, out),
+        Cell::Small(rows) => {
+            for (i, (cmd, v)) in rows.iter().enumerate() {
+                let rr = Rect::from_min_size(pos2(r.left() + PAD, r.top() + i as f32 * ROW + 1.0), vec2(r.width() - 2.0 * PAD, ROW - 2.0));
+                small(ui, rr, cmd, v, out);
+            }
+        }
+        Cell::Icons(cmds) => {
+            for (i, cmd) in cmds.iter().enumerate() {
+                let (col, row) = (i / 3, i % 3);
+                let rr = Rect::from_min_size(
+                    pos2(r.left() + col as f32 * (SMALL_ICON + 8.0), r.top() + row as f32 * ROW + 1.0),
+                    vec2(SMALL_ICON + 6.0, ROW - 2.0),
+                );
+                icon_button(ui, rr, cmd, out);
+            }
+        }
+        Cell::Lists(ws, w) => {
+            for (i, wd) in ws.iter().enumerate() {
+                let rr = Rect::from_min_size(pos2(r.left() + PAD, r.top() + i as f32 * ROW + 2.0), vec2(*w, ROW - 4.0));
+                list(ui, app, rr, *wd, out);
+            }
+        }
+        Cell::ListIcons(wd, w, cmds) => {
+            list(ui, app, Rect::from_min_size(pos2(r.left() + PAD, r.top() + 2.0), vec2(*w, ROW - 4.0)), *wd, out);
+            let per_row = ((*w / (SMALL_ICON + 8.0)).floor() as usize).max(1);
+            for (i, cmd) in cmds.iter().enumerate() {
+                let (row, col) = (1 + i / per_row, i % per_row);
+                let rr = Rect::from_min_size(
+                    pos2(r.left() + PAD + col as f32 * (SMALL_ICON + 8.0), r.top() + row as f32 * ROW + 1.0),
+                    vec2(SMALL_ICON + 6.0, ROW - 2.0),
+                );
+                icon_button(ui, rr, cmd, out);
+            }
+        }
+    }
+}
+
+/// Every command of a panel, for its collapsed button's menu.
+fn panel_commands(panel: &PanelDef) -> Vec<Cmd> {
+    let mut v = Vec::new();
+    for cell in panel.cells {
+        match cell {
+            Cell::Big(cmd, _) => v.push(*cmd),
+            Cell::Small(rows) => v.extend(rows.iter().map(|(cmd, _)| *cmd)),
+            Cell::Icons(cmds) => v.extend(cmds.iter().copied()),
+            Cell::Lists(..) => {}
+            Cell::ListIcons(_, _, cmds) => v.extend(cmds.iter().copied()),
+        }
+    }
+    v.extend(panel.more.iter().copied());
+    v
+}
+
+/// The ribbon (tab row and the active tab's panels).
+pub fn ribbon(app: &mut CadApp, ui: &mut egui::Ui) {
+    let t = Tokens::get();
+    let h = if app.ui.ribbon_minimized { TAB_ROW + 2.0 } else { HEIGHT };
+    let mut out: Option<Action> = None;
+    egui::Panel::top("cc_ribbon").exact_size(h).frame(egui::Frame::NONE.fill(t.chrome)).show(ui, |ui| {
+        let r = ui.max_rect();
+        // Tabs.
+        let mut x = r.left() + 8.0;
+        let active = TABS.iter().find(|tab| tab.id == app.ui.ribbon_tab).map_or("home", |tab| tab.id);
+        for tab in TABS {
+            let label = tab_label(tab.title);
+            let w = ui.painter().layout_no_wrap(label.clone(), crate::theme::body(), t.text).size().x + 18.0;
+            let tr = Rect::from_min_size(pos2(x, r.top()), vec2(w, TAB_ROW));
+            let resp = ui.interact(tr, ui.scope_id().with(("rb_tab", tab.id)), Sense::click());
+            let is_active = tab.id == active;
+            if is_active && !app.ui.ribbon_minimized {
+                ui.painter().rect_filled(tr, egui::CornerRadius { nw: 3, ne: 3, sw: 0, se: 0 }, t.panel);
+            } else if resp.hovered() {
+                ui.painter().rect_filled(tr, 3.0, t.control_hover.gamma_multiply(0.5));
+            }
+            ui.painter().text(tr.center(), Align2::CENTER_CENTER, label, crate::theme::body(), if is_active { t.text } else { t.text_dim });
+            icons::describe_control(ui, &resp, tab.title, Some(is_active), true);
+            if resp.double_clicked() {
+                out = Some(Action::ToggleMinimized);
+            } else if resp.clicked() {
+                out = Some(Action::Tab(tab.id));
+            }
+            x += w + 2.0;
+        }
+        if app.ui.ribbon_minimized {
+            return;
+        }
+        // Body.
+        let body = Rect::from_min_max(pos2(r.left(), r.top() + TAB_ROW), r.max);
+        ui.painter().rect_filled(body, 0.0, t.panel);
+        ui.painter().hline(body.x_range(), body.bottom() - 0.5, Stroke::new(1.0, t.border));
+        let Some(tab) = TABS.iter().find(|tab| tab.id == active) else { return };
+        let full: Vec<f32> = tab.panels.iter().map(|p| panel_width(ui, p)).collect();
+        let collapsed: Vec<f32> = tab.panels.iter().map(|p| collapsed_width(ui, p)).collect();
+        let expanded = fit(&full, &collapsed, body.width() - 12.0);
+        let mut x = body.left() + 6.0;
+        for (i, panel) in tab.panels.iter().enumerate() {
+            let open = expanded.get(i).copied().unwrap_or(true);
+            let w = if open { full.get(i).copied().unwrap_or(0.0) } else { collapsed.get(i).copied().unwrap_or(0.0) };
+            let pr = Rect::from_min_size(pos2(x, body.top() + 2.0), vec2(w, BODY + TITLE));
+            if open {
+                let mut cx = pr.left() + PAD;
+                for cell in panel.cells {
+                    let cw = cell_width(ui, cell);
+                    draw_cell(ui, app, cell, Rect::from_min_size(pos2(cx, pr.top()), vec2(cw, BODY)), &mut out);
+                    cx += cw;
+                }
+                // Title row (with the expander when the panel has more commands).
+                let title_r = Rect::from_min_max(pos2(pr.left(), pr.bottom() - TITLE), pr.max);
+                let label = crate::i18n::ribbon(panel.title);
+                if panel.more.is_empty() {
+                    ui.painter().text(title_r.center(), Align2::CENTER_CENTER, label, font(), t.text_dim);
+                } else {
+                    let resp = ui.interact(title_r, ui.scope_id().with(("rb_more", tab.id, panel.title)), Sense::click());
+                    hover_fill(ui, title_r, &resp);
+                    let tw = text_w(ui, panel.title);
+                    ui.painter().text(pos2(title_r.center().x - 6.0, title_r.center().y), Align2::CENTER_CENTER, label, font(), t.text_dim);
+                    icons::paint(
+                        ui.painter(),
+                        Rect::from_center_size(pos2(title_r.center().x + tw / 2.0 + 2.0, title_r.center().y), vec2(8.0, 8.0)),
+                        Icon::ChevronDown,
+                        false,
+                    );
+                    icons::describe_control(ui, &resp, panel.title, None, false);
+                    egui::Popup::menu(&resp).show(|ui| variant_menu(ui, panel.more, &mut out));
+                }
+            } else {
+                let resp = ui.interact(pr, ui.scope_id().with(("rb_collapsed", tab.id, panel.title)), Sense::click());
+                hover_fill(ui, pr, &resp);
+                let first = panel_commands(panel).into_iter().next();
+                if let Some(cmd) = &first {
+                    paint_icon(
+                        ui.painter(),
+                        Rect::from_center_size(pos2(pr.center().x, pr.top() + 6.0 + BIG_ICON / 2.0), vec2(BIG_ICON, BIG_ICON)),
+                        cmd,
+                        false,
+                    );
+                }
+                ui.painter().text(
+                    pos2(pr.center().x, pr.top() + BIG_ICON + 14.0),
+                    Align2::CENTER_TOP,
+                    crate::i18n::ribbon(panel.title),
+                    font(),
+                    t.text,
+                );
+                icons::paint(ui.painter(), Rect::from_center_size(pos2(pr.center().x, pr.bottom() - 10.0), vec2(9.0, 9.0)), Icon::ChevronDown, false);
+                icons::describe_control(ui, &resp, panel.title, None, false);
+                let cmds = panel_commands(panel);
+                egui::Popup::menu(&resp).show(|ui| variant_menu(ui, &cmds, &mut out));
+            }
+            x += w;
+            // Panel separator.
+            ui.painter().vline(x, (pr.top() + 4.0)..=(pr.bottom() - 4.0), Stroke::new(1.0, t.border));
+            x += 1.0;
+        }
+    });
+    match out {
+        Some(Action::Start(id)) => app.start(id),
+        Some(Action::Tab(id)) => {
+            app.ui.ribbon_tab = id.to_string();
+            app.ui.ribbon_minimized = false;
+        }
+        Some(Action::ToggleMinimized) => app.ui.ribbon_minimized = !app.ui.ribbon_minimized,
+        Some(Action::Layer(name)) => {
+            let selection = app.session.selection();
+            let result = if selection.is_empty() {
+                app.run("layer.current", json!({ "name": name }))
+            } else {
+                app.run("properties.set", json!({ "handles": selection.iter().map(|handle| handle.hex()).collect::<Vec<_>>(), "layer": name }))
+            };
+            if let Err(error) = result {
+                app.set_status(error.to_string());
+            }
+        }
+        Some(Action::LayerToggle(name, key, value)) => {
+            let _ = app.run("layer.set", json!({ "name": name, key: value }));
+        }
+        None => {}
+    }
+}
+
+/// Switch to the Windows layout: Ribbon, quick access tool bar in the title row,
+/// Properties docked left, no Tool Sets or Layers palette.
+pub fn apply_pc_layout(app: &mut CadApp) {
+    let workspace = crate::docking::pc_workspace();
+    crate::docking::publish_visibility(app, &workspace);
+    app.ui.docking = workspace;
+    app.ui.show_ribbon = true;
+    app.ui.show_toolbar = false;
+    app.ui.pc_layout_applied = true;
+}
+
+/// Back to CADCraft's Mac layout: Tool Bar, Tool Sets, Layers and Properties
+/// palettes on the right, no Ribbon.
+pub fn apply_mac_layout(app: &mut CadApp) {
+    let workspace = crate::docking::default_workspace();
+    crate::docking::publish_visibility(app, &workspace);
+    app.ui.docking = workspace;
+    app.ui.show_ribbon = false;
+    app.ui.show_toolbar = true;
+    app.ui.pc_layout_applied = true;
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn panels_collapse_from_the_right() {
+        let full = [100.0, 100.0, 100.0];
+        let collapsed = [40.0, 40.0, 40.0];
+        assert_eq!(fit(&full, &collapsed, 400.0), vec![true, true, true]);
+        assert_eq!(fit(&full, &collapsed, 250.0), vec![true, true, false]);
+        assert_eq!(fit(&full, &collapsed, 190.0), vec![true, false, false]);
+        assert_eq!(fit(&full, &collapsed, 10.0), vec![false, false, false]);
+    }
+
+    #[test]
+    fn tabs_match_the_windows_layout_order() {
+        let titles: Vec<&str> = TABS.iter().map(|t| t.title).collect();
+        assert_eq!(titles, ["Home", "Insert", "Annotate", "Parametric", "View", "Manage", "Output", "Collaborate"]);
+        let home: Vec<&str> = TABS[0].panels.iter().map(|p| p.title).collect();
+        assert_eq!(home, ["Draw", "Modify", "Annotation", "Layers", "Block", "Properties", "Groups", "Utilities", "Clipboard"]);
+    }
+
+    /// The everyday drafting commands of the Home tab are all real commands (not greyed out).
+    #[test]
+    fn home_tab_core_commands_exist() {
+        for id in [
+            "line",
+            "pline",
+            "circle",
+            "arc",
+            "rectang",
+            "ellipse",
+            "hatch",
+            "move",
+            "copy",
+            "stretch",
+            "rotate",
+            "mirror",
+            "scale",
+            "trim",
+            "fillet",
+            "arrayrect",
+            "erase",
+            "explode",
+            "offset",
+            "mtext",
+            "dim",
+            "dimlinear",
+            "mleader",
+            "table",
+            "insert",
+            "block",
+            "matchprop",
+            "laymcur",
+            "layoff",
+            "layiso",
+            "layfrz",
+            "laylck",
+        ] {
+            assert!(known(id), "{id} is not a command");
+        }
+        for v in CIRCLES.iter().chain(ARCS).chain(ELLIPSES) {
+            assert!(known(v.id), "{} is not a command", v.id);
+        }
+    }
+
+    #[test]
+    fn colour_and_lineweight_names() {
+        assert_eq!(lineweight_name(30), "0.30 mm");
+        assert_eq!(lineweight_name(-1), "ByLayer");
+        assert_eq!(color_name(4), "Cyan");
+        assert_eq!(color_name(42), "Color 42");
+    }
+}
diff --git a/crates/ui-egui/tests/it/chrome_geometry.rs b/crates/ui-egui/tests/it/chrome_geometry.rs
--- a/crates/ui-egui/tests/it/chrome_geometry.rs
+++ b/crates/ui-egui/tests/it/chrome_geometry.rs
@@ -8,9 +8,11 @@ use egui::{Rect, Shape, vec2};
 use egui_kittest::Harness;
 use egui_kittest::kittest::Queryable;
 
-fn harness(width: f32, height: f32, scale: f32, message: bool) -> Harness<'static, CadApp> {
+/// `pc`: the Windows layout (Ribbon); else the Mac layout (Tool Bar).
+fn harness(width: f32, height: f32, scale: f32, message: bool, pc: bool) -> Harness<'static, CadApp> {
     let mut app = CadApp::new(Session::default(), Services::default());
     app.run("new", serde_json::json!({})).unwrap();
+    app.run(if pc { "ui.layout.pc" } else { "ui.layout.mac" }, serde_json::json!({})).unwrap();
     app.ui.start_tab = false;
     app.ui.in_window_menu = false;
     app.integrated_titlebar = true;
@@ -46,11 +48,11 @@ fn visible_text(shape: &Shape, clip: Rect, result: &mut Vec<(String, Rect)>) {
     }
 }
 
-fn check_status_labels_case(width: f32, height: f32, scale: f32, message: bool, overlaps: &mut Vec<String>) {
-    let mut h = harness(width, height, scale, message);
+fn check_status_labels_case(width: f32, height: f32, scale: f32, message: bool, pc: bool, overlaps: &mut Vec<String>) {
+    let mut h = harness(width, height, scale, message, pc);
     if let Ok(directory) = std::env::var("CRAFT_UI_VISUAL_DIR") {
         std::fs::create_dir_all(&directory).unwrap();
-        h.render().unwrap().save(format!("{directory}/cad-{width}-{scale}-{message}.png")).unwrap();
+        h.render().unwrap().save(format!("{directory}/cad-{width}-{scale}-{message}-{}.png", if pc { "pc" } else { "mac" })).unwrap();
     }
     let mut texts = Vec::new();
     for shape in &h.output().shapes {
@@ -76,8 +78,8 @@ fn status_labels_never_overlap_at_standard_and_compact_sizes() {
     let mut overlaps = Vec::new();
     for (width, height) in [(1200.0, 760.0), (900.0, 560.0)] {
         for scale in [1.0, 2.0] {
-            for message in [false, true] {
-                check_status_labels_case(width, height, scale, message, &mut overlaps);
+            for (message, pc) in [(false, false), (true, false), (false, true), (true, true)] {
+                check_status_labels_case(width, height, scale, message, pc, &mut overlaps);
             }
         }
     }
@@ -87,7 +89,7 @@ fn status_labels_never_overlap_at_standard_and_compact_sizes() {
 #[test]
 fn layout_tabs_stay_anchored_when_toolsets_toggle_resize_or_move() {
     for scale in [1.0, 2.0] {
-        let mut h = harness(1200.0, 760.0, scale, false);
+        let mut h = harness(1200.0, 760.0, scale, false, false);
         let tab_rects =
             |h: &Harness<'_, CadApp>| ["Model", "Layout1", "Layout2"].map(|name| h.get_by_role_and_label(egui::accesskit::Role::Tab, name).rect());
         let original = tab_rects(&h);
@@ -114,7 +116,7 @@ fn layout_tabs_stay_anchored_when_toolsets_toggle_resize_or_move() {
 #[test]
 fn layout_tabs_and_add_button_work_with_toolsets_shown_or_hidden() {
     for visible in [true, false] {
-        let mut h = harness(1400.0, 760.0, 1.0, false);
+        let mut h = harness(1400.0, 760.0, 1.0, false, false);
         h.state_mut().run("ui.toggle.toolsets", serde_json::json!({"on":visible})).unwrap();
         h.run_steps(5);
         h.get_by_role_and_label(egui::accesskit::Role::Tab, "Layout1").click();
@@ -137,7 +139,7 @@ fn layout_tabs_and_add_button_work_with_toolsets_shown_or_hidden() {
 
 #[test]
 fn compact_toolbar_overflow_keeps_help_reachable() {
-    let mut h = harness(900.0, 560.0, 1.0, false);
+    let mut h = harness(900.0, 560.0, 1.0, false, false);
     h.get_by_label("More toolbar commands").click();
     h.run_steps(3);
     h.get_by_label("Help").click();
@@ -147,8 +149,54 @@ fn compact_toolbar_overflow_keeps_help_reachable() {
 
 #[test]
 fn toolbar_plot_preview_opens_dialog() {
-    let mut h = harness(1200.0, 760.0, 1.0, false);
+    let mut h = harness(1200.0, 760.0, 1.0, false, false);
     h.get_by_label("Plot Preview").click();
     h.run_steps(3);
     assert_eq!(h.state().ui.dialog.as_deref(), Some("preview"));
 }
+
+#[test]
+fn ribbon_output_tab_preview_opens_dialog() {
+    let mut h = harness(1200.0, 760.0, 1.0, false, true);
+    h.get_by_label("Output").click();
+    h.run_steps(3);
+    assert_eq!(h.state().ui.ribbon_tab, "output");
+    h.get_by_label("Preview").click();
+    h.run_steps(3);
+    assert_eq!(h.state().ui.dialog.as_deref(), Some("preview"));
+}
+
+#[test]
+fn ribbon_home_tab_starts_line() {
+    let mut h = harness(1200.0, 760.0, 1.0, false, true);
+    h.get_by_label("Line").click();
+    h.run_steps(3);
+    assert!(h.state().session.running.is_some(), "LINE should be prompting");
+}
+
+#[test]
+fn the_in_window_menu_bar_sits_above_the_ribbon() {
+    let mut h = harness(1200.0, 760.0, 1.0, false, true);
+    // Windows and Linux draw the menu bar in the window (macOS uses the system one).
+    h.state_mut().ui.in_window_menu = true;
+    h.run_steps(5);
+    let mut texts = Vec::new();
+    for shape in &h.output().shapes {
+        visible_text(&shape.shape, shape.clip_rect, &mut texts);
+    }
+    let top = |label: &str| texts.iter().find(|(text, _)| text == label).map(|(_, rect)| rect.top()).unwrap_or_else(|| panic!("no {label:?} drawn"));
+    // The menu bar's File, then the Ribbon's Home tab under it.
+    assert!(top("File") < top("Home"), "File at {}, Home at {}", top("File"), top("Home"));
+}
+
+#[test]
+fn the_windows_layout_has_no_collapsed_tool_sets_strip() {
+    // The Ribbon holds the tools: no 22 px Tool Sets strip beside the Properties palette.
+    let h = harness(1200.0, 760.0, 1.0, false, true);
+    assert!(h.query_by_label("Expand Tool Sets").is_none());
+    // The Mac layout with Tool Sets hidden keeps its strip to bring them back.
+    let mut h = harness(1200.0, 760.0, 1.0, false, false);
+    h.state_mut().ui.show_toolsets = false;
+    h.run_steps(3);
+    assert!(h.query_by_label("Expand Tool Sets").is_some());
+}
diff --git a/crates/ui-egui/tests/it/main.rs b/crates/ui-egui/tests/it/main.rs
--- a/crates/ui-egui/tests/it/main.rs
+++ b/crates/ui-egui/tests/it/main.rs
@@ -24,6 +24,7 @@ mod parameters_rename;
 mod pickdrag;
 mod properties_units;
 mod rapid_clicks;
+mod ribbon_layers;
 mod selection_cycling;
 mod shared_controls;
 mod shortcut_menus;
diff --git a/crates/ui-egui/tests/it/ribbon_layers.rs b/crates/ui-egui/tests/it/ribbon_layers.rs
new file mode 100644
--- /dev/null
+++ b/crates/ui-egui/tests/it/ribbon_layers.rs
@@ -0,0 +1,108 @@
+//! Reference workflow: current/selected/mixed layer control with undo and command isolation.
+use cadcraft_engine::{Input, Session, doc::Handle, geom::Vec2};
+use cadcraft_ui_egui::{CadApp, Services};
+use egui::vec2;
+use egui_kittest::{
+    Harness,
+    kittest::{NodeT, Queryable},
+};
+use serde_json::json;
+
+fn harness() -> Harness<'static, CadApp> {
+    let mut app = CadApp::new(Session::default(), Services::default());
+    app.run("new", json!({})).unwrap();
+    app.run("ui.layout.pc", json!({})).unwrap();
+    app.ui.start_tab = false;
+    app.run("layer.new", json!({"name":"CC_A","color":1})).unwrap();
+    app.run("layer.new", json!({"name":"CC_B","color":3})).unwrap();
+    let mut h = Harness::builder().with_size(vec2(1800.0, 1000.0)).build_ui_state(
+        |ui, app: &mut CadApp| {
+            app.logic(ui.ctx());
+            app.ui(ui);
+        },
+        app,
+    );
+    h.run_steps(4);
+    h
+}
+fn pick(h: &mut Harness<'_, CadApp>, name: &str) {
+    h.get_by_label("Ribbon Layer").click();
+    h.run_steps(4);
+    h.get_by_label(name).click();
+    h.run_steps(4);
+}
+fn line(h: &mut Harness<'_, CadApp>, y: i32) -> Handle {
+    let v = h.state_mut().run("line", json!({"points":[[0,y],[100,y]]})).unwrap();
+    Handle::parse_hex(v["handles"][0].as_str().unwrap()).unwrap()
+}
+fn current(h: &Harness<'_, CadApp>) -> String {
+    h.state().session.doc().unwrap().header.str("CLAYER", "0")
+}
+#[test]
+fn current_and_selected_layer_changes_are_distinct_and_undoable() {
+    let mut h = harness();
+    pick(&mut h, "CC_A");
+    assert_eq!(current(&h), "CC_A");
+    assert_eq!(h.get_by_label("Ribbon Layer").accesskit_node().value().as_deref(), Some("CC_A"));
+    let a = line(&mut h, 0);
+    pick(&mut h, "CC_B");
+    let b = line(&mut h, 40);
+    pick(&mut h, "0");
+    h.state_mut().session.set_selection(vec![a, b]);
+    h.run_steps(4);
+    // Mixed layers have no selected row; choosing one edits both objects, not CLAYER.
+    h.get_by_label("Ribbon Layer").click();
+    h.run_steps(4);
+    assert_eq!(h.get_by_label("Ribbon Layer").accesskit_node().value().unwrap_or_default(), "");
+    h.get_by_label("CC_A").click();
+    h.run_steps(4);
+    assert_eq!(current(&h), "0");
+    for handle in [a, b] {
+        assert_eq!(h.state().session.doc().unwrap().entity(handle).unwrap().common.layer, "CC_A");
+    }
+    assert_eq!(h.state().session.selection(), vec![a, b]);
+    h.state_mut().run("undo", json!({})).unwrap();
+    assert_eq!(h.state().session.doc().unwrap().entity(b).unwrap().common.layer, "CC_B");
+    assert_eq!(h.state().session.doc().unwrap().entity(a).unwrap().common.layer, "CC_A");
+    assert_eq!(current(&h), "0");
+    h.state_mut().run("redo", json!({})).unwrap();
+    assert_eq!(h.state().session.doc().unwrap().entity(b).unwrap().common.layer, "CC_A");
+}
+#[test]
+fn layer_picker_during_line_does_not_cancel_or_supply_a_point() {
+    let mut h = harness();
+    h.state_mut().session.start("line").unwrap();
+    h.state_mut().session.input(Input::Point(Vec2::new(0.0, 0.0))).unwrap();
+    h.run_steps(3);
+    pick(&mut h, "CC_B");
+    assert_eq!(h.state().session.running.as_ref().unwrap().id, "line");
+    assert_eq!(current(&h), "CC_B");
+    h.state_mut().session.input(Input::Point(Vec2::new(100.0, 0.0))).unwrap();
+    let doc = h.state().session.doc().unwrap();
+    let entities = doc.space(&cadcraft_engine::doc::Space::Model).unwrap();
+    assert_eq!(entities.len(), 1);
+    assert_eq!(entities.iter().next().unwrap().common.layer, "CC_B");
+}
+#[test]
+fn layer_state_buttons_preserve_current_layer_and_locked_objects_are_not_reassigned() {
+    let mut h = harness();
+    pick(&mut h, "CC_A");
+    let a = line(&mut h, 0);
+    pick(&mut h, "0");
+    h.state_mut().run("layer.set", json!({"name":"CC_A","locked":true})).unwrap();
+    h.state_mut().session.set_selection(vec![a]);
+    h.run_steps(4);
+    assert_eq!(h.get_by_label("Ribbon Layer").accesskit_node().value().as_deref(), Some("CC_A"));
+    h.get_by_label("Ribbon Layer").click();
+    h.run_steps(4);
+    assert!(h.get_by_label("CC_B").accesskit_node().is_disabled());
+    assert!(h.get_all_by_label("Freeze/Thaw").next().unwrap().accesskit_node().is_disabled());
+    let row_y = h.get_all_by_label("CC_A").find(|node| node.accesskit_node().is_disabled()).unwrap().rect().center().y;
+    h.get_all_by_label("Lock/Unlock").find(|node| (node.rect().center().y - row_y).abs() < 2.0).unwrap().click();
+    h.run_steps(4);
+    assert!(!h.state().session.doc().unwrap().layer("CC_A").unwrap().locked);
+    h.get_by_label("CC_B").click();
+    h.run_steps(4);
+    assert_eq!(h.state().session.doc().unwrap().entity(a).unwrap().common.layer, "CC_B");
+    assert_eq!(current(&h), "0");
+}
diff --git a/docs/ui-parity.md b/docs/ui-parity.md
--- a/docs/ui-parity.md
+++ b/docs/ui-parity.md
@@ -1,6 +1,6 @@
 # UI parity
 
-> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-22 · **Change:** minor (distinct polyline midpoint grips) · **Target:** Autodesk AutoCAD 2027 for Mac (26.0)
+> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-26 · **Change:** minor (selection-aware ribbon layer control) · **Target:** Autodesk AutoCAD 2027 for Mac (26.0)
 
 How CADCraft's interaction compares with AutoCAD's: point entry, object snaps, tracking, dynamic
 input, grips, selection, the command line, chrome and navigation. A drafter's speed in AutoCAD
@@ -161,6 +161,7 @@ down-arrow options menu (DYNPROMPT), and DYNDIM.
 | Tool Sets palette (Drafting, Modeling, … with flyouts) | yes | yes for 2D groups |
 | Properties Inspector, Quick Properties | yes | yes (every control acts or is visibly unavailable, #182); block references show read-only Position X/Y/Z from their stored insertion point; coordinate editing is still missing |
 | Layer Properties Manager (filters, states, VP overrides) | yes | yes |
+| Ribbon layer selection | Windows ribbon: current or selected-object layers | The optional Windows ribbon shows the current layer with no selection, a shared selected-object layer, or a blank mixed value. Choosing a layer edits selected objects through Properties without changing the current layer. Locked-layer selections must be unlocked before reassignment. |
 | Blocks palette, Content | yes | Blocks window (click to insert, #28) |
 | Status bar toggles | ~25 | most toggles; Annotation Visibility and Workspace do nothing |
 | ViewCube | interactive 3D | clickable for 2D (Top/Home → extents); dragging does nothing (#7) |
@@ -203,6 +204,7 @@ down-arrow options menu (DYNPROMPT), and DYNDIM.
 
 | Date | Change | Summary |
 |---|---|---|
+| 2026-10-26 | minor | Selection-aware ribbon layer display and edits reuse layer/property commands and Undo. No layout or drawing schema change. |
 | 2026-10-22 | minor | Polyline segment midpoint grips use rectangles, distinct from square vertex grips, with matching hover and pick bounds (#1950) |
 | 2026-10-13 | minor | Filled-region highlight boundaries are traversed in deterministic coordinate order, fixing dash-phase jumps on CPU hover/selection redraws and GPU highlight rebuilds; CPU dash-position, pixel and GPU mesh regressions |
 | 2026-10-14 | minor | Selection cycling (#1143): the list and badge of SELECTIONCYCLING, Shift+Space through overlapping objects (#1145) |
diff --git a/xtask/src/parity.rs b/xtask/src/parity.rs
--- a/xtask/src/parity.rs
+++ b/xtask/src/parity.rs
@@ -136,7 +136,9 @@ mod tests {
         assert_eq!(day(20_736), "2026-10-10");
         assert_eq!(date(std::time::UNIX_EPOCH - std::time::Duration::from_secs(86_400)), "1970-01-01");
         // Past chrono's range keeps the proleptic Gregorian date, not the epoch (old `civil_from_days`).
-        assert_eq!(day(115_740_740), "318857-05-20");
+        // Checked on the day count: Windows' SystemTime ends in the year 30828, so building that
+        // SystemTime panics there.
+        assert_eq!(civil_from_days(115_740_740), (318_857, 5, 20));
         // Windows' SystemTime can't hold u64::MAX / 2 seconds, so it is only checked where it exists.
         if let Some(far) = std::time::UNIX_EPOCH.checked_add(std::time::Duration::from_secs(u64::MAX / 2)) {
             assert_eq!(date(far), "292277026596-12-04");
diff --git a/xtask/src/stats.rs b/xtask/src/stats.rs
--- a/xtask/src/stats.rs
+++ b/xtask/src/stats.rs
@@ -134,7 +134,7 @@ pub fn run(_root: &Path, exact: bool) -> Result<(), String> {
 
 #[cfg(test)]
 mod tests {
-    use super::{Counts, count_tests, walk};
+    use super::{Counts, count_tests};
 
     #[test]
     fn counts_attributes_and_proptest_blocks() {
@@ -155,7 +155,7 @@ mod tests {
         // A directory symlink pointing back at `real` would be re-scanned if followed.
         symlink(&real, base.join("alias")).unwrap();
         let mut c = Counts::default();
-        walk(&base, &mut c).unwrap();
+        super::walk(&base, &mut c).unwrap();
         assert_eq!(c.files, 1, "the aliased directory was followed and counted twice");
         std::fs::remove_dir_all(&base).ok();
     }
