diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs
--- a/crates/engine/src/lib.rs
+++ b/crates/engine/src/lib.rs
@@ -575,6 +575,7 @@ pub struct LastUsed {
 }
 
 impl Default for LastUsed {
+    /// Nothing used yet: every command starts from AutoCAD's own defaults.
     fn default() -> Self {
         LastUsed {
             hatch_select: false,
@@ -629,6 +630,8 @@ pub enum LeaderType {
 }
 
 impl Default for MLeaderOptions {
+    /// Multileaders as MLEADER draws them by default: arrowhead first, two points, a landing and
+    /// text content.
     fn default() -> Self {
         MLeaderOptions {
             order: LeaderOrder::Arrowhead,
@@ -643,6 +646,44 @@ impl Default for MLeaderOptions {
     }
 }
 
+/// Apply the user's dimension defaults to the current dimension style of a new drawing (values a
+/// style refuses are skipped).
+pub fn apply_dim_defaults(defaults: &serde_json::Map<String, serde_json::Value>, d: &mut Drawing) {
+    if defaults.is_empty() {
+        return;
+    }
+    let name = d.header.str("DIMSTYLE", "Standard");
+    if let Some(style) = d.dim_styles.iter_mut().find(|s| s.name.eq_ignore_ascii_case(&name)) {
+        let _ = style.apply_fields(defaults);
+        d.sync_dim_vars();
+    }
+}
+
+impl Session {
+    /// Keep `defaults` for new drawings; open drawings keep their styles.
+    pub fn set_dim_defaults(&mut self, defaults: serde_json::Map<String, serde_json::Value>) {
+        self.dim_defaults = defaults;
+    }
+
+    /// The profile's defaults at start-up: kept for new drawings and, when the open drawing is
+    /// the untouched first one (no file, nothing drawn or undoable), given to it too, since the
+    /// startup drawing is made before the profile is read.
+    pub fn restore_dim_defaults(&mut self, defaults: serde_json::Map<String, serde_json::Value>) {
+        self.set_dim_defaults(defaults);
+        if let [only] = self.docs.as_mut_slice()
+            && only.path.is_none()
+            && only.undo.is_empty()
+            && only.redo.is_empty()
+            && only.doc.model.is_empty()
+        {
+            let mut d = (*only.doc).clone();
+            apply_dim_defaults(&self.dim_defaults, &mut d);
+            only.doc = std::sync::Arc::new(d);
+            only.saved = only.doc.clone();
+        }
+    }
+}
+
 /// The editor session: open drawings, settings, the running command and the command log.
 pub struct Session {
     pub docs: Vec<DocState>,
@@ -697,6 +738,10 @@ pub struct Session {
     /// SIngle typed at "Select objects": the next selection ends the prompt.
     pub select_single: bool,
     pub untitled_counter: u32,
+    /// Dimension style values the user keeps for new drawings (the Options dialog's Dimensions
+    /// tab, saved in the profile): DIM* variables or style field names, applied to the current
+    /// dimension style of every drawing [`Session::new_drawing`] makes. Empty: the built-in style.
+    pub dim_defaults: serde_json::Map<String, serde_json::Value>,
     /// The last dimension created (DIMCONTINUE / DIMBASELINE).
     pub last_dim: Option<Handle>,
     /// Options remembered between invocations of interactive commands.
@@ -754,6 +799,7 @@ impl Session {
             select_mode: None,
             select_single: false,
             untitled_counter: 0,
+            dim_defaults: serde_json::Map::new(),
             last_dim: None,
             last_used: LastUsed::default(),
             clipboard_source: None,
@@ -766,7 +812,8 @@ impl Session {
     /// Append a new untitled drawing (`DrawingN`, metric or imperial default view), end a running command, make it active, bump the document epoch and return its index.
     pub fn new_drawing(&mut self, metric: bool) -> usize {
         self.untitled_counter += 1;
-        let d = if metric { Drawing::new_metric() } else { Drawing::new_imperial() };
+        let mut d = if metric { Drawing::new_metric() } else { Drawing::new_imperial() };
+        apply_dim_defaults(&self.dim_defaults, &mut d);
         let mut st = DocState::new(d, &format!("Drawing{}", self.untitled_counter), None);
         st.set_view_quiet(if metric { View { center: Vec2::new(210.0, 148.5), height: 297.0 } } else { View::default() });
         self.end_running();
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,6 +61,33 @@ fn title_left(left: f32, width: f32, text_w: f32) -> f32 {
     (left + (width - text_w) / 2.0).max(left + MAC_CONTENT_LEFT)
 }
 
+/// Width the Tool Bar leaves free at the right for the Options button.
+const OPTIONS_BUTTON_RESERVE: f32 = 40.0;
+/// The Options button's side.
+const OPTIONS_BUTTON_SIZE: f32 = 26.0;
+
+/// The Options button: a gear in the top right corner of the window that opens the Options
+/// dialog. It is drawn every frame, whatever the Tool Bar, menu bar, status bar, palettes or
+/// start page show, and nothing turns it off, so the preferences are always one click away. It
+/// sits above the panels (its own foreground area) and under dialogs' windows only by order.
+pub fn options_button(app: &mut CadApp, ctx: &egui::Context) {
+    let top = if app.integrated_titlebar { (MAC_TITLE_BAR - OPTIONS_BUTTON_SIZE) / 2.0 } else { 4.0 };
+    let mut clicked = false;
+    egui::Area::new(egui::Id::unique("cc_options_button")).order(egui::Order::Foreground).anchor(egui::Align2::RIGHT_TOP, vec2(-8.0, top)).show(
+        ctx,
+        |ui| {
+            let resp = icons::button(ui, Icon::Gear, OPTIONS_BUTTON_SIZE, "Options", false);
+            icons::describe_control(ui, &resp, "Options", None, false);
+            let mac = ctx.os().is_mac();
+            let tip = format!("{} ({})", crate::i18n::t("Options"), if mac { "⌘," } else { "OPTIONS" });
+            clicked = resp.on_hover_text(tip).clicked();
+        },
+    );
+    if clicked {
+        app.start("options");
+    }
+}
+
 /// 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();
@@ -88,23 +115,26 @@ pub fn title_and_toolbar(app: &mut CadApp, ui: &mut egui::Ui) {
             let mut clicked = None;
             let mac = ui.ctx().os().is_mac();
             let widths: Vec<_> = TOOLBAR.iter().map(|group| group.len() as f32 * (size + 6.0) - 6.0).collect();
-            let available = (row.right() - 12.0 - x).max(0.0);
+            let available = (row.right() - 12.0 - OPTIONS_BUTTON_RESERVE - x).max(0.0);
             let Some(fit) = craft_ui::layout::fit_toolbar(&widths, available, 24.0, 30.0) else {
                 // Very small windows still expose the complete command row by scrolling.
-                ui.scope_builder(egui::UiBuilder::new().max_rect(row.shrink2(vec2(4.0, 0.0))), |ui| {
-                    egui::ScrollArea::horizontal().id_salt("toolbar_small").show(ui, |ui| {
-                        ui.horizontal(|ui| {
-                            for group in TOOLBAR {
-                                for (icon, cmd, tip, _) in *group {
-                                    if icons::button(ui, *icon, size, tip, false).clicked() {
-                                        clicked = Some(*cmd);
+                ui.scope_builder(
+                    egui::UiBuilder::new().max_rect(Rect::from_min_max(row.min + vec2(4.0, 0.0), row.max - vec2(4.0 + OPTIONS_BUTTON_RESERVE, 0.0))),
+                    |ui| {
+                        egui::ScrollArea::horizontal().id_salt("toolbar_small").show(ui, |ui| {
+                            ui.horizontal(|ui| {
+                                for group in TOOLBAR {
+                                    for (icon, cmd, tip, _) in *group {
+                                        if icons::button(ui, *icon, size, tip, false).clicked() {
+                                            clicked = Some(*cmd);
+                                        }
                                     }
+                                    ui.add_space(18.0);
                                 }
-                                ui.add_space(18.0);
-                            }
+                            });
                         });
-                    });
-                });
+                    },
+                );
                 if let Some(command) = clicked {
                     app.start(command);
                 }
diff --git a/crates/ui-egui/src/control.rs b/crates/ui-egui/src/control.rs
--- a/crates/ui-egui/src/control.rs
+++ b/crates/ui-egui/src/control.rs
@@ -273,6 +273,8 @@ pub fn inspect(app: &CadApp, ctx: &egui::Context) -> Value {
     })
 }
 
+/// The key a `ui.key` request names: egui's own key names, plus the common aliases (`return`,
+/// `esc`, …).
 fn key_from(name: &str) -> Option<egui::Key> {
     egui::Key::from_name(name).or(match name.to_ascii_lowercase().as_str() {
         "enter" | "return" => Some(egui::Key::Enter),
@@ -285,6 +287,8 @@ fn key_from(name: &str) -> Option<egui::Key> {
     })
 }
 
+/// Run one control-channel request against the app and its frame: the `ui.*`, `app.*`, `cmdline.*`
+/// and `engine.*` methods listed in `METHODS`.
 pub fn handle(app: &mut CadApp, ctx: &egui::Context, req: &ControlRequest) -> Outcome {
     let p = &req.params;
     let s = |k: &str| p.get(k).and_then(Value::as_str);
@@ -445,24 +449,13 @@ pub fn handle(app: &mut CadApp, ctx: &egui::Context, req: &ControlRequest) -> Ou
             app.synthetic.push(egui::Event::Text(s("text").unwrap_or("").to_string()));
             ok(Value::Null)
         }
-        "ui.set" => {
-            let mut cur = serde_json::to_value(&app.ui).unwrap_or(json!({}));
-            if let (Some(o), Some(src)) = (cur.as_object_mut(), p.as_object()) {
-                for (k, v) in src {
-                    o.insert(k.clone(), v.clone());
-                }
-            }
-            match serde_json::from_value::<crate::UiState>(cur) {
-                Ok(u) => {
-                    if let Err(error) = crate::docking::validate(&u.docking) {
-                        return err(error);
-                    }
-                    app.ui = u;
-                    ok(serde_json::to_value(&app.ui).unwrap_or_default())
-                }
+        "ui.set" => match p.as_object() {
+            Some(src) => match app.set_ui_state(src) {
+                Ok(()) => ok(serde_json::to_value(&app.ui).unwrap_or_default()),
                 Err(e) => err(e),
-            }
-        }
+            },
+            None => ok(serde_json::to_value(&app.ui).unwrap_or_default()),
+        },
         "ui.resize" => {
             let (Some(w), Some(h)) = (f("width"), f("height")) else { return err("missing width/height") };
             ctx.send_viewport_cmd(egui::ViewportCommand::InnerSize(egui::vec2(w as f32, h as f32)));
diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv
--- a/crates/ui-egui/src/i18n/it.tsv
+++ b/crates/ui-egui/src/i18n/it.tsv
@@ -1365,3 +1365,58 @@ menu	Modify	Edita
 	Check for Updates…	Verifica aggiornamenti…
 	CADCraft is up to date.	CADCraft è aggiornato.
 	Could not check for updates.	Impossibile verificare gli aggiornamenti.
+	Above	Sopra
+	Angular precision	Precisione angolare
+	Angular units	Unità angolari
+	Architectural	Architettoniche
+	Arrow size	Dimensione delle frecce
+	Changes apply to the current dimension style at once and are kept for new drawings. Other drawings keep their own styles.	Le modifiche si applicano subito allo stile di quota corrente e vengono mantenute per i nuovi disegni. Gli altri disegni conservano i propri stili.
+	Comma (,)	Virgola (,)
+	Current dimension style	Stile di quota corrente
+	Decimal	Decimali
+	Decimal degrees	Gradi decimali
+	Decimal separator	Separatore decimale
+	Degrees, minutes, seconds	Gradi, primi, secondi
+	Edit as JSON	Modifica come JSON
+	Engineering	Ingegneristiche
+	Extension beyond dimension line	Estensione oltre la linea di quota
+	Extension line offset	Distanza della linea di estensione dall'origine
+	Forget defaults for new drawings	Dimentica i valori predefiniti per i nuovi disegni
+	Fractional	Frazionarie
+	Gradians	Gradianti
+	JIS	JIS
+	Linear scale factor	Fattore di scala lineare
+	Linear units	Unità lineari
+	No drawing is open: these values are kept for new drawings.	Nessun disegno aperto: questi valori vengono mantenuti per i nuovi disegni.
+	Not valid JSON	JSON non valido
+	Offset from dimension line	Distanza del testo dalla linea di quota
+	Outside	All'esterno
+	Overall scale	Scala complessiva
+	Period (.)	Punto (.)
+	Precision (decimal places)	Precisione (cifre decimali)
+	Prefix	Prefisso
+	Radians	Radianti
+	Round off to	Arrotonda a
+	Scientific	Scientifiche
+	Space	Spazio
+	Suffix	Suffisso
+	Suppress leading zeros	Sopprimi gli zeri iniziali
+	Suppress trailing zeros	Sopprimi gli zeri finali
+	Text position	Posizione del testo
+	The settings must be a JSON object.	Le impostazioni devono essere un oggetto JSON.
+	Windows desktop	Formato Windows
+	Zero suppression	Soppressione degli zeri
+	{n} settings	{n} impostazioni
+	Interface state	Stato dell'interfaccia
+	Panels, toolbars and recent files	Pannelli, barre degli strumenti e file recenti
+	Dimensions	Quote
+	All settings	Tutte le impostazioni
+	Files	File
+	Default template for new drawings	Modello predefinito per i nuovi disegni
+	Alternate font	Font alternativo
+	Fallback fonts	Font di riserva
+	The template is a .dwt or .dxf file that New and the first drawing at startup are made from; empty: none.	Il modello è un file .dwt o .dxf da cui vengono creati i nuovi disegni e il primo disegno all'avvio; vuoto: nessuno.
+	The alternate font replaces a text style font that can't be found; empty: the built-in drafting font.	Il font alternativo sostituisce il font di uno stile di testo che non viene trovato; vuoto: il font da disegno incorporato.
+	Fallback fonts (names separated by commas) are searched for characters a text's own font lacks.	Nei font di riserva (nomi separati da virgole) si cercano i caratteri che mancano nel font del testo.
+	The automatic save file location is on Open and Save.	Il percorso dei file di salvataggio automatico si trova in Apri e salva.
+	Unknown setting	Impostazione sconosciuta
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
@@ -59,6 +59,8 @@ fn translation_gaps() {
     }
 }
 
+/// The command labels, menu headings, tool names and object snap names that `language`'s catalog
+/// has no row for.
 fn missing_commands_menus_tools_and_snap_labels(language: &str) -> Vec<String> {
     let catalog = catalog(language).unwrap();
     let mut sources = BTreeSet::new();
@@ -81,6 +83,8 @@ fn missing_commands_menus_tools_and_snap_labels(language: &str) -> Vec<String> {
     sources.into_iter().filter(|s| !s.is_empty() && !catalog.contains_key(&("", *s))).map(str::to_string).collect()
 }
 
+/// The `tl!`/`tf!` literals of the UI files listed here that `language`'s catalog has no row for: a
+/// new label must come with its translation.
 fn missing_marked_ui_literals(language: &str) -> BTreeSet<String> {
     let catalog = catalog(language).unwrap();
     let sources = [
@@ -104,6 +108,9 @@ fn missing_marked_ui_literals(language: &str) -> BTreeSet<String> {
         include_str!("../quick.rs"),
         include_str!("../vpscale.rs"),
         include_str!("../options/dialog.rs"),
+        include_str!("../options/dimensions.rs"),
+        include_str!("../options/advanced.rs"),
+        include_str!("../options/locations.rs"),
     ];
     let mut missing = BTreeSet::new();
     for source in sources {
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,58 @@
 	Check for Updates…	Перевірити оновлення…
 	CADCraft is up to date.	Встановлено найновішу версію CADCraft.
 	Could not check for updates.	Не вдалося перевірити оновлення.
+	Above	Над лінією
+	Angular precision	Точність кутів
+	Angular units	Одиниці кутів
+	Architectural	Архітектурні
+	Arrow size	Розмір стрілок
+	Changes apply to the current dimension style at once and are kept for new drawings. Other drawings keep their own styles.	Зміни застосовуються до поточного стилю розмірів одразу й зберігаються для нових креслень. Інші креслення зберігають власні стилі.
+	Comma (,)	Кома (,)
+	Current dimension style	Поточний стиль розмірів
+	Decimal	Десяткові
+	Decimal degrees	Десяткові градуси
+	Decimal separator	Десятковий роздільник
+	Degrees, minutes, seconds	Градуси, хвилини, секунди
+	Edit as JSON	Редагувати як JSON
+	Engineering	Інженерні
+	Extension beyond dimension line	Виступ виносної лінії за розмірну
+	Extension line offset	Відступ виносної лінії від об'єкта
+	Forget defaults for new drawings	Забути типові значення для нових креслень
+	Fractional	Дробові
+	Gradians	Гради
+	JIS	JIS
+	Linear scale factor	Коефіцієнт лінійного масштабу
+	Linear units	Лінійні одиниці
+	No drawing is open: these values are kept for new drawings.	Немає відкритого креслення: ці значення зберігаються для нових креслень.
+	Not valid JSON	Некоректний JSON
+	Offset from dimension line	Відступ тексту від розмірної лінії
+	Outside	Зовні
+	Overall scale	Загальний масштаб
+	Period (.)	Крапка (.)
+	Precision (decimal places)	Точність (знаків після коми)
+	Prefix	Префікс
+	Radians	Радіани
+	Round off to	Округляти до
+	Scientific	Наукові
+	Space	Пробіл
+	Suffix	Суфікс
+	Suppress leading zeros	Прибирати початкові нулі
+	Suppress trailing zeros	Прибирати кінцеві нулі
+	Text position	Положення тексту
+	The settings must be a JSON object.	Налаштування мають бути об'єктом JSON.
+	Windows desktop	Як у Windows
+	Zero suppression	Придушення нулів
+	{n} settings	Налаштувань: {n}
+	Interface state	Стан інтерфейсу
+	Panels, toolbars and recent files	Панелі, панелі інструментів і нещодавні файли
+	Dimensions	Розміри
+	All settings	Усі налаштування
+	Files	Файли
+	Default template for new drawings	Типовий шаблон для нових креслень
+	Alternate font	Замінний шрифт
+	Fallback fonts	Резервні шрифти
+	The template is a .dwt or .dxf file that New and the first drawing at startup are made from; empty: none.	Шаблон — це файл .dwt або .dxf, з якого створюються нові креслення і перше креслення під час запуску; порожньо — без шаблону.
+	The alternate font replaces a text style font that can't be found; empty: the built-in drafting font.	Замінний шрифт заступає шрифт текстового стилю, якого не знайдено; порожньо — вбудований креслярський шрифт.
+	Fallback fonts (names separated by commas) are searched for characters a text's own font lacks.	У резервних шрифтах (назви через кому) шукаються символи, яких немає у власному шрифті тексту.
+	The automatic save file location is on Open and Save.	Папка файлів автозбереження — на вкладці «Відкриття та збереження».
+	Unknown setting	Невідоме налаштування
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
@@ -382,6 +382,22 @@ impl CadApp {
         r
     }
 
+    /// Change interface state fields by name (`ui.set` and the Options dialog's All settings tab):
+    /// `patch` is merged into [`UiState`]'s JSON and the result must still be a valid state, or
+    /// nothing changes.
+    pub fn set_ui_state(&mut self, patch: &serde_json::Map<String, Value>) -> Result<(), String> {
+        let mut cur = serde_json::to_value(&self.ui).map_err(|e| e.to_string())?;
+        if let Some(o) = cur.as_object_mut() {
+            for (k, v) in patch {
+                o.insert(k.clone(), v.clone());
+            }
+        }
+        let u: UiState = serde_json::from_value(cur).map_err(|e| e.to_string())?;
+        docking::validate(&u.docking).map_err(|e| e.to_string())?;
+        self.ui = u;
+        Ok(())
+    }
+
     /// Start a command as if typed (interactive when it has prompts).
     pub fn start(&mut self, name: &str) {
         if menus::run_ui_command(self, name, &Value::Null).is_some() {
@@ -837,6 +853,7 @@ impl CadApp {
                 chrome::start_page(self, ui);
             }
         });
+        chrome::options_button(self, ui.ctx());
         self.opening.show(ui.ctx());
         #[cfg(not(target_arch = "wasm32"))]
         updates_ui::show(self, ui.ctx());
diff --git a/crates/ui-egui/src/options/advanced.rs b/crates/ui-egui/src/options/advanced.rs
new file mode 100644
--- /dev/null
+++ b/crates/ui-egui/src/options/advanced.rs
@@ -0,0 +1,299 @@
+//! The Options dialog's All settings tab: every setting the OPTIONS JSON form reports, one row
+//! each, edited in place, and the whole document as editable JSON. Changes go through the same
+//! `options` call as the other tabs, so the values are checked as everywhere else; a refused value
+//! is shown here and nothing changes.
+
+use egui::RichText;
+use serde_json::{Map, Value};
+
+use crate::CadApp;
+use crate::theme::Tokens;
+
+/// The last refused change, kept until a change is accepted (egui temp data).
+pub(super) const ERROR: &str = "cc_options_advanced_error";
+const FILTER: &str = "cc_options_advanced_filter";
+const JSON_TEXT: &str = "cc_options_advanced_json";
+const JSON_OPEN: &str = "cc_options_advanced_json_open";
+
+/// The compact text of a value for its edit field.
+fn compact(v: &Value) -> String {
+    match v {
+        Value::String(s) => s.clone(),
+        Value::Null => String::new(),
+        other => other.to_string(),
+    }
+}
+
+/// What the text of an edit field means for a setting whose value is `old`: a string stays a
+/// string, a number or boolean is read as one, an array or object as JSON; `null` takes whatever
+/// JSON the text is, else the text. `Err` is the reason it is refused.
+pub(super) fn parse_edit(old: &Value, text: &str) -> Result<Value, String> {
+    let t = text.trim();
+    match old {
+        Value::String(_) => Ok(Value::String(text.to_string())),
+        Value::Bool(_) => match t.to_ascii_lowercase().as_str() {
+            "true" | "1" | "on" | "yes" => Ok(Value::Bool(true)),
+            "false" | "0" | "off" | "no" => Ok(Value::Bool(false)),
+            _ => Err("expected true or false".into()),
+        },
+        Value::Number(n) if n.is_i64() || n.is_u64() => {
+            t.parse::<i64>().map(Value::from).or_else(|_| t.parse::<f64>().map(Value::from)).map_err(|_| "expected a number".to_string())
+        }
+        Value::Number(_) => t.parse::<f64>().ok().filter(|x| x.is_finite()).map(Value::from).ok_or_else(|| "expected a number".to_string()),
+        Value::Array(_) | Value::Object(_) => serde_json::from_str(t).map_err(|e| format!("not valid JSON: {e}")),
+        Value::Null => {
+            if t.is_empty() {
+                Ok(Value::Null)
+            } else {
+                Ok(serde_json::from_str(t).unwrap_or_else(|_| Value::String(text.to_string())))
+            }
+        }
+    }
+}
+
+/// The settings of `new` that differ from `old` (an object each): what an edited document changes.
+pub(super) fn changed(old: &Map<String, Value>, new: &Map<String, Value>) -> Map<String, Value> {
+    new.iter().filter(|(k, v)| old.get(*k) != Some(*v)).map(|(k, v)| (k.clone(), v.clone())).collect()
+}
+
+/// What a JSON document `new` changes: settings (`report` keys) and interface state (`interface`
+/// keys), or the first key that is neither.
+pub(super) fn split_edits(
+    report: &Map<String, Value>,
+    interface: &Map<String, Value>,
+    new: &Map<String, Value>,
+) -> Result<(Map<String, Value>, Map<String, Value>), String> {
+    if let Some(key) = new.keys().find(|k| !report.contains_key(*k) && !interface.contains_key(*k)) {
+        return Err(key.clone());
+    }
+    let mut diff = changed(report, new);
+    diff.retain(|k, _| report.contains_key(k));
+    let mut ui_diff = changed(interface, new);
+    ui_diff.retain(|k, _| interface.contains_key(k));
+    Ok((diff, ui_diff))
+}
+
+/// A text field that commits when it loses focus or on Enter; `Some(text)` on a change.
+fn commit_text(ui: &mut egui::Ui, id: egui::Id, current: &str, width: f32, label: egui::Id) -> Option<String> {
+    let mut buf = ui.data(|d| d.get_temp::<String>(id)).unwrap_or_else(|| current.to_string());
+    let r = ui.add(egui::TextEdit::singleline(&mut buf).desired_width(width)).labelled_by(label);
+    if r.changed() {
+        ui.data_mut(|d| d.insert_temp(id, buf.clone()));
+    }
+    if r.lost_focus() {
+        ui.data_mut(|d| d.remove::<String>(id));
+        if buf != current {
+            return Some(buf);
+        }
+    }
+    None
+}
+
+/// Show `text` above the All settings rows until the next edit (`None` clears it): a refused value
+/// or JSON document says why.
+fn set_error(ui: &egui::Ui, text: Option<String>) {
+    let id = egui::Id::unique(ERROR);
+    ui.data_mut(|d| {
+        match text {
+            Some(t) => {
+                d.insert_temp(id, t);
+            }
+            None => d.remove::<String>(id),
+        };
+    });
+}
+
+/// Interface state fields the All settings tab leaves alone: the open dialog and the panel layout
+/// (their own commands and dialogs change them).
+const INTERFACE_SKIPPED: [&str; 2] = ["dialog", "docking"];
+
+/// The interface state (`ui.set` fields) that the options JSON form does not report already.
+fn interface_state(app: &CadApp, report: &Map<String, Value>) -> Map<String, Value> {
+    match serde_json::to_value(&app.ui) {
+        Ok(Value::Object(m)) => m.into_iter().filter(|(k, _)| !report.contains_key(k) && !INTERFACE_SKIPPED.contains(&k.as_str())).collect(),
+        _ => Map::new(),
+    }
+}
+
+/// `edits` go through the `options` call; `ui_edits` through [`CadApp::set_ui_state`].
+pub(super) fn advanced(app: &CadApp, ui: &mut egui::Ui, edits: &mut Map<String, Value>, ui_edits: &mut Map<String, Value>) {
+    let t = Tokens::get();
+    let report = match super::report(app) {
+        Value::Object(m) => m,
+        _ => Map::new(),
+    };
+    if let Some(e) = ui.data(|d| d.get_temp::<String>(egui::Id::unique(ERROR))) {
+        ui.label(RichText::new(e).color(egui::Color32::from_rgb(0xe0, 0x5a, 0x4a)));
+    }
+    let mut filter = ui.data(|d| d.get_temp::<String>(egui::Id::unique(FILTER))).unwrap_or_default();
+    ui.horizontal(|ui| {
+        ui.label(crate::tl!("Filter"));
+        if ui.add(egui::TextEdit::singleline(&mut filter).desired_width(180.0)).changed() {
+            ui.data_mut(|d| d.insert_temp(egui::Id::unique(FILTER), filter.clone()));
+        }
+        ui.label(RichText::new(crate::tf!("{n} settings", n = report.len() + interface_state(app, &report).len())).small().color(t.text_dim));
+    });
+    let needle = filter.trim().to_ascii_lowercase();
+    let interface = interface_state(app, &report);
+    ui.add_space(4.0);
+    egui::ScrollArea::vertical().id_salt("options_advanced").max_height(250.0).auto_shrink([false, true]).show(ui, |ui| {
+        super::dialog::grid(ui, "options_advanced_grid", |ui| {
+            for (key, value) in &report {
+                if !needle.is_empty() && !key.to_ascii_lowercase().contains(&needle) {
+                    continue;
+                }
+                let label = ui.label(RichText::new(key).monospace());
+                row(ui, key, label.id, value, edits);
+                ui.end_row();
+            }
+            let shown: Vec<_> = interface.iter().filter(|(k, _)| needle.is_empty() || k.to_ascii_lowercase().contains(&needle)).collect();
+            if !shown.is_empty() {
+                ui.label(RichText::new(crate::tl!("Interface state")).strong());
+                ui.label(RichText::new(crate::tl!("Panels, toolbars and recent files")).small().color(t.text_dim));
+                ui.end_row();
+                for (key, value) in shown {
+                    let label = ui.label(RichText::new(key).monospace());
+                    row(ui, key, label.id, value, ui_edits);
+                    ui.end_row();
+                }
+            }
+        });
+    });
+    ui.add_space(4.0);
+    json_editor(ui, &report, &interface, edits, ui_edits);
+}
+
+/// One setting's editor, named by its row's `label` for screen readers: a check box for a
+/// boolean, a number field for a number, else a text field.
+fn row(ui: &mut egui::Ui, key: &str, label: egui::Id, value: &Value, edits: &mut Map<String, Value>) {
+    match value {
+        Value::Bool(b) => {
+            let mut on = *b;
+            if ui.checkbox(&mut on, "").labelled_by(label).changed() {
+                edits.insert(key.into(), Value::Bool(on));
+            }
+        }
+        Value::Number(n) if n.is_i64() || n.is_u64() => {
+            let mut x = n.as_i64().unwrap_or(0);
+            if ui.add(egui::DragValue::new(&mut x).speed(1.0)).labelled_by(label).changed() {
+                edits.insert(key.into(), Value::from(x));
+            }
+        }
+        Value::Number(n) => {
+            let mut x = n.as_f64().unwrap_or(0.0);
+            if ui.add(egui::DragValue::new(&mut x).speed(0.05).max_decimals(6)).labelled_by(label).changed() {
+                edits.insert(key.into(), Value::from(x));
+            }
+        }
+        other => {
+            let id = egui::Id::unique(("cc_options_advanced_text", key));
+            if let Some(text) = commit_text(ui, id, &compact(other), 290.0, label) {
+                match parse_edit(other, &text) {
+                    Ok(v) => {
+                        edits.insert(key.into(), v);
+                    }
+                    Err(e) => set_error(ui, Some(format!("{key}: {e}"))),
+                }
+            }
+        }
+    }
+}
+
+/// The whole document as JSON to edit, settings and interface state: Apply sends what changed
+/// through `options` (settings) or `ui_edits` (interface state); an unknown key changes nothing.
+fn json_editor(
+    ui: &mut egui::Ui,
+    report: &Map<String, Value>,
+    interface: &Map<String, Value>,
+    edits: &mut Map<String, Value>,
+    ui_edits: &mut Map<String, Value>,
+) {
+    let open_id = egui::Id::unique(JSON_OPEN);
+    let text_id = egui::Id::unique(JSON_TEXT);
+    let open = ui.data(|d| d.get_temp::<bool>(open_id)).unwrap_or(false);
+    let r = egui::CollapsingHeader::new(crate::tl!("Edit as JSON")).id_salt("options_advanced_json").default_open(open).show(ui, |ui| {
+        let pretty = || {
+            let mut all = report.clone();
+            all.extend(interface.clone());
+            serde_json::to_string_pretty(&Value::Object(all)).unwrap_or_default()
+        };
+        let mut text = ui.data(|d| d.get_temp::<String>(text_id)).unwrap_or_else(pretty);
+        let edit = egui::TextEdit::multiline(&mut text).code_editor().desired_rows(6).desired_width(f32::INFINITY);
+        if ui.add(edit).changed() {
+            ui.data_mut(|d| d.insert_temp(text_id, text.clone()));
+        }
+        ui.horizontal(|ui| {
+            if ui.button(crate::tl!("Apply")).clicked() {
+                match serde_json::from_str::<Value>(&text) {
+                    Ok(Value::Object(new)) => match split_edits(report, interface, &new) {
+                        Ok((diff, ui_diff)) => {
+                            set_error(ui, None);
+                            edits.extend(diff);
+                            ui_edits.extend(ui_diff);
+                            ui.data_mut(|d| d.remove::<String>(text_id));
+                        }
+                        Err(key) => set_error(ui, Some(format!("{}: {key}", crate::tl!("Unknown setting")))),
+                    },
+                    Ok(_) => set_error(ui, Some(crate::tl!("The settings must be a JSON object.").to_string())),
+                    Err(e) => set_error(ui, Some(format!("{}: {e}", crate::tl!("Not valid JSON")))),
+                }
+            }
+            if ui.button(crate::tl!("Reload")).clicked() {
+                ui.data_mut(|d| d.remove::<String>(text_id));
+                set_error(ui, None);
+            }
+            if ui.button(crate::tl!("Copy as JSON")).clicked() {
+                ui.ctx().copy_text(pretty());
+            }
+        });
+    });
+    ui.data_mut(|d| d.insert_temp(open_id, r.openness > 0.5));
+}
+
+#[cfg(test)]
+mod tests {
+    use serde_json::json;
+
+    use super::*;
+
+    /// A text field's edit becomes the same kind of JSON value as the setting it edits (bool,
+    /// number, list), or an error the tab shows.
+    #[test]
+    fn edited_text_is_read_as_the_kind_of_value_it_replaces() {
+        assert_eq!(parse_edit(&json!("dark"), "light"), Ok(json!("light")));
+        assert_eq!(parse_edit(&json!("a"), " spaced "), Ok(json!(" spaced ")), "a string is kept as typed");
+        assert_eq!(parse_edit(&json!(true), "OFF"), Ok(json!(false)));
+        assert!(parse_edit(&json!(true), "maybe").is_err());
+        assert_eq!(parse_edit(&json!(7), "12"), Ok(json!(12)));
+        assert_eq!(parse_edit(&json!(7), "1.5"), Ok(json!(1.5)), "the engine decides whether a whole number is needed");
+        assert!(parse_edit(&json!(7), "x").is_err());
+        assert_eq!(parse_edit(&json!(0.5), "2"), Ok(json!(2.0)));
+        assert!(parse_edit(&json!(0.5), "NaN").is_err() && parse_edit(&json!(0.5), "inf").is_err());
+        assert_eq!(parse_edit(&json!([1.0, 1.0]), "[2, 3]"), Ok(json!([2, 3])));
+        assert!(parse_edit(&json!([1.0, 1.0]), "[2,").is_err());
+        assert_eq!(parse_edit(&Value::Null, ""), Ok(Value::Null), "empty means the default");
+        assert_eq!(parse_edit(&Value::Null, "#ff0000"), Ok(json!("#ff0000")));
+        assert_eq!(parse_edit(&Value::Null, "12"), Ok(json!(12)));
+    }
+
+    /// Apply sends only what changed, so an unedited document re-sets nothing.
+    #[test]
+    fn only_the_settings_that_differ_are_sent() {
+        let old = json!({ "a": 1, "b": "x", "c": null }).as_object().cloned().unwrap_or_default();
+        let new = json!({ "a": 1, "b": "y", "c": null, "d": true }).as_object().cloned().unwrap_or_default();
+        assert_eq!(Value::Object(changed(&old, &new)), json!({ "b": "y", "d": true }));
+        assert!(changed(&old, &old).is_empty());
+    }
+
+    /// Settings go to `options`, interface keys to `set_ui_state`, and a key that is neither
+    /// changes nothing.
+    #[test]
+    fn the_json_document_routes_settings_and_interface_state_and_refuses_unknown_keys() {
+        let map = |v: Value| v.as_object().cloned().unwrap_or_default();
+        let report = map(json!({ "a": 1, "b": "x" }));
+        let interface = map(json!({ "showToolbar": true }));
+        let (settings, ui) = split_edits(&report, &interface, &map(json!({ "a": 2, "b": "x", "showToolbar": false }))).unwrap();
+        assert_eq!((Value::Object(settings), Value::Object(ui)), (json!({ "a": 2 }), json!({ "showToolbar": false })));
+        assert_eq!(split_edits(&report, &interface, &map(json!({ "nope": 1 }))), Err("nope".to_string()));
+    }
+}
diff --git a/crates/ui-egui/src/options/dialog.rs b/crates/ui-egui/src/options/dialog.rs
--- a/crates/ui-egui/src/options/dialog.rs
+++ b/crates/ui-egui/src/options/dialog.rs
@@ -12,13 +12,16 @@ use crate::animation::AnimationPref;
 use crate::theme::{ThemePref, Tokens};
 
 /// The tabs: (id in the dialog name, label).
-pub(super) const TABS: [(&str, &str); 6] = [
+pub(super) const TABS: [(&str, &str); 9] = [
+    ("locations", "Files"),
     ("display", "Display"),
     ("files", "Open and Save"),
     ("drafting", "Drafting"),
+    ("dimensions", "Dimensions"),
     ("selection", "Selection"),
     ("preferences", "User Preferences"),
     ("profiles", "Profiles"),
+    ("advanced", "All settings"),
 ];
 
 const WIDTH: f32 = 580.0;
@@ -28,6 +31,9 @@ const MIN_TAB_HEIGHT: f32 = 200.0;
 pub fn dialog(app: &mut CadApp, ctx: &egui::Context, name: &str, open: &mut bool) {
     let tab = name.strip_prefix("options:").and_then(|t| TABS.iter().find(|(id, _)| *id == t)).map_or("display", |(id, _)| *id);
     let mut changes = Map::new();
+    // The All settings tab's edits run apart, so a refused value is shown in the tab.
+    let mut edits = Map::new();
+    let mut ui_edits = Map::new();
     let mut next_tab = None;
     let mut start = None;
     let mut close = false;
@@ -54,6 +60,9 @@ pub fn dialog(app: &mut CadApp, ctx: &egui::Context, name: &str, open: &mut bool
             match tab {
                 "files" => files(app, ui, &mut changes),
                 "drafting" => drafting(app, ui, &mut changes, &mut start),
+                "dimensions" => super::dimensions::dimensions(app, ui, &mut changes),
+                "locations" => super::locations::locations(ui, &mut changes),
+                "advanced" => super::advanced::advanced(app, ui, &mut edits, &mut ui_edits),
                 "selection" => selection(app, ui, &mut changes),
                 "preferences" => preferences(app, ui, &mut changes),
                 "profiles" => profiles(app, ui, &mut changes),
@@ -80,16 +89,38 @@ pub fn dialog(app: &mut CadApp, ctx: &egui::Context, name: &str, open: &mut bool
     if !changes.is_empty() {
         let _ = app.run("options", Value::Object(changes));
     }
+    if !ui_edits.is_empty() {
+        let result = app.set_ui_state(&ui_edits);
+        let error = egui::Id::unique(super::advanced::ERROR);
+        ctx.data_mut(|d| match result {
+            Ok(()) => d.remove::<String>(error),
+            Err(e) => {
+                d.insert_temp(error, e);
+            }
+        });
+    }
+    if !edits.is_empty() {
+        let error = egui::Id::unique(super::advanced::ERROR);
+        let result = app.run("options", Value::Object(edits));
+        ctx.data_mut(|d| match result {
+            Ok(_) => d.remove::<String>(error),
+            Err(e) => {
+                d.insert_temp(error, e);
+            }
+        });
+    }
     if let Some(id) = start {
         app.start(id);
     }
 }
 
-fn grid(ui: &mut egui::Ui, id: &str, add: impl FnOnce(&mut egui::Ui)) {
+/// A two-column grid of caption and control, spaced as every Options tab lays out its rows.
+pub(super) fn grid(ui: &mut egui::Ui, id: &str, add: impl FnOnce(&mut egui::Ui)) {
     egui::Grid::new(id).num_columns(2).spacing(vec2(14.0, 8.0)).show(ui, add);
 }
 
-fn hint(ui: &mut egui::Ui, text: &str) {
+/// A small dimmed line under a tab's rows explaining them.
+pub(super) fn hint(ui: &mut egui::Ui, text: &str) {
     ui.label(RichText::new(text).small().color(Tokens::get().text_dim));
 }
 
@@ -562,6 +593,8 @@ fn preferences(app: &CadApp, ui: &mut egui::Ui, changes: &mut Map<String, Value>
     hint(ui, crate::tl!("Turned off, a right-click while a command runs always opens the shortcut menu."));
 }
 
+/// Profiles tab: where the preferences are kept, Reset to Defaults and Copy as JSON; edits
+/// accumulate in `changes`.
 fn profiles(app: &CadApp, ui: &mut egui::Ui, changes: &mut Map<String, Value>) {
     ui.label(crate::tl!(
         "CADCraft keeps these preferences, object snap modes and drafting toggles for your user account and restores them when it starts."
@@ -577,3 +610,17 @@ fn profiles(app: &CadApp, ui: &mut egui::Ui, changes: &mut Map<String, Value>) {
     });
     hint(ui, crate::tl!("Reset keeps the interface language."));
 }
+
+#[cfg(test)]
+mod tests {
+    /// The tab names come from a constant list the catalog scanner can't see: each one needs its
+    /// own row in the reference (Ukrainian) and Italian catalogs.
+    #[test]
+    fn every_tab_name_is_translated() {
+        for (_, label) in super::TABS {
+            for language in ["uk", "it"] {
+                assert_ne!(crate::i18n::tr(language, label), label, "{language}: {label}");
+            }
+        }
+    }
+}
diff --git a/crates/ui-egui/src/options/dimensions.rs b/crates/ui-egui/src/options/dimensions.rs
new file mode 100644
--- /dev/null
+++ b/crates/ui-egui/src/options/dimensions.rs
@@ -0,0 +1,302 @@
+//! The Options dialog's Dimensions tab: the current dimension style's number format (units,
+//! decimal places, decimal separator, zero suppression, rounding, prefix and suffix), angular
+//! units, text and arrow sizes. A change goes into the `dimStyle` option, which sets the open
+//! drawing's current dimension style and keeps the value for the drawings made from now on.
+
+use egui::RichText;
+use serde_json::{Map, Value, json};
+
+use crate::CadApp;
+use crate::theme::Tokens;
+
+/// DIMLUNIT values and their names.
+fn linear_units() -> [(i64, &'static str); 6] {
+    [
+        (2, crate::tl!("Decimal")),
+        (1, crate::tl!("Scientific")),
+        (3, crate::tl!("Engineering")),
+        (4, crate::tl!("Architectural")),
+        (5, crate::tl!("Fractional")),
+        (6, crate::tl!("Windows desktop")),
+    ]
+}
+
+/// DIMAUNIT values and their names.
+fn angular_units() -> [(i64, &'static str); 4] {
+    [(0, crate::tl!("Decimal degrees")), (1, crate::tl!("Degrees, minutes, seconds")), (2, crate::tl!("Gradians")), (3, crate::tl!("Radians"))]
+}
+
+/// DIMTAD values and their names.
+fn text_positions() -> [(i64, &'static str); 4] {
+    [(0, crate::tl!("Centered")), (1, crate::tl!("Above")), (2, crate::tl!("Outside")), (3, crate::tl!("JIS"))]
+}
+
+/// DIMDSEP values and their names.
+fn separators() -> [(&'static str, &'static str); 3] {
+    [(".", crate::tl!("Period (.)")), (",", crate::tl!("Comma (,)")), (" ", crate::tl!("Space"))]
+}
+
+/// DIMZIN bit: suppress leading zeros / trailing zeros.
+const LEADING: i64 = 4;
+const TRAILING: i64 = 8;
+
+/// A variable's value: the open drawing's current style, else the user's default for new
+/// drawings, else the built-in style's.
+fn value(app: &CadApp, style: &Map<String, Value>, var: &str) -> Value {
+    if let Some(v) = style.get(var) {
+        return v.clone();
+    }
+    if let Some(v) = app.session.dim_defaults.get(var) {
+        return v.clone();
+    }
+    let mut built_in = serde_json::to_value(cadcraft_engine::doc::DimStyle::default()).unwrap_or(Value::Null);
+    let field = cadcraft_engine::doc::DIMVARS.iter().find(|(v, _)| *v == var).map(|(_, f)| *f).unwrap_or("");
+    built_in.get_mut(field).map(Value::take).unwrap_or(Value::Null)
+}
+
+/// A dimension variable as a whole number, a real one rounded (styles read from files may store
+/// them as reals).
+fn int(v: &Value) -> i64 {
+    v.as_i64().or_else(|| v.as_f64().map(|f| f.round() as i64)).unwrap_or(0)
+}
+
+/// A combo box over `(value, label)` pairs; a pick goes into `dim` under `var`.
+fn choice(ui: &mut egui::Ui, label: egui::Id, salt: &str, current: i64, items: &[(i64, &str)], var: &str, dim: &mut Map<String, Value>) {
+    let shown = items.iter().find(|(v, _)| *v == current).map_or_else(|| current.to_string(), |(_, l)| (*l).to_string());
+    let combo = egui::ComboBox::from_id_salt(salt).selected_text(shown).width(220.0).show_ui(ui, |ui| {
+        for (v, label) in items {
+            if ui.selectable_label(*v == current, *label).clicked() && *v != current {
+                dim.insert(var.into(), json!(v));
+            }
+        }
+    });
+    combo.response.labelled_by(label);
+}
+
+/// What a drag field commits: while it is dragged its value lives in egui's temp data under `id`
+/// (the field shows it) and nothing is sent; the value goes out once, when the drag ends or a
+/// typed value is entered, so one edit is one `dimstyle` run and one undo step.
+fn commit<N: Copy + PartialEq + Send + Sync + 'static>(
+    ui: &egui::Ui,
+    id: egui::Id,
+    r: &egui::Response,
+    held: Option<N>,
+    x: N,
+    current: N,
+) -> Option<N> {
+    if r.dragged() {
+        ui.data_mut(|d| d.insert_temp(id, x));
+        return None;
+    }
+    if held.is_some() {
+        ui.data_mut(|d| d.remove::<N>(id));
+    }
+    ((held.is_some() || r.changed()) && x != current).then_some(x)
+}
+
+/// A decimal number field; an edit goes into `dim` under `var` (see [`commit`]).
+fn number(
+    ui: &mut egui::Ui,
+    label: egui::Id,
+    current: f64,
+    range: std::ops::RangeInclusive<f64>,
+    speed: f64,
+    var: &str,
+    dim: &mut Map<String, Value>,
+) {
+    let id = egui::Id::unique(("cc_options_dim_number", var));
+    let held = ui.data(|d| d.get_temp::<f64>(id));
+    let mut x = held.unwrap_or(current);
+    let r = ui.add(egui::DragValue::new(&mut x).range(range).speed(speed).max_decimals(6).update_while_editing(false)).labelled_by(label);
+    if let Some(x) = commit(ui, id, &r, held, x, current) {
+        dim.insert(var.into(), json!(x));
+    }
+}
+
+/// A whole-number field; an edit goes into `dim` under `var` (see [`commit`]).
+fn whole(ui: &mut egui::Ui, label: egui::Id, current: i64, range: std::ops::RangeInclusive<i64>, var: &str, dim: &mut Map<String, Value>) {
+    let id = egui::Id::unique(("cc_options_dim_whole", var));
+    let held = ui.data(|d| d.get_temp::<i64>(id));
+    let mut x = held.unwrap_or(current);
+    let r = ui.add(egui::DragValue::new(&mut x).range(range).update_while_editing(false)).labelled_by(label);
+    if let Some(x) = commit(ui, id, &r, held, x, current) {
+        dim.insert(var.into(), json!(x));
+    }
+}
+
+/// A text field that commits when it loses focus; `Some(text)` on a change.
+fn text_field(ui: &mut egui::Ui, label: egui::Id, id: &str, current: &str, width: f32) -> Option<String> {
+    let id = egui::Id::unique(("cc_options_dim_text", id));
+    let mut buf = ui.data(|d| d.get_temp::<String>(id)).unwrap_or_else(|| current.to_string());
+    let r = ui.add(egui::TextEdit::singleline(&mut buf).desired_width(width)).labelled_by(label);
+    if r.changed() {
+        ui.data_mut(|d| d.insert_temp(id, buf.clone()));
+    }
+    if r.lost_focus() {
+        ui.data_mut(|d| d.remove::<String>(id));
+        if buf != current {
+            return Some(buf);
+        }
+    }
+    None
+}
+
+/// DIMPOST split into its prefix and suffix around the `<>` that stands for the measurement; a
+/// template without one is a suffix.
+pub(super) fn split_post(post: &str) -> (String, String) {
+    match post.split_once("<>") {
+        Some((a, b)) => (a.to_string(), b.to_string()),
+        None => (String::new(), post.to_string()),
+    }
+}
+
+/// The DIMPOST template of a prefix and suffix (empty when both are).
+pub(super) fn join_post(prefix: &str, suffix: &str) -> String {
+    if prefix.is_empty() && suffix.is_empty() { String::new() } else { format!("{prefix}<>{suffix}") }
+}
+
+/// Dimensions tab: the current dimension style's number format, text and arrow sizes; edits go into
+/// `changes` as one `dimStyle` change.
+pub(super) fn dimensions(app: &CadApp, ui: &mut egui::Ui, changes: &mut Map<String, Value>) {
+    let t = Tokens::get();
+    let style = super::current_dim_style(app);
+    let mut dim = Map::new();
+    match super::current_dim_style_name(app) {
+        Some(name) => {
+            ui.label(RichText::new(format!("{}: {name}", crate::tl!("Current dimension style"))).strong());
+        }
+        None => {
+            ui.label(RichText::new(crate::tl!("No drawing is open: these values are kept for new drawings.")).color(t.text_dim));
+        }
+    }
+    ui.add_space(4.0);
+    egui::ScrollArea::vertical().id_salt("options_dimensions").max_height(300.0).auto_shrink([false, true]).show(ui, |ui| {
+        super::dialog::grid(ui, "options_dimensions_grid", |ui| {
+            let v = |var: &str| value(app, &style, var);
+
+            let label = ui.label(crate::tl!("Linear units"));
+            choice(ui, label.id, "dim_lunit", int(&v("DIMLUNIT")), &linear_units(), "DIMLUNIT", &mut dim);
+            ui.end_row();
+
+            let label = ui.label(crate::tl!("Precision (decimal places)"));
+            whole(ui, label.id, int(&v("DIMDEC")), 0..=8, "DIMDEC", &mut dim);
+            ui.end_row();
+
+            let label = ui.label(crate::tl!("Decimal separator"));
+            let sep = v("DIMDSEP").as_str().unwrap_or(".").to_string();
+            let sep = if sep.is_empty() { ".".to_string() } else { sep };
+            let shown = separators().iter().find(|(c, _)| *c == sep).map_or_else(|| sep.clone(), |(_, l)| (*l).to_string());
+            let combo = egui::ComboBox::from_id_salt("dim_dsep").selected_text(shown).width(220.0).show_ui(ui, |ui| {
+                for (c, label) in separators() {
+                    if ui.selectable_label(c == sep, label).clicked() && c != sep {
+                        dim.insert("DIMDSEP".into(), json!(c));
+                    }
+                }
+            });
+            combo.response.labelled_by(label.id);
+            ui.end_row();
+
+            ui.label(crate::tl!("Zero suppression"));
+            ui.vertical(|ui| {
+                let zin = int(&v("DIMZIN"));
+                for (bit, label) in [(LEADING, crate::tl!("Suppress leading zeros")), (TRAILING, crate::tl!("Suppress trailing zeros"))] {
+                    let mut on = zin & bit != 0;
+                    if ui.checkbox(&mut on, label).changed() {
+                        dim.insert("DIMZIN".into(), json!(if on { zin | bit } else { zin & !bit }));
+                    }
+                }
+            });
+            ui.end_row();
+
+            let label = ui.label(crate::tl!("Round off to"));
+            number(ui, label.id, v("DIMRND").as_f64().unwrap_or(0.0), 0.0..=1.0e6, 0.01, "DIMRND", &mut dim);
+            ui.end_row();
+
+            let label = ui.label(crate::tl!("Linear scale factor"));
+            number(ui, label.id, v("DIMLFAC").as_f64().unwrap_or(1.0), 1.0e-6..=1.0e6, 0.01, "DIMLFAC", &mut dim);
+            ui.end_row();
+
+            let (prefix, suffix) = split_post(v("DIMPOST").as_str().unwrap_or(""));
+            let label = ui.label(crate::tl!("Prefix"));
+            if let Some(new) = text_field(ui, label.id, "prefix", &prefix, 120.0) {
+                dim.insert("DIMPOST".into(), json!(join_post(&new, &suffix)));
+            }
+            ui.end_row();
+            let label = ui.label(crate::tl!("Suffix"));
+            if let Some(new) = text_field(ui, label.id, "suffix", &suffix, 120.0) {
+                dim.insert("DIMPOST".into(), json!(join_post(&prefix, &new)));
+            }
+            ui.end_row();
+
+            let label = ui.label(crate::tl!("Angular units"));
+            choice(ui, label.id, "dim_aunit", int(&v("DIMAUNIT")), &angular_units(), "DIMAUNIT", &mut dim);
+            ui.end_row();
+
+            let label = ui.label(crate::tl!("Angular precision"));
+            whole(ui, label.id, int(&v("DIMADEC")), 0..=8, "DIMADEC", &mut dim);
+            ui.end_row();
+
+            let label = ui.label(crate::tl!("Text height"));
+            number(ui, label.id, v("DIMTXT").as_f64().unwrap_or(0.18), 1.0e-6..=1.0e6, 0.01, "DIMTXT", &mut dim);
+            ui.end_row();
+
+            let label = ui.label(crate::tl!("Text position"));
+            choice(ui, label.id, "dim_tad", int(&v("DIMTAD")), &text_positions(), "DIMTAD", &mut dim);
+            ui.end_row();
+
+            let label = ui.label(crate::tl!("Offset from dimension line"));
+            number(ui, label.id, v("DIMGAP").as_f64().unwrap_or(0.09), 0.0..=1.0e6, 0.01, "DIMGAP", &mut dim);
+            ui.end_row();
+
+            let label = ui.label(crate::tl!("Arrow size"));
+            number(ui, label.id, v("DIMASZ").as_f64().unwrap_or(0.18), 0.0..=1.0e6, 0.01, "DIMASZ", &mut dim);
+            ui.end_row();
+
+            let label = ui.label(crate::tl!("Extension line offset"));
+            number(ui, label.id, v("DIMEXO").as_f64().unwrap_or(0.0625), 0.0..=1.0e6, 0.01, "DIMEXO", &mut dim);
+            ui.end_row();
+
+            let label = ui.label(crate::tl!("Extension beyond dimension line"));
+            number(ui, label.id, v("DIMEXE").as_f64().unwrap_or(0.18), 0.0..=1.0e6, 0.01, "DIMEXE", &mut dim);
+            ui.end_row();
+
+            let label = ui.label(crate::tl!("Overall scale"));
+            number(ui, label.id, v("DIMSCALE").as_f64().unwrap_or(1.0), 0.0..=1.0e6, 0.1, "DIMSCALE", &mut dim);
+            ui.end_row();
+        });
+    });
+    if !dim.is_empty() {
+        changes.insert("dimStyle".into(), Value::Object(dim));
+    }
+    ui.add_space(4.0);
+    ui.horizontal(|ui| {
+        let has_defaults = !app.session.dim_defaults.is_empty();
+        if ui.add_enabled(has_defaults, egui::Button::new(crate::tl!("Forget defaults for new drawings"))).clicked() {
+            changes.insert("dimDefaults".into(), json!({}));
+        }
+    });
+    super::dialog::hint(
+        ui,
+        crate::tl!("Changes apply to the current dimension style at once and are kept for new drawings. Other drawings keep their own styles."),
+    );
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    /// Prefix and suffix are DIMPOST's text around `<>`; splitting and joining it gives the same
+    /// template back.
+    #[test]
+    fn prefix_and_suffix_round_trip_through_the_post_template() {
+        assert_eq!(split_post("<> mm"), (String::new(), " mm".to_string()));
+        assert_eq!(split_post("Ø<>"), ("Ø".to_string(), String::new()));
+        assert_eq!(split_post("~<> cm"), ("~".to_string(), " cm".to_string()));
+        assert_eq!(split_post(" mm"), (String::new(), " mm".to_string()), "a template without <> is a suffix");
+        assert_eq!(split_post(""), (String::new(), String::new()));
+        assert_eq!(join_post("", ""), "");
+        assert_eq!(join_post("Ø", " mm"), "Ø<> mm");
+        let (a, b) = split_post("~<> cm");
+        assert_eq!(join_post(&a, &b), "~<> cm");
+    }
+}
diff --git a/crates/ui-egui/src/options/locations.rs b/crates/ui-egui/src/options/locations.rs
new file mode 100644
--- /dev/null
+++ b/crates/ui-egui/src/options/locations.rs
@@ -0,0 +1,46 @@
+//! The Options dialog's Files tab, as AutoCAD's: the files CADCraft uses rather than how it saves
+//! (Open and Save). The default template for new drawings (QNEWTEMPLATE), the font that stands in
+//! for a missing one (FONTALT) and the fonts searched for characters a text's font lacks
+//! (FONTFALLBACK). They are process-wide settings, kept in the user profile.
+
+use serde_json::{Map, Value, json};
+
+/// A text field that commits when it loses focus; `Some(text)` (trimmed) on a change.
+fn commit_text(ui: &mut egui::Ui, salt: &str, current: &str, label: egui::Id) -> Option<String> {
+    let id = egui::Id::unique(("cc_options_locations", salt));
+    let mut text = ui.data(|d| d.get_temp::<String>(id)).unwrap_or_else(|| current.to_string());
+    let r = ui.add(egui::TextEdit::singleline(&mut text).desired_width(330.0)).labelled_by(label);
+    if r.changed() {
+        ui.data_mut(|d| d.insert_temp(id, text.clone()));
+    }
+    if r.lost_focus() {
+        ui.data_mut(|d| d.remove::<String>(id));
+        if text.trim() != current {
+            return Some(text.trim().to_string());
+        }
+    }
+    None
+}
+
+/// Files tab: the default template and the font substitutes, edited as text and sent when a field
+/// loses focus.
+pub(super) fn locations(ui: &mut egui::Ui, changes: &mut Map<String, Value>) {
+    super::dialog::grid(ui, "options_locations", |ui| {
+        let rows = [
+            ("QNEWTEMPLATE", crate::tl!("Default template for new drawings"), cadcraft_engine::cmd::file::qnew_template()),
+            ("FONTALT", crate::tl!("Alternate font"), cadcraft_fonts::ttf::font_alt()),
+            ("FONTFALLBACK", crate::tl!("Fallback fonts"), cadcraft_fonts::ttf::fallback_fonts()),
+        ];
+        for (var, label, current) in rows {
+            let caption = ui.label(label);
+            if let Some(text) = commit_text(ui, var, &current, caption.id) {
+                changes.insert(var.into(), json!(text));
+            }
+            ui.end_row();
+        }
+    });
+    super::dialog::hint(ui, crate::tl!("The template is a .dwt or .dxf file that New and the first drawing at startup are made from; empty: none."));
+    super::dialog::hint(ui, crate::tl!("The alternate font replaces a text style font that can't be found; empty: the built-in drafting font."));
+    super::dialog::hint(ui, crate::tl!("Fallback fonts (names separated by commas) are searched for characters a text's own font lacks."));
+    super::dialog::hint(ui, crate::tl!("The automatic save file location is on Open and Save."));
+}
diff --git a/crates/ui-egui/src/options/mod.rs b/crates/ui-egui/src/options/mod.rs
--- a/crates/ui-egui/src/options/mod.rs
+++ b/crates/ui-egui/src/options/mod.rs
@@ -22,6 +22,11 @@
 //! | `selectionColor` | the colour selected objects are highlighted in: `"#rrggbb"`, or `null` for the theme's |
 //! | `snapMarkerSize` | object snap marker size in points, 5–31 |
 //! | `interfaceScale` | the interface's size, 0.5–3 (1: the system's own) |
+//! | `dimStyle` | `{"DIMDEC": 2, "DIMDSEP": ","}`: dimension variables ([`DIM_OPTION_VARS`]) set on the current dimension style of the open drawing and kept for the drawings made from now on; reported as the current style's values |
+//! | `dimDefaults` | the same kind of object, kept for new drawings only (`null` or `{}` forgets them) |
+//! | `QNEWTEMPLATE` | the template file new drawings are made from (`""`: none) |
+//! | `FONTALT` | the font that replaces a text style font that can't be found (`""`: the built-in one) |
+//! | `FONTFALLBACK` | fonts searched for characters a text's font lacks, separated by commas |
 //!
 //! SAVETIME and SAVEFILEPATH are the engine's autosave settings, kept with the autosave state
 //! rather than in the profile.
@@ -30,7 +35,10 @@
 //! [`CadApp::profile_json`] under [`PROFILE_KEY`] next to the interface prefs and restores it with
 //! [`CadApp::load_profile`].
 
+mod advanced;
 mod dialog;
+mod dimensions;
+mod locations;
 
 pub use dialog::dialog;
 
@@ -56,6 +64,37 @@ pub const SNAP_MARKER_SIZES: std::ops::RangeInclusive<f32> = 5.0..=31.0;
 /// Interface scales the JSON form accepts (1: the system's own size).
 pub const INTERFACE_SCALES: std::ops::RangeInclusive<f32> = 0.5..=3.0;
 
+/// The dimension variables the Options dialog and the `dimStyle` / `dimDefaults` JSON keys edit:
+/// units and precision, the decimal separator, zero suppression, rounding, prefix and suffix,
+/// angular units, text and arrow sizes.
+pub const DIM_OPTION_VARS: &[&str] = &[
+    "DIMLUNIT", "DIMDEC", "DIMDSEP", "DIMZIN", "DIMRND", "DIMLFAC", "DIMPOST", "DIMAUNIT", "DIMADEC", "DIMAZIN", "DIMFRAC", "DIMTXT", "DIMGAP",
+    "DIMTAD", "DIMASZ", "DIMEXO", "DIMEXE", "DIMSCALE",
+];
+
+/// A `dimStyle` / `dimDefaults` object, checked: only [`DIM_OPTION_VARS`] (any case), and values a
+/// dimension style takes. `null` is an empty object.
+fn dim_map(key: &str, v: &Value) -> Result<Map<String, Value>, String> {
+    let obj = match v {
+        Value::Null => return Ok(Map::new()),
+        Value::Object(o) => o,
+        _ => return Err(format!("{key} must be an object of dimension variables")),
+    };
+    let mut out = Map::new();
+    for (name, value) in obj {
+        let n = name.trim().to_ascii_uppercase();
+        if !DIM_OPTION_VARS.contains(&n.as_str()) {
+            return Err(format!("{key}: {name} is not a dimension option ({})", DIM_OPTION_VARS.join(", ")));
+        }
+        out.insert(n, value.clone());
+    }
+    let rejected = cadcraft_engine::doc::DimStyle::default().apply_fields(&out);
+    if !rejected.is_empty() {
+        return Err(format!("{key}: invalid value for {}", rejected.join(", ")));
+    }
+    Ok(out)
+}
+
 /// The interface's own preferences (the drafting ones are engine settings).
 #[derive(Clone, Copy, Debug, PartialEq)]
 pub struct UiOptions {
@@ -149,8 +188,14 @@ struct UiChanges {
     savefilepath: Option<String>,
     time_sensitive_right_click: Option<bool>,
     interface_scale: Option<f32>,
+    dim_style: Option<Map<String, Value>>,
+    dim_defaults: Option<Map<String, Value>>,
+    qnew_template: Option<String>,
+    font_alt: Option<String>,
+    font_fallback: Option<String>,
 }
 
+/// A colour setting of the JSON form: `"#rrggbb"`, or null for the theme's own.
 fn color_param(key: &str, v: &Value) -> Result<Option<Color32>, String> {
     match v {
         Value::Null => Ok(None),
@@ -159,6 +204,8 @@ fn color_param(key: &str, v: &Value) -> Result<Option<Color32>, String> {
     }
 }
 
+/// Read one interface key of the OPTIONS JSON form into `c`: `Ok(true)` when it is one, `Ok(false)`
+/// for a key that belongs to the engine, an error for a bad value.
 fn parse_ui(key: &str, v: &Value, c: &mut UiChanges) -> Result<bool, String> {
     let text = || v.as_str().ok_or_else(|| format!("{key} must be text"));
     match key {
@@ -191,6 +238,8 @@ fn parse_ui(key: &str, v: &Value, c: &mut UiChanges) -> Result<bool, String> {
             c.save_version = Some(crate::SaveVersion::parse(name.trim()).ok_or("saveVersion must be 2000, 2004, 2007, 2010, 2013 or 2018")?);
         }
         "timeSensitiveRightClick" => c.time_sensitive_right_click = Some(v.as_bool().ok_or("timeSensitiveRightClick must be true or false")?),
+        "dimStyle" => c.dim_style = Some(dim_map(key, v)?),
+        "dimDefaults" => c.dim_defaults = Some(dim_map(key, v)?),
         k if k.eq_ignore_ascii_case("SAVETIME") => {
             let max = cadcraft_engine::autosave::MAX_SAVETIME;
             let n = v
@@ -199,6 +248,9 @@ fn parse_ui(key: &str, v: &Value, c: &mut UiChanges) -> Result<bool, String> {
                 .ok_or_else(|| format!("SAVETIME must be from 0 to {max} minutes"))?;
             c.savetime = Some(n.round() as u32);
         }
+        k if k.eq_ignore_ascii_case("QNEWTEMPLATE") => c.qnew_template = Some(setting_text(key, v)?),
+        k if k.eq_ignore_ascii_case("FONTALT") => c.font_alt = Some(setting_text(key, v)?),
+        k if k.eq_ignore_ascii_case("FONTFALLBACK") => c.font_fallback = Some(setting_text(key, v)?),
         k if k.eq_ignore_ascii_case("SAVEFILEPATH") => {
             let p = v.as_str().map(str::trim).filter(|p| p.chars().count() <= MAX_PATH_CHARS && !p.chars().any(char::is_control));
             c.savefilepath = Some(p.ok_or("SAVEFILEPATH must be a folder path")?.to_string());
@@ -208,6 +260,15 @@ fn parse_ui(key: &str, v: &Value, c: &mut UiChanges) -> Result<bool, String> {
     Ok(true)
 }
 
+/// A file path or font names: text of at most [`MAX_PATH_CHARS`] characters, no control characters.
+fn setting_text(key: &str, v: &Value) -> Result<String, String> {
+    v.as_str()
+        .map(str::trim)
+        .filter(|p| p.chars().count() <= MAX_PATH_CHARS && !p.chars().any(char::is_control))
+        .map(str::to_string)
+        .ok_or_else(|| format!("{} must be text of at most {MAX_PATH_CHARS} characters", key.to_ascii_uppercase()))
+}
+
 /// The interface preferences as the JSON form reports them.
 fn ui_json(app: &CadApp) -> Map<String, Value> {
     let o = &app.options;
@@ -227,9 +288,32 @@ fn ui_json(app: &CadApp) -> Map<String, Value> {
     m.insert("SAVETIME".into(), json!(cadcraft_engine::autosave::savetime()));
     m.insert("SAVEFILEPATH".into(), json!(cadcraft_engine::autosave::savefilepath()));
     m.insert("timeSensitiveRightClick".into(), json!(o.time_sensitive_right_click));
+    m.insert("dimStyle".into(), Value::Object(current_dim_style(app)));
+    m.insert("dimDefaults".into(), Value::Object(app.session.dim_defaults.clone()));
+    m.insert("QNEWTEMPLATE".into(), json!(cadcraft_engine::cmd::file::qnew_template()));
+    m.insert("FONTALT".into(), json!(cadcraft_fonts::ttf::font_alt()));
+    m.insert("FONTFALLBACK".into(), json!(cadcraft_fonts::ttf::fallback_fonts()));
     m
 }
 
+/// The name of the open drawing's current dimension style.
+pub fn current_dim_style_name(app: &CadApp) -> Option<String> {
+    app.session.doc().ok().map(|d| d.header.str("DIMSTYLE", "Standard"))
+}
+
+/// [`DIM_OPTION_VARS`] as the open drawing's current dimension style has them (empty with no
+/// drawing).
+pub fn current_dim_style(app: &CadApp) -> Map<String, Value> {
+    let Ok(d) = app.session.doc() else { return Map::new() };
+    let Some(style) = d.dim_style(&d.header.str("DIMSTYLE", "Standard")) else { return Map::new() };
+    let fields = serde_json::to_value(style).unwrap_or(Value::Null);
+    cadcraft_engine::doc::DIMVARS
+        .iter()
+        .filter(|(var, _)| DIM_OPTION_VARS.contains(var))
+        .filter_map(|(var, field)| fields.get(*field).map(|v| ((*var).to_string(), v.clone())))
+        .collect()
+}
+
 /// Every preference: the interface's and the engine's profile variables.
 pub fn report(app: &CadApp) -> Value {
     let mut m = ui_json(app);
@@ -255,6 +339,7 @@ fn run_json(app: &mut CadApp, params: &Value) -> Result<Value, String> {
     if reset {
         reset_ui(app);
     }
+    app.apply_file_settings(&ui);
     if let Some(t) = ui.theme {
         app.ui.theme = t;
     }
@@ -300,11 +385,42 @@ fn run_json(app: &mut CadApp, params: &Value) -> Result<Value, String> {
     if let Some(b) = ui.time_sensitive_right_click {
         app.options.time_sensitive_right_click = b;
     }
+    if let Some(defaults) = ui.dim_defaults {
+        app.session.set_dim_defaults(defaults);
+    }
+    if let Some(style) = ui.dim_style {
+        apply_dim_style(app, &style)?;
+    }
     Ok(report(app))
 }
 
+/// Set `style` on the open drawing's current dimension style, then keep it for new drawings: a
+/// style the drawing refuses is an error and the defaults stay as they were.
+fn apply_dim_style(app: &mut CadApp, style: &Map<String, Value>) -> Result<(), String> {
+    if style.is_empty() {
+        return Ok(());
+    }
+    if let Some(name) = current_dim_style_name(app) {
+        let mut params = style.clone();
+        params.insert("name".into(), json!(name));
+        params.insert("current".into(), json!(true));
+        let r = app.session.execute("dimstyle", &Value::Object(params)).map_err(|e| e.to_string())?;
+        if let Some(ignored) = r.get("ignored").and_then(Value::as_array).filter(|a| !a.is_empty()) {
+            let names: Vec<String> = ignored.iter().map(|v| v.as_str().map_or_else(|| v.to_string(), str::to_string)).collect();
+            return Err(format!("dimStyle: invalid value for {}", names.join(", ")));
+        }
+    }
+    let mut defaults = app.session.dim_defaults.clone();
+    defaults.extend(style.clone());
+    app.session.set_dim_defaults(defaults);
+    Ok(())
+}
+
 /// The interface preferences back to their defaults (the interface language stays).
 fn reset_ui(app: &mut CadApp) {
+    app.session.set_dim_defaults(Map::new());
+    let none = || Some(String::new());
+    app.apply_file_settings(&UiChanges { qnew_template: none(), font_alt: none(), font_fallback: none(), ..UiChanges::default() });
     let d = crate::UiState::default();
     app.ui.theme = d.theme;
     app.ui.animations = d.animations;
@@ -331,6 +447,10 @@ impl CadApp {
             "snapMarkerSize": self.options.snap_marker_size,
             "timeSensitiveRightClick": self.options.time_sensitive_right_click,
             "interfaceScale": self.options.interface_scale,
+            "dimDefaults": self.session.dim_defaults,
+            "QNEWTEMPLATE": cadcraft_engine::cmd::file::qnew_template(),
+            "FONTALT": cadcraft_fonts::ttf::font_alt(),
+            "FONTFALLBACK": cadcraft_fonts::ttf::fallback_fonts(),
         })
         .to_string()
     }
@@ -346,9 +466,19 @@ impl CadApp {
             cadcraft_engine::cmd::options::restore(&mut self.session, vars);
         }
         let mut c = UiChanges::default();
-        for key in
-            ["systemCursor", "modelBackground", "paperBackground", "selectionColor", "snapMarkerSize", "timeSensitiveRightClick", "interfaceScale"]
-        {
+        for key in [
+            "systemCursor",
+            "modelBackground",
+            "paperBackground",
+            "selectionColor",
+            "snapMarkerSize",
+            "timeSensitiveRightClick",
+            "interfaceScale",
+            "dimDefaults",
+            "QNEWTEMPLATE",
+            "FONTALT",
+            "FONTFALLBACK",
+        ] {
             if let Some(value) = v.get(key) {
                 let _ = parse_ui(key, value, &mut c);
             }
@@ -375,6 +505,27 @@ impl CadApp {
         if let Some(n) = c.interface_scale {
             self.options.interface_scale = n;
         }
+        self.apply_file_settings(&c);
+        if let Some(defaults) = c.dim_defaults {
+            self.session.restore_dim_defaults(defaults);
+        }
+    }
+
+    /// The Files tab's settings (process-wide): the default template and the font substitutes. A
+    /// font change redraws the text.
+    fn apply_file_settings(&mut self, c: &UiChanges) {
+        if let Some(p) = &c.qnew_template {
+            cadcraft_engine::cmd::file::set_qnew_template(p);
+        }
+        if let Some(name) = &c.font_alt {
+            cadcraft_fonts::ttf::set_font_alt(name);
+        }
+        if let Some(list) = &c.font_fallback {
+            cadcraft_fonts::ttf::set_fallback_fonts(list);
+        }
+        if c.font_alt.is_some() || c.font_fallback.is_some() {
+            self.session.touch();
+        }
     }
 }
 
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
@@ -17,9 +17,11 @@ mod lisp_files;
 mod measure_distance_snapping;
 mod menubar_hover;
 mod mspace_viewport_view;
+mod options_button;
 mod native_menu_text_keys;
 mod native_windows;
 mod options_dialog;
+mod options_dimensions;
 mod parameters_rename;
 mod pickdrag;
 mod properties_units;
diff --git a/crates/ui-egui/tests/it/options_button.rs b/crates/ui-egui/tests/it/options_button.rs
new file mode 100644
--- /dev/null
+++ b/crates/ui-egui/tests/it/options_button.rs
@@ -0,0 +1,72 @@
+//! The Options button: a gear in the top right corner that no interface toggle hides, so the
+//! preferences are always one click away.
+
+use cadcraft_engine::Session;
+use cadcraft_engine::doc::Drawing;
+use cadcraft_ui_egui::{CadApp, Services};
+use egui::accesskit::Role;
+use egui::vec2;
+use egui_kittest::Harness;
+use egui_kittest::kittest::Queryable;
+
+/// The app with one drawing in a window `width` points wide, a few frames run.
+fn harness(width: f32) -> Harness<'static, CadApp> {
+    let mut session = Session::empty();
+    session.open_drawing(Drawing::new_metric(), "Options button", None);
+    let app = CadApp::new(session, Services::default());
+    let mut h = Harness::builder().with_size(vec2(width, 800.0)).build_ui_state(
+        |ui, app: &mut CadApp| {
+            app.logic(ui.ctx());
+            app.ui(ui);
+        },
+        app,
+    );
+    h.run_steps(3);
+    h
+}
+
+/// Click the button named Options, by its accessibility label, and check the dialog opened.
+fn click_options(h: &mut Harness<'static, CadApp>) {
+    h.state_mut().ui.dialog = None;
+    h.run_steps(3);
+    h.get_by_role_and_label(Role::Button, "Options").click();
+    h.run_steps(3);
+    assert_eq!(h.state().ui.dialog.as_deref(), Some("options"));
+}
+
+/// With the default interface, the gear opens Options.
+#[test]
+fn the_options_button_opens_the_options_dialog() {
+    let mut h = harness(1600.0);
+    click_options(&mut h);
+}
+
+/// With every bar, palette and panel hidden, and on the start page, the gear is still there.
+#[test]
+fn no_interface_toggle_hides_the_options_button() {
+    let mut h = harness(1600.0);
+    {
+        let ui = &mut h.state_mut().ui;
+        ui.show_toolbar = false;
+        ui.show_status_bar = false;
+        ui.in_window_menu = false;
+        ui.show_file_tabs = false;
+        ui.show_toolsets = false;
+        ui.show_palettes = false;
+        ui.show_command_line = false;
+        ui.show_viewcube = false;
+    }
+    h.run_steps(3);
+    click_options(&mut h);
+    // The start page too.
+    h.state_mut().ui.start_tab = true;
+    h.run_steps(3);
+    click_options(&mut h);
+}
+
+/// A window too narrow for the tool bar still shows the gear.
+#[test]
+fn a_narrow_window_still_shows_it() {
+    let mut h = harness(420.0);
+    click_options(&mut h);
+}
diff --git a/crates/ui-egui/tests/it/options_dimensions.rs b/crates/ui-egui/tests/it/options_dimensions.rs
new file mode 100644
--- /dev/null
+++ b/crates/ui-egui/tests/it/options_dimensions.rs
@@ -0,0 +1,246 @@
+//! The Options dialog's Dimensions tab and its JSON keys: `dimStyle` sets the open drawing's
+//! current dimension style and is kept for new drawings, `dimDefaults` only for new drawings; a
+//! refused value changes nothing; the profile keeps them across a restart.
+
+use cadcraft_engine::Session;
+use cadcraft_ui_egui::{CadApp, Services};
+use serde_json::{Value, json};
+
+/// An app with the startup drawing and no file dialogs.
+fn app() -> CadApp {
+    CadApp::new(Session::new(), Services::default())
+}
+
+/// Every text shape painted in a frame, one per line, to check what a tab shows.
+fn painted_text(shapes: &[egui::epaint::ClippedShape]) -> String {
+    /// Append the text of `shape` and of the shapes it groups.
+    fn collect(shape: &egui::Shape, text: &mut String) {
+        match shape {
+            egui::Shape::Text(s) => {
+                text.push_str(s.galley.text());
+                text.push('\n');
+            }
+            egui::Shape::Vec(shapes) => shapes.iter().for_each(|s| collect(s, text)),
+            _ => {}
+        }
+    }
+    let mut text = String::new();
+    shapes.iter().for_each(|s| collect(&s.shape, &mut text));
+    text
+}
+
+/// Run two frames of the app and return the text they painted.
+fn render(app: &mut CadApp, ctx: &egui::Context) -> String {
+    let input = egui::RawInput { screen_rect: Some(egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(1600.0, 1000.0))), ..Default::default() };
+    let mut text = String::new();
+    for _ in 0..2 {
+        app.logic(ctx);
+        let mut output = ctx.run_ui(input.clone(), |ui| app.ui(ui));
+        text = painted_text(&output.shapes);
+        output.textures_delta.clear();
+    }
+    text
+}
+
+/// The current dimension style's variable `var` in the open drawing.
+fn current(a: &mut CadApp, var: &str) -> Value {
+    let name = cadcraft_ui_egui::options::current_dim_style_name(a).unwrap_or_default();
+    let r = a.session.execute("dimstyle.list", &json!({ "name": name })).unwrap();
+    r["variables"][var].clone()
+}
+
+/// `dimStyle` changes the open drawing's current style, and the JSON form reports it back.
+#[test]
+fn dim_style_sets_the_current_style_and_is_reported() {
+    let mut a = app();
+    let before = current(&mut a, "DIMDEC");
+    let r = a.run("options", json!({ "dimStyle": { "DIMDEC": 2, "dimdsep": ",", "DIMZIN": 12 } })).unwrap();
+    assert_eq!(current(&mut a, "DIMDEC"), json!(2));
+    assert_eq!(current(&mut a, "DIMDSEP"), json!(","));
+    assert_eq!(current(&mut a, "DIMZIN"), json!(12));
+    assert_ne!(before, json!(2), "the test changes something");
+    // Reported as the current style's values, and kept as the defaults for new drawings.
+    assert_eq!(r["dimStyle"]["DIMDEC"], json!(2));
+    assert_eq!(r["dimStyle"]["DIMDSEP"], json!(","));
+    assert_eq!(r["dimDefaults"], json!({ "DIMDEC": 2, "DIMDSEP": ",", "DIMZIN": 12 }));
+}
+
+/// Defaults reach drawings made later, never drawings already open.
+#[test]
+fn new_drawings_take_the_defaults_and_other_drawings_keep_their_style() {
+    let mut a = app();
+    a.run("options", json!({ "dimStyle": { "DIMDEC": 1, "DIMDSEP": "," } })).unwrap();
+    let first = a.session.active;
+    a.run("new", json!({})).unwrap();
+    assert_ne!(a.session.active, first);
+    assert_eq!(current(&mut a, "DIMDEC"), json!(1));
+    assert_eq!(current(&mut a, "DIMDSEP"), json!(","));
+    // Changing the current drawing's style later leaves the first one as it was.
+    a.run("options", json!({ "dimStyle": { "DIMDEC": 5 } })).unwrap();
+    a.run("document.switch", json!({ "index": first })).unwrap();
+    assert_eq!(current(&mut a, "DIMDEC"), json!(1));
+    // `dimDefaults` is for new drawings only.
+    a.run("options", json!({ "dimDefaults": { "DIMDEC": 3 } })).unwrap();
+    assert_eq!(current(&mut a, "DIMDEC"), json!(1), "the open drawing keeps its style");
+    a.run("new", json!({})).unwrap();
+    assert_eq!(current(&mut a, "DIMDEC"), json!(3));
+    // Forgotten, new drawings are as built in again.
+    a.run("options", json!({ "dimDefaults": {} })).unwrap();
+    a.run("new", json!({})).unwrap();
+    assert_ne!(current(&mut a, "DIMDEC"), json!(3));
+}
+
+/// `dimDefaults` leaves even a fresh, untouched drawing as it is: only the profile read at
+/// start-up gives it the defaults.
+#[test]
+fn dim_defaults_leave_the_untouched_first_drawing_alone() {
+    let mut a = app();
+    let before = current(&mut a, "DIMDEC");
+    a.run("options", json!({ "dimDefaults": { "DIMDEC": 7 } })).unwrap();
+    assert_eq!(current(&mut a, "DIMDEC"), before);
+    let mut fresh = app();
+    fresh.session.restore_dim_defaults(serde_json::from_value(json!({ "DIMDEC": 7 })).unwrap());
+    assert_eq!(current(&mut fresh, "DIMDEC"), json!(7), "start-up: the profile's defaults reach the first drawing");
+}
+
+/// A value the dimension style refuses, or a name it doesn't have, is an error and leaves both the
+/// style and the defaults as they were.
+#[test]
+fn refused_values_and_unknown_names_change_nothing() {
+    let mut a = app();
+    let before = a.run("options", json!({})).unwrap();
+    for bad in [
+        json!({ "dimStyle": { "DIMDEC": -1 } }),
+        json!({ "dimStyle": { "DIMFOO": 1 } }),
+        json!({ "dimStyle": { "DIMTXT": 0 } }),
+        json!({ "dimStyle": 5 }),
+        json!({ "dimDefaults": { "DIMBLK": "x" } }),
+        json!({ "dimStyle": { "DIMDEC": 2 }, "PICKBOX": "nonsense" }),
+    ] {
+        assert!(a.run("options", bad.clone()).is_err(), "{bad}");
+    }
+    let after = a.run("options", json!({})).unwrap();
+    assert_eq!(before["dimStyle"], after["dimStyle"]);
+    assert_eq!(after["dimDefaults"], json!({}));
+}
+
+/// The defaults survive a restart through the profile, the startup drawing included, and Reset to
+/// Defaults drops them.
+#[test]
+fn the_profile_keeps_the_defaults_and_reset_forgets_them() {
+    let mut a = app();
+    a.run("options", json!({ "dimStyle": { "DIMDEC": 2, "DIMDSEP": "," } })).unwrap();
+    let saved = a.profile_json();
+    let mut b = app();
+    b.load_profile(&saved);
+    assert_eq!(b.session.dim_defaults.get("DIMDEC"), Some(&json!(2)));
+    // The startup drawing is made before the profile is read: it takes the defaults too.
+    assert_eq!(current(&mut b, "DIMDEC"), json!(2));
+    b.run("new", json!({})).unwrap();
+    assert_eq!(current(&mut b, "DIMDSEP"), json!(","));
+    // A hand-edited profile with a bad entry still restores the rest.
+    let mut c = app();
+    c.load_profile(r#"{"version":1,"dimDefaults":{"DIMDEC":"junk"},"systemCursor":true}"#);
+    assert!(c.session.dim_defaults.is_empty());
+    b.run("options", json!({ "reset": true })).unwrap();
+    assert!(b.session.dim_defaults.is_empty());
+}
+
+/// Both new tabs draw their rows without a drawing error.
+#[test]
+fn the_dimensions_and_all_settings_tabs_render() {
+    let mut a = app();
+    a.ui.dialog = Some("options:dimensions".into());
+    let ctx = egui::Context::default();
+    let text = render(&mut a, &ctx);
+    for want in
+        ["Dimensions", "Current dimension style", "Linear units", "Decimal separator", "Precision (decimal places)", "Suppress trailing zeros"]
+    {
+        assert!(text.contains(want), "{want}: {text}");
+    }
+    assert_eq!(a.ui.dialog.as_deref(), Some("options:dimensions"));
+    a.ui.dialog = Some("options:advanced".into());
+    let text = render(&mut a, &ctx);
+    for want in ["All settings", "APERTURE", "AUTOSNAP", "Filter", "Edit as JSON"] {
+        assert!(text.contains(want), "{want}: {text}");
+    }
+}
+
+/// The interface state is listed with the settings, and `set_ui_state` refuses a value of the wrong
+/// kind without changing anything.
+#[test]
+fn the_all_settings_tab_lists_the_interface_state_and_set_ui_state_checks_it() {
+    let mut a = app();
+    // Filtered to the toolbar, the tab shows the interface state's row.
+    a.ui.dialog = Some("options:advanced".into());
+    let ctx = egui::Context::default();
+    ctx.data_mut(|d| d.insert_temp(egui::Id::unique("cc_options_advanced_filter"), "toolbar".to_string()));
+    let text = render(&mut a, &ctx);
+    for want in ["Interface state", "showToolbar"] {
+        assert!(text.contains(want), "{want}: {text}");
+    }
+    // The same call changes it, and a bad value or an invalid layout changes nothing.
+    assert!(a.ui.show_toolbar);
+    let patch = json!({ "showToolbar": false });
+    a.set_ui_state(patch.as_object().unwrap()).unwrap();
+    assert!(!a.ui.show_toolbar);
+    let bad = json!({ "showToolbar": "maybe" });
+    assert!(a.set_ui_state(bad.as_object().unwrap()).is_err());
+    assert!(!a.ui.show_toolbar, "a refused value leaves the state as it was");
+}
+
+/// Where the text `label` is painted (its galley's top left), if anywhere.
+fn text_pos(shapes: &[egui::epaint::ClippedShape], want: &dyn Fn(&str) -> bool) -> Vec<egui::Pos2> {
+    /// Append where `shape`, or a shape it groups, paints text that `want` accepts.
+    fn collect(shape: &egui::Shape, want: &dyn Fn(&str) -> bool, out: &mut Vec<egui::Pos2>) {
+        match shape {
+            egui::Shape::Text(s) if want(s.galley.text()) => out.push(s.pos),
+            egui::Shape::Vec(shapes) => shapes.iter().for_each(|s| collect(s, want, out)),
+            _ => {}
+        }
+    }
+    let mut out = Vec::new();
+    shapes.iter().for_each(|s| collect(&s.shape, want, &mut out));
+    out
+}
+
+/// Dragging a number over several frames sends nothing until release, then one `dimstyle` run: one
+/// undo step.
+#[test]
+fn dragging_a_dimension_value_is_one_undo_step() {
+    let mut a = app();
+    a.ui.dialog = Some("options:dimensions".into());
+    let ctx = egui::Context::default();
+    let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(1600.0, 1000.0));
+    let frame = |a: &mut CadApp, events: Vec<egui::Event>| {
+        let input = egui::RawInput { screen_rect: Some(screen), events, ..Default::default() };
+        a.logic(&ctx);
+        let mut output = ctx.run_ui(input, |ui| a.ui(ui));
+        output.textures_delta.clear();
+        output.shapes
+    };
+    frame(&mut a, Vec::new());
+    let shapes = frame(&mut a, Vec::new());
+    // The precision field: the number painted on the label's row, right of it.
+    let label = text_pos(&shapes, &|t| t == "Precision (decimal places)");
+    let label = *label.first().expect("the precision row");
+    let before = current(&mut a, "DIMDEC");
+    let digits = before.to_string();
+    let field = text_pos(&shapes, &|t| t == digits).into_iter().find(|p| (p.y - label.y).abs() < 6.0 && p.x > label.x).expect("the precision field");
+    let start = field + egui::vec2(3.0, 6.0);
+    let undo_before = a.session.state().unwrap().undo.len();
+    let press = |pressed, pos| egui::Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() };
+    frame(&mut a, vec![egui::Event::PointerMoved(start)]);
+    frame(&mut a, vec![press(true, start)]);
+    // A drag over several frames, each of which changes the shown value.
+    for step in 1..=6 {
+        frame(&mut a, vec![egui::Event::PointerMoved(start - egui::vec2(step as f32 * 2.0, 0.0))]);
+    }
+    assert_eq!(a.session.state().unwrap().undo.len(), undo_before, "nothing is sent while the field is dragged");
+    let end = start - egui::vec2(12.0, 0.0);
+    frame(&mut a, vec![press(false, end)]);
+    frame(&mut a, Vec::new());
+    let after = current(&mut a, "DIMDEC");
+    assert_ne!(after, before, "the drag changed the precision");
+    assert_eq!(a.session.state().unwrap().undo.len(), undo_before + 1, "one drag, one undo step");
+}
diff --git a/crates/ui-egui/tests/options_files.rs b/crates/ui-egui/tests/options_files.rs
new file mode 100644
--- /dev/null
+++ b/crates/ui-egui/tests/options_files.rs
@@ -0,0 +1,96 @@
+//! Options ▸ Files: the default template (QNEWTEMPLATE) and the font substitutes (FONTALT,
+//! FONTFALLBACK) through the JSON form, the tab, the profile and Reset to Defaults.
+//! (Its own test binary: these settings are process-wide, so everything runs in one test.)
+
+use cadcraft_engine::Session;
+use cadcraft_ui_egui::{CadApp, Services};
+use serde_json::json;
+
+/// An app with the startup drawing and no file dialogs.
+fn app() -> CadApp {
+    CadApp::new(Session::new(), Services::default())
+}
+
+/// Every text shape painted in a frame, one per line, to check what the tab shows.
+fn painted_text(shapes: &[egui::epaint::ClippedShape]) -> String {
+    /// Append the text of `shape` and of the shapes it groups.
+    fn collect(shape: &egui::Shape, text: &mut String) {
+        match shape {
+            egui::Shape::Text(s) => {
+                text.push_str(s.galley.text());
+                text.push('\n');
+            }
+            egui::Shape::Vec(shapes) => shapes.iter().for_each(|s| collect(s, text)),
+            _ => {}
+        }
+    }
+    let mut text = String::new();
+    shapes.iter().for_each(|s| collect(&s.shape, &mut text));
+    text
+}
+
+/// Run two frames of the app and return the text they painted.
+fn render(app: &mut CadApp) -> String {
+    let ctx = egui::Context::default();
+    let input = egui::RawInput { screen_rect: Some(egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(1600.0, 1000.0))), ..Default::default() };
+    let mut text = String::new();
+    for _ in 0..2 {
+        app.logic(&ctx);
+        let mut output = ctx.run_ui(input.clone(), |ui| app.ui(ui));
+        text = painted_text(&output.shapes);
+        output.textures_delta.clear();
+    }
+    text
+}
+
+/// QNEWTEMPLATE, FONTALT and FONTFALLBACK are reported, set (GETVAR sees them), refused when
+/// malformed, shown in the tab, kept by the profile and cleared by Reset.
+#[test]
+fn the_files_settings_are_reported_set_kept_and_reset() {
+    let mut a = app();
+    let r = a.run("options", json!({})).unwrap();
+    for key in ["QNEWTEMPLATE", "FONTALT", "FONTFALLBACK"] {
+        assert_eq!(r[key], json!(""), "{key} is reported, empty by default");
+    }
+
+    let r = a
+        .run("options", json!({ "qnewtemplate": " /tmp/cc-office.dwt ", "FONTALT": "DejaVu Sans", "FontFallback": "Noto Sans; Noto Sans CJK" }))
+        .unwrap();
+    assert_eq!(r["QNEWTEMPLATE"], json!("/tmp/cc-office.dwt"));
+    assert_eq!(r["FONTALT"], json!("DejaVu Sans"));
+    assert_eq!(r["FONTFALLBACK"], json!("Noto Sans, Noto Sans CJK"));
+    // The engine's variables are the same settings.
+    let var = |a: &mut CadApp, name: &str| a.session.execute("getvar", &json!({ "name": name })).unwrap()["value"].clone();
+    assert_eq!(var(&mut a, "QNEWTEMPLATE"), json!("/tmp/cc-office.dwt"));
+    assert_eq!(var(&mut a, "FONTALT"), json!("DejaVu Sans"));
+
+    // Refused values change nothing.
+    for bad in
+        [json!({ "FONTALT": 3 }), json!({ "QNEWTEMPLATE": "a\nb" }), json!({ "QNEWTEMPLATE": "x".repeat(2000) }), json!({ "FONTFALLBACK": null })]
+    {
+        assert!(a.run("options", bad.clone()).is_err(), "{bad}");
+    }
+    assert_eq!(var(&mut a, "QNEWTEMPLATE"), json!("/tmp/cc-office.dwt"));
+
+    // The Files tab shows them.
+    a.ui.dialog = Some("options:locations".into());
+    let text = render(&mut a);
+    for want in ["Files", "Default template for new drawings", "/tmp/cc-office.dwt", "Alternate font", "DejaVu Sans", "Fallback fonts"] {
+        assert!(text.contains(want), "{want}: {text}");
+    }
+
+    // The profile keeps them; Reset to Defaults clears them.
+    let saved = a.profile_json();
+    a.run("options", json!({ "reset": true })).unwrap();
+    assert_eq!(var(&mut a, "QNEWTEMPLATE"), json!(""));
+    assert_eq!(var(&mut a, "FONTFALLBACK"), json!(""));
+    let mut b = app();
+    b.load_profile(&saved);
+    let r = b.run("options", json!({})).unwrap();
+    assert_eq!((r["QNEWTEMPLATE"].clone(), r["FONTALT"].clone()), (json!("/tmp/cc-office.dwt"), json!("DejaVu Sans")));
+    // A hand-edited profile with a bad entry keeps the setting as it was.
+    b.load_profile(r#"{"version":1,"QNEWTEMPLATE":["x"],"FONTALT":"a\u0007b"}"#);
+    assert_eq!(var(&mut b, "QNEWTEMPLATE"), json!("/tmp/cc-office.dwt"));
+    assert_eq!(var(&mut b, "FONTALT"), json!("DejaVu Sans"));
+    b.run("options", json!({ "reset": true })).unwrap();
+}
diff --git a/docs/gaps.md b/docs/gaps.md
--- a/docs/gaps.md
+++ b/docs/gaps.md
@@ -1,6 +1,6 @@
 # Where CADCraft falls short of AutoCAD
 
-> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-25 · **Change:** minor (DIMTFILL limits in the `*D` block, EXPLODE and DXF: F7 updated) · **Target:** Autodesk AutoCAD 2027
+> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-25 · **Change:** minor (Options: always-visible button, Files, Dimensions and All settings tabs: F20 narrowed) · **Target:** Autodesk AutoCAD 2027
 
 Every known shortfall, one row each, ranked within each section by user impact. This is the work
 list: agents pick from here (top of a section first), and remove or update a row in the same PR
@@ -41,7 +41,7 @@ do them first.
 | F16 | Groups: no group bounding box or single grip when a group is selected (GROUPDISPLAYMODE), copying a whole group does not make a new group, no Object Grouping dialog (GROUP, GROUPEDIT, UNGROUP, -GROUP, PICKSTYLE group selection and the DXF/DWG round trip landed, #417); PICKSTYLE's associative-hatch bit is stored but unused | `cmd/groups.rs` | Low | 2–4 |
 | F17 | Attributes (ATTSYNC, global -ATTEDIT, BATTMAN editing and DATAEXTRACTION/EATTEXT landed, #433): no verify, preset or lock-position modes, and multiline attributes (read, drawn and saved since 2026-10-12) can't be created or switched in BATTMAN; data extraction covers block references and attribute values (CSV or a table) only — no wizard, nested blocks, other objects' properties, `.dxe` templates or spreadsheet output | `cmd/blocks/battman.rs`, `cmd/blocks/extract.rs` | Low | 4–6 |
 | F19 | MATCHPROP Settings is a keyword prompt (no dialog) and has no material, shadow or centre-object settings; TEXTTOFRONT and HATCHTOBACK act on the current space only; OVERKILL doesn't compare polyline segments with other objects (picker and Settings, DRAWORDER Above/Under, TEXTTOFRONT, HATCHTOBACK, OVERKILL tolerance/ignore/overlap/end-to-end options landed, #439) | `props/matchprop.rs`, `draworder.rs`, `overkill.rs` | Low | 2–4 |
-| F20 | OPTIONS dialog done (#23): Display, Open and Save, Drafting, Selection, User Preferences and Profiles tabs, and the user profile (object snap modes, drafting toggles, Dynamic Input, PICKBOX, APERTURE, GRIPSIZE, CURSORSIZE, PICKFIRST, PICKADD, QPMODE, model and paper background, snap marker size, system cursor) survives restarts. Left: right-click customization, the autosave interval and default save version on Open and Save (the features themselves landed, #406, #421), AutoSnap magnet and tooltip toggles, colours of the other drawing-area elements, named profiles with import and export | `ui-egui/src/options/` | Low | 2–4 |
+| F20 | OPTIONS dialog done (#23): Files, Display, Open and Save, Drafting, Dimensions, Selection, User Preferences, Profiles and All settings tabs, a gear button that is always shown, and the user profile (object snap modes, drafting toggles, Dynamic Input, PICKBOX, APERTURE, GRIPSIZE, CURSORSIZE, PICKFIRST, PICKADD, QPMODE, model and paper background, snap marker size, system cursor, dimension defaults for new drawings, default template, alternate and fallback fonts) survives restarts. Left: right-click customization, AutoSnap magnet and tooltip toggles, colours of the other drawing-area elements, named profiles with import and export, the Plot and Publish, System and 3D Modeling tabs | `ui-egui/src/options/` | Low | 2–4 |
 | F21 | MLINE, REGION and HELIX are objects since #510, with gaps: MLEDIT tees trim square; UNION/SUBTRACT/INTERSECT only combine regions whose boundaries don't meet; regions are saved as closed polylines (no ACIS data); PURGE skips multiline styles | `draw2/mline.rs`, `inquiry/regions.rs`, `dxf_region.rs` | Low | 6–10 |
 | F22 | Inquiry leftovers (COUNT across spaces and nested blocks, the QuickCalc palette and COMPARE landed, #563; MASSPROP on regions, #510): COMPARE compares the tab shown, not every layout at once (COMPAREEXPORT, the comparison as a new drawing, landed); COMPAREEXPORT puts the two sets of objects in model-space groups, where AutoCAD puts them in blocks; QuickCalc keeps its history and variables for the session only | `file/compare.rs`, `palettes/quickcalc.rs` | Low | 3–5 |
 | F23 | No Sheet Set Manager, eTransmit, Share, Traces, Markup Import, Activity Insights | menu items uncovered | Low-medium (team workflows) | 40–70 |
@@ -150,6 +150,7 @@ Detail: [localization-parity.md](localization-parity.md).
 
 | Date | Change | Summary |
 |---|---|---|
+| 2026-10-25 | minor | OPTIONS: a gear button in the top right corner that no interface toggle hides; a Files tab (QNEWTEMPLATE, FONTALT, FONTFALLBACK, now kept in the user profile), a Dimensions tab (linear units, decimal places, decimal separator, zero suppression, rounding, prefix and suffix, angular units, text and arrow sizes; the `dimStyle` / `dimDefaults` JSON keys, kept for new drawings) and an All settings tab that edits every setting of the JSON form and the interface state, one row each or as a JSON document: F20 narrowed |
 | 2026-10-25 | minor | DIMTFILL limits: with DIMTFILL 1 (the drawing's background color) the written `*D` block and EXPLODE keep no background mask (a WIPEOUT over the text box would), and a true-color DIMTFILLCLR is written to DXF as the nearest ACI: F7 updated |
 | 2026-10-24 | minor | COMPAREEXPORT (typed, or Export on the comparison bar) opens the comparison as a new untitled drawing, `compare_<this> vs <other>.dwg`: this drawing's objects in one group and the other drawing's differing objects in a second group, in the comparison colors (green, red, gray for objects in both), block contents and style colors included: F22 narrowed |
 | 2026-10-24 | minor | DIMTFILL and DIMTFILLCLR: a dimension's text background (none, the drawing's background color, or a color) hides what is drawn under the text; SETVAR, DIMOVERRIDE and style fields, DIMSTYLE groups 69 and 70 in DXF from R2007 (CADCraft xdata before), and a SOLID in the `*D` block and from EXPLODE for DIMTFILL 2: F7 narrowed |
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-25 · **Change:** minor (Options: always-visible button, Files, Dimensions and All settings tabs) · **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
@@ -166,7 +166,7 @@ down-arrow options menu (DYNPROMPT), and DYNDIM.
 | ViewCube | interactive 3D | clickable for 2D (Top/Home → extents); dragging does nothing (#7) |
 | Navigation bar, SteeringWheels | yes | no |
 | Workspaces | Drafting & Annotation, 3D Modeling | no |
-| Options / Preferences dialog | many tabs | **no** (issue #23) |
+| Options / Preferences dialog | many tabs | yes (#23): Files, Display, Open and Save, Drafting, Dimensions, Selection, User Preferences, Profiles and All settings tabs, a gear button in the top right corner that no interface toggle hides; no Plot and Publish, System or 3D Modeling tabs |
 | Customisation (CUI, aliases editor, tool palettes) | yes | Command Aliases dialog (ALIASEDIT, #1381); no CUI or tool palettes |
 | Themes | Dark, Light | Dark, Light, System (#179) |
 | Accessibility | VoiceOver basics | AccessKit through eframe; screen-reader coverage untested; magnifier issue #39; animation effects follow the OS setting (View ▸ Accessibility ▸ Animation Effects: System, On, Off) |
@@ -203,6 +203,7 @@ down-arrow options menu (DYNPROMPT), and DYNDIM.
 
 | Date | Change | Summary |
 |---|---|---|
+| 2026-10-25 | minor | Options: a gear button in the top right corner, drawn whatever the Tool Bar, menu bar and palettes show; a Files tab (default template, alternate and fallback fonts, kept in the profile), a Dimensions tab (number format of the current dimension style, kept for new drawings) and an All settings tab (every setting of the OPTIONS JSON form and the interface state, one row each or as JSON) |
 | 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) |
