diff --git a/apps/vectorcraft/src/caption_style.rs b/apps/vectorcraft/src/caption_style.rs
new file mode 100644
--- /dev/null
+++ b/apps/vectorcraft/src/caption_style.rs
@@ -0,0 +1,285 @@
+//! Linux: which desktop the custom title bar's caption buttons look like with the Classic OG colour
+//! scheme, and which buttons go on which side (`vectorcraft_ui_egui::titlebar::CaptionStyle`), as in
+//! PhotoCraft (its `caption_style.rs`). GNOME gets libadwaita's buttons in its `button-layout` order
+//! (from `gsettings`), KDE (and SteamOS's desktop mode) Breeze's in KWin's order (`kwinrc`); other desktops get plain round buttons, or Adwaita's
+//! or Breeze's when their GTK theme is one of those, in GTK's `gtk-decoration-layout` order. Read
+//! once, when the app starts.
+//!
+//! `VECTORCRAFT_CAPTION_STYLE` (`windows`, `adwaita`, `breeze`, `generic`) and
+//! `VECTORCRAFT_CAPTION_LAYOUT` (a GTK layout such as `close,minimize:`) override what is found.
+
+#[cfg(target_os = "linux")]
+use vectorcraft_ui_egui::titlebar::{CaptionFamily, CaptionStyle};
+
+#[cfg(target_os = "linux")]
+#[derive(Clone, Copy, PartialEq, Eq, Debug)]
+enum Desktop {
+    Gnome,
+    Kde,
+    Other,
+}
+
+/// The desktop, from the first of its variables that is set: `ubuntu:GNOME`, `Budgie:GNOME` and
+/// the like count as GNOME.
+#[cfg(target_os = "linux")]
+fn desktop(var: &dyn Fn(&str) -> Option<String>) -> Desktop {
+    for key in ["XDG_CURRENT_DESKTOP", "XDG_SESSION_DESKTOP", "DESKTOP_SESSION"] {
+        let Some(v) = var(key).map(|v| v.to_ascii_lowercase()).filter(|v| !v.trim().is_empty()) else { continue };
+        return if v.contains("gnome") {
+            Desktop::Gnome
+        } else if v.contains("kde") || v.contains("plasma") {
+            Desktop::Kde
+        } else {
+            Desktop::Other
+        };
+    }
+    if var("KDE_FULL_SESSION").is_some() {
+        Desktop::Kde
+    } else if var("GNOME_DESKTOP_SESSION_ID").is_some() {
+        Desktop::Gnome
+    } else {
+        Desktop::Other
+    }
+}
+
+/// GNOME's apps are libadwaita's whatever the GTK theme, and KWin draws Breeze; elsewhere the GTK
+/// theme says (Adwaita, adw-gtk3, Yaru, Breeze), and any other theme gets the plain buttons.
+#[cfg(target_os = "linux")]
+fn family(desktop: Desktop, gtk_theme: Option<&str>) -> CaptionFamily {
+    match desktop {
+        Desktop::Gnome => CaptionFamily::Adwaita,
+        Desktop::Kde => CaptionFamily::Breeze,
+        Desktop::Other => {
+            let theme = gtk_theme.unwrap_or_default().trim().to_ascii_lowercase();
+            if ["adwaita", "adw-gtk3", "yaru"].iter().any(|p| theme.starts_with(p)) {
+                CaptionFamily::Adwaita
+            } else if theme.starts_with("breeze") {
+                CaptionFamily::Breeze
+            } else {
+                CaptionFamily::Generic
+            }
+        }
+    }
+}
+
+/// The value of `key` in `[section]` of an INI-style file (GTK's `settings.ini`, KDE's `kwinrc`).
+#[cfg(target_os = "linux")]
+fn ini_value(text: &str, section: &str, key: &str) -> Option<String> {
+    let mut inside = false;
+    for line in text.lines().map(str::trim) {
+        if let Some(name) = line.strip_prefix('[').and_then(|l| l.strip_suffix(']')) {
+            inside = name == section;
+        } else if inside
+            && let Some((k, v)) = line.split_once('=')
+            && k.trim() == key
+        {
+            return Some(v.trim().to_string());
+        }
+    }
+    None
+}
+
+/// KWin's button order (`[org.kde.kdecoration2]` `ButtonsOnLeft` / `ButtonsOnRight`, one letter a
+/// button: `I` minimize, `A` maximize, `X` close), as a GTK layout. Missing keys keep KWin's
+/// defaults: the window menu on the left, minimize, maximize and close on the right.
+#[cfg(target_os = "linux")]
+fn kwin_layout(kwinrc: &str) -> String {
+    let side = |key: &str, default: &str| {
+        let letters = ini_value(kwinrc, "org.kde.kdecoration2", key).unwrap_or_else(|| default.to_string());
+        letters
+            .chars()
+            .filter_map(|c| match c {
+                'I' => Some("minimize"),
+                'A' => Some("maximize"),
+                'X' => Some("close"),
+                _ => None,
+            })
+            .collect::<Vec<_>>()
+            .join(",")
+    };
+    format!("{}:{}", side("ButtonsOnLeft", "MS"), side("ButtonsOnRight", "HIAX"))
+}
+
+/// What the style is worked out from.
+#[cfg(target_os = "linux")]
+#[derive(Clone, Default)]
+struct Found {
+    gtk_theme: Option<String>,
+    /// A GTK decoration layout.
+    layout: Option<String>,
+}
+
+/// The style from what was found.
+#[cfg(target_os = "linux")]
+fn resolve(desktop: Desktop, found: &Found, var: &dyn Fn(&str) -> Option<String>) -> CaptionStyle {
+    let family =
+        var("VECTORCRAFT_CAPTION_STYLE").and_then(|s| CaptionFamily::parse(&s)).unwrap_or_else(|| family(desktop, found.gtk_theme.as_deref()));
+    // GNOME's own default shows Close only; GTK's shows all three.
+    let default = if desktop == Desktop::Gnome { "appmenu:close" } else { "menu:minimize,maximize,close" };
+    let layout = var("VECTORCRAFT_CAPTION_LAYOUT").or_else(|| found.layout.clone()).unwrap_or_else(|| default.to_string());
+    CaptionStyle::from_gtk_layout(family, &layout)
+}
+
+/// A settings file, if it is there and small (a settings file is a few kilobytes).
+#[cfg(target_os = "linux")]
+fn read_text(path: &std::path::Path) -> Option<String> {
+    use std::io::Read as _;
+    const MAX: u64 = 1 << 20;
+    let mut text = String::new();
+    std::fs::File::open(path).ok()?.take(MAX).read_to_string(&mut text).ok()?;
+    Some(text)
+}
+
+/// The GTK theme and the button layout from the desktop's files: `kwinrc` on KDE, GTK 3's
+/// `settings.ini` elsewhere (on GNOME, its own setting replaces it: [`gnome_button_layout`]).
+#[cfg(target_os = "linux")]
+fn from_files(desktop: Desktop, var: &dyn Fn(&str) -> Option<String>) -> Found {
+    let config = var("XDG_CONFIG_HOME")
+        .filter(|d| std::path::Path::new(d).is_absolute())
+        .map(std::path::PathBuf::from)
+        .or_else(|| var("HOME").map(|h| std::path::Path::new(&h).join(".config")));
+    let Some(config) = config else { return Found::default() };
+    let gtk = read_text(&config.join("gtk-3.0").join("settings.ini")).unwrap_or_default();
+    let gtk_theme = ini_value(&gtk, "Settings", "gtk-theme-name");
+    let layout = if desktop == Desktop::Kde {
+        Some(kwin_layout(&read_text(&config.join("kwinrc")).unwrap_or_default()))
+    } else {
+        ini_value(&gtk, "Settings", "gtk-decoration-layout")
+    };
+    Found { gtk_theme, layout }
+}
+
+/// GNOME's `org.gnome.desktop.wm.preferences` `button-layout`, from `gsettings` (none when it is
+/// missing, as in some sandboxes, or fails).
+#[cfg(target_os = "linux")]
+fn gnome_button_layout() -> Option<String> {
+    let out = std::process::Command::new("gsettings").args(["get", "org.gnome.desktop.wm.preferences", "button-layout"]).output().ok()?;
+    if !out.status.success() {
+        return None;
+    }
+    gsettings_string(&String::from_utf8_lossy(&out.stdout))
+}
+
+/// A string as `gsettings get` prints it (`'appmenu:close'`, or `@ms 'appmenu:close'` for a maybe
+/// type), without the quotes; none when empty.
+#[cfg(target_os = "linux")]
+fn gsettings_string(printed: &str) -> Option<String> {
+    let s = printed.trim();
+    let s = s.strip_prefix("@ms ").unwrap_or(s).trim();
+    let s = s.strip_prefix('\'').and_then(|s| s.strip_suffix('\'')).unwrap_or(s).trim();
+    (!s.is_empty()).then(|| s.to_string())
+}
+
+/// The desktop's caption buttons, for `Services::caption_style`; `None` off Linux (Windows keeps its
+/// own buttons, macOS its traffic lights).
+pub fn detect() -> Option<vectorcraft_ui_egui::titlebar::CaptionStyle> {
+    #[cfg(target_os = "linux")]
+    {
+        let var = |k: &str| std::env::var(k).ok();
+        let desktop = desktop(&var);
+        let mut found = from_files(desktop, &var);
+        // GNOME keeps its button layout in its settings (what Settings and Tweaks change), not in
+        // GTK's settings.ini.
+        if desktop == Desktop::Gnome
+            && let Some(layout) = gnome_button_layout()
+        {
+            found.layout = Some(layout);
+        }
+        Some(resolve(desktop, &found, &var))
+    }
+    #[cfg(not(target_os = "linux"))]
+    {
+        None
+    }
+}
+
+#[cfg(all(test, target_os = "linux"))]
+mod tests {
+    #![allow(clippy::unwrap_used)]
+    use super::*;
+    use vectorcraft_ui_egui::titlebar::Caption::{Close, Maximize, Minimize};
+
+    fn env(pairs: &[(&str, &str)]) -> impl Fn(&str) -> Option<String> + use<> {
+        let pairs: Vec<(String, String)> = pairs.iter().map(|(k, v)| (k.to_string(), v.to_string())).collect();
+        move |k| pairs.iter().find(|(key, _)| key == k).map(|(_, v)| v.clone())
+    }
+
+    #[test]
+    fn gsettings_strings_lose_their_quotes() {
+        assert_eq!(gsettings_string("'appmenu:minimize,maximize,close'\n").as_deref(), Some("appmenu:minimize,maximize,close"));
+        assert_eq!(gsettings_string("@ms 'close,minimize:'").as_deref(), Some("close,minimize:"));
+        assert_eq!(gsettings_string("''\n"), None);
+        assert_eq!(gsettings_string(""), None);
+        // GNOME's layout then sets the buttons (Minimize and Maximize turned on in Tweaks).
+        let found = Found { gtk_theme: None, layout: gsettings_string("'appmenu:minimize,maximize,close'") };
+        let s = resolve(Desktop::Gnome, &found, &env(&[]));
+        assert_eq!((s.family, s.left, s.right), (CaptionFamily::Adwaita, vec![], vec![Minimize, Maximize, Close]));
+    }
+
+    #[test]
+    fn the_desktop_comes_from_the_first_variable_set() {
+        assert_eq!(desktop(&env(&[("XDG_CURRENT_DESKTOP", "ubuntu:GNOME")])), Desktop::Gnome);
+        assert_eq!(desktop(&env(&[("XDG_CURRENT_DESKTOP", "KDE")])), Desktop::Kde);
+        assert_eq!(desktop(&env(&[("XDG_CURRENT_DESKTOP", "X-Cinnamon"), ("KDE_FULL_SESSION", "true")])), Desktop::Other);
+        assert_eq!(desktop(&env(&[("XDG_CURRENT_DESKTOP", " "), ("DESKTOP_SESSION", "plasma")])), Desktop::Kde);
+        assert_eq!(desktop(&env(&[("KDE_FULL_SESSION", "true")])), Desktop::Kde);
+        assert_eq!(desktop(&env(&[("GNOME_DESKTOP_SESSION_ID", "this-is-deprecated")])), Desktop::Gnome);
+        assert_eq!(desktop(&env(&[])), Desktop::Other);
+    }
+
+    #[test]
+    fn the_family_follows_the_desktop_then_the_gtk_theme() {
+        assert_eq!(family(Desktop::Gnome, Some("Arc")), CaptionFamily::Adwaita);
+        assert_eq!(family(Desktop::Kde, None), CaptionFamily::Breeze);
+        assert_eq!(family(Desktop::Other, Some("adw-gtk3-dark")), CaptionFamily::Adwaita);
+        assert_eq!(family(Desktop::Other, Some("Yaru-blue")), CaptionFamily::Adwaita);
+        assert_eq!(family(Desktop::Other, Some("Breeze-Dark")), CaptionFamily::Breeze);
+        assert_eq!(family(Desktop::Other, Some("Mint-Y")), CaptionFamily::Generic);
+        assert_eq!(family(Desktop::Other, None), CaptionFamily::Generic);
+    }
+
+    #[test]
+    fn kwin_button_letters_become_a_gtk_layout() {
+        assert_eq!(kwin_layout(""), ":minimize,maximize,close", "KWin's defaults");
+        let rc = "[Desktops]\nButtonsOnLeft=X\n[org.kde.kdecoration2]\nButtonsOnLeft=XIA\nButtonsOnRight=\nlibrary=org.kde.breeze\n";
+        assert_eq!(kwin_layout(rc), "close,minimize,maximize:");
+        assert_eq!(ini_value(rc, "org.kde.kdecoration2", "library").as_deref(), Some("org.kde.breeze"));
+        assert_eq!(ini_value(rc, "nope", "library"), None);
+        // Garbage is just no buttons on that side, never a panic.
+        assert_eq!(kwin_layout("[org.kde.kdecoration2]\nButtonsOnRight=ÿ€=X\n"), ":close");
+    }
+
+    #[test]
+    fn layouts_and_overrides_resolve_to_a_style() {
+        let none = env(&[]);
+        let gnome = resolve(Desktop::Gnome, &Found::default(), &none);
+        assert_eq!((gnome.family, gnome.left, gnome.right), (CaptionFamily::Adwaita, vec![], vec![Close]), "GNOME's default layout");
+        // KDE, SteamOS's desktop mode among them: Breeze's buttons in KWin's default order.
+        let kde = resolve(Desktop::Kde, &Found { layout: Some(kwin_layout("")), ..Found::default() }, &none);
+        assert_eq!((kde.family, kde.left, kde.right), (CaptionFamily::Breeze, vec![], vec![Minimize, Maximize, Close]));
+        let found = Found { gtk_theme: Some("Mint-Y".into()), layout: Some("close:".into()) };
+        let other = resolve(Desktop::Other, &found, &none);
+        assert_eq!((other.family, other.left, other.right), (CaptionFamily::Generic, vec![Close], vec![]));
+        let forced = env(&[("VECTORCRAFT_CAPTION_STYLE", "Breeze"), ("VECTORCRAFT_CAPTION_LAYOUT", "minimize,close:maximize")]);
+        let s = resolve(Desktop::Gnome, &found, &forced);
+        assert_eq!((s.family, s.left, s.right), (CaptionFamily::Breeze, vec![Minimize, Close], vec![Maximize]));
+        // An unknown override name falls back to what was found.
+        assert_eq!(resolve(Desktop::Kde, &found, &env(&[("VECTORCRAFT_CAPTION_STYLE", "aqua")])).family, CaptionFamily::Breeze);
+    }
+
+    #[test]
+    fn settings_files_are_read_from_the_config_dir() {
+        let dir = std::env::temp_dir().join(format!("vectorcraft-caption-{}", std::process::id()));
+        std::fs::create_dir_all(dir.join("gtk-3.0")).unwrap();
+        std::fs::write(dir.join("gtk-3.0/settings.ini"), "[Settings]\ngtk-theme-name=Yaru\ngtk-decoration-layout=close,maximize:\n").unwrap();
+        std::fs::write(dir.join("kwinrc"), "[org.kde.kdecoration2]\nButtonsOnRight=AX\n").unwrap();
+        let var = env(&[("XDG_CONFIG_HOME", dir.to_str().unwrap())]);
+        let other = from_files(Desktop::Other, &var);
+        assert_eq!((other.gtk_theme.as_deref(), other.layout.as_deref()), (Some("Yaru"), Some("close,maximize:")));
+        let kde = from_files(Desktop::Kde, &var);
+        assert_eq!(kde.layout.as_deref(), Some(":maximize,close"));
+        // A relative XDG_CONFIG_HOME is ignored, as the XDG spec says; no HOME: nothing found.
+        assert_eq!(from_files(Desktop::Other, &env(&[("XDG_CONFIG_HOME", "rel")])).layout, None);
+        let _ = std::fs::remove_dir_all(dir);
+    }
+}
diff --git a/apps/vectorcraft/src/main.rs b/apps/vectorcraft/src/main.rs
--- a/apps/vectorcraft/src/main.rs
+++ b/apps/vectorcraft/src/main.rs
@@ -23,6 +23,7 @@ compile_error!("windows7 requires --target x86_64-win7-windows-msvc; the ordinar
 compile_error!("the win7 target requires --no-default-features --features windows7");
 
 mod automation_args;
+mod caption_style;
 mod clipboard;
 #[cfg(feature = "wgpu")]
 mod compositor_gpu;
@@ -569,6 +570,8 @@ fn services(parent: Parent, ui: egui::Context) -> Services {
         screen_color: Some(mac_screen_color::sampler(sampled_ui)),
         // File → Print: the system's printers and print queue.
         print: Some(Box::new(printing::SystemPrint)),
+        // Classic OG's caption buttons look like the desktop's (Linux).
+        caption_style: caption_style::detect(),
         ..Default::default()
     }
 }
diff --git a/crates/engine/src/cmd/prefscmds.rs b/crates/engine/src/cmd/prefscmds.rs
--- a/crates/engine/src/cmd/prefscmds.rs
+++ b/crates/engine/src/cmd/prefscmds.rs
@@ -268,7 +268,7 @@ pub const PREF_SPECS: &[PrefSpec] = &[
         "Brightness",
         choice(&[("dark", "Dark"), ("mediumDark", "Medium Dark"), ("mediumLight", "Medium Light"), ("light", "Light")])
     ),
-    p!("uiColorScheme", "User Interface", "", "Color Scheme", choice(&[("modern", "Modern"), ("classic", "Classic")])),
+    p!("uiColorScheme", "User Interface", "", "Color Scheme", choice(&[("modern", "Modern"), ("classic", "Classic"), ("classicOg", "Classic OG")])),
     p!(
         "uiAccent",
         "User Interface",
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
@@ -916,8 +916,9 @@ pub struct Prefs {
     pub ui_brightness: String,
     /// User Interface › Accent Color: the colour of what is on, chosen or focused.
     pub ui_accent: String,
-    /// User Interface › Color Scheme: "modern" (with the accent above) or "classic" (neutral
-    /// greys and a blue accent).
+    /// User Interface › Color Scheme: "modern" (with the accent above), "classic" (neutral greys
+    /// and a blue accent) or "classicOg" (VectorCraft 0.11's look: Classic's greys on flat panels,
+    /// with tab strips, pill buttons and the desktop's caption buttons, as in PhotoCraft).
     pub ui_color_scheme: String,
     pub canvas_color: String,
     pub auto_collapse_icon_panels: bool,
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
@@ -26,10 +26,16 @@ const WIDGET_H: f32 = 22.0;
 /// On macOS, under the system's transparent title bar, its empty space drags and zooms the window
 /// too ([`titlebar::drag_area`]). With the system title bar (Windows and Linux, `system_title_bar`) the OS draws its own icon and
 /// title, so the in-app bar shows Home, menus and workspace controls only: no brand mark.
+///
+/// With the Classic OG colour scheme it is laid out as PhotoCraft's: the caption buttons follow the
+/// desktop and may sit at either end ([`titlebar::reserved`]), the workspace switcher is a boxed
+/// dropdown, and the document's name (or VectorCraft) is centred in the room between the menus and
+/// the controls.
 pub fn app_bar(app: &mut VectorcraftApp, ui: &mut Ui) {
     let t = Tokens::get(ui.ctx());
     let custom = app.custom_titlebar;
     let system = system_title_bar(app);
+    let (caption_left, caption_right) = titlebar::reserved(app, ui.ctx());
     let left = app.titlebar_inset.clamp(8.0, f32::from(i8::MAX)) as i8;
     let frame = egui::Frame::NONE.fill(t.app_bar).inner_margin(egui::Margin { left, right: if custom { 0 } else { 14 }, top: 0, bottom: 0 });
     let bar = egui::Panel::top("app_bar").exact_size(APP_BAR_HEIGHT).frame(frame.stroke(Stroke::new(1.0, t.border))).show(ui, |ui| {
@@ -39,6 +45,11 @@ pub fn app_bar(app: &mut VectorcraftApp, ui: &mut Ui) {
             titlebar::drag_area(ui, ui.max_rect());
         }
         ui.horizontal_centered(|ui| {
+            // Caption buttons at the left end (a desktop that puts them there): the bar starts
+            // after them.
+            if caption_left > 0.0 {
+                ui.add_space((caption_left - f32::from(left) + 4.0).max(0.0));
+            }
             // With the system title bar the OS draws its own icon: no in-app brand mark.
             if !system {
                 let (r, _) = ui.allocate_exact_size(vec2(MARK, MARK), Sense::hover());
@@ -56,7 +67,7 @@ pub fn app_bar(app: &mut VectorcraftApp, ui: &mut Ui) {
             // short, Discord goes first (it is also under Help), then the search icon (the palette
             // stays under its shortcut), then the workspace switcher narrows.
             let full = ui.max_rect();
-            let right_edge = if custom { full.right() - titlebar::WIDTH - 10.0 } else { full.right() };
+            let right_edge = if custom { full.right() - caption_right - 10.0 } else { full.right() };
             let right = egui::Rect::from_min_max(egui::pos2(menus_end + 8.0, full.top()), egui::pos2(right_edge, full.bottom()));
             let room = right.width();
             // Built-in workspace names translate; the user's own stay as typed.
@@ -74,7 +85,18 @@ pub fn app_bar(app: &mut VectorcraftApp, ui: &mut Ui) {
             let ui = &mut rui;
             // Workspace switcher: shows the current workspace, opens Window → Workspace.
             let (wr, wresp) = ui.allocate_exact_size(vec2(ws_w, WIDGET_H), Sense::click());
-            ui.painter().rect_filled(wr, CornerRadius::same(4), if wresp.hovered() { t.hover } else { t.panel });
+            if t.flat {
+                // Classic OG: a boxed dropdown, as PhotoCraft's workspace switcher.
+                ui.painter().rect(
+                    wr,
+                    CornerRadius::same(4),
+                    if wresp.hovered() { t.hover } else { t.input },
+                    Stroke::new(1.0, t.input_border),
+                    egui::StrokeKind::Inside,
+                );
+            } else {
+                ui.painter().rect_filled(wr, CornerRadius::same(4), if wresp.hovered() { t.hover } else { t.panel });
+            }
             ui.painter().with_clip_rect(wr.shrink2(vec2(4.0, 0.0))).galley(wr.left_center() + vec2(10.0, -ws.size().y / 2.0), ws, t.text);
             icons::paint(ui, "chevron-down", egui::Rect::from_center_size(wr.right_center() - vec2(12.0, 0.0), vec2(12.0, 12.0)), t.text_dim);
             let wresp = wresp.on_hover_text(tl!("Switch workspace"));
@@ -95,7 +117,10 @@ pub fn app_bar(app: &mut VectorcraftApp, ui: &mut Ui) {
                 ui.add_space(10.0);
                 crate::community::discord_button(app, ui, false);
             }
-            if app.services.native_menu.is_some() {
+            if t.flat && !system {
+                // Classic OG: PhotoCraft's title, in the room between the menus and the controls.
+                bar_title(app, ui, &t, full, menus_end, ui.min_rect().left());
+            } else if app.services.native_menu.is_some() {
                 let title = ui.painter().layout_no_wrap("VectorCraft".into(), egui::FontId::proportional(13.5), t.text);
                 let title_rect = egui::Rect::from_center_size(full.center(), title.size());
                 // Keep the title centered only while the interactive groups leave room for it.
@@ -110,6 +135,33 @@ pub fn app_bar(app: &mut VectorcraftApp, ui: &mut Ui) {
     }
 }
 
+/// Room the bar's title keeps from the menus and the controls (PhotoCraft's).
+const TITLE_GAP: f32 = 16.0;
+
+/// The bar's title with Classic OG, as in PhotoCraft: the active document's name (`*` before it with
+/// unsaved changes, as on its tab) or VectorCraft. It is centred on the window, slid along when that
+/// would bring it near the menus (which end at `from`) or the controls (which start at `to`), and
+/// shortened next to the menus when the room between them is too small for all of it; it is left
+/// out when only a sliver is free.
+fn bar_title(app: &VectorcraftApp, ui: &Ui, t: &Tokens, full: egui::Rect, from: f32, to: f32) {
+    let title = app.session.active().map(|d| format!("{}{}", if d.is_dirty() { "*" } else { "" }, d.title())).unwrap_or_else(|| "VectorCraft".into());
+    let font = theme::semibold(13.0);
+    let galley = ui.painter().layout_no_wrap(title.clone(), font.clone(), t.text_dim);
+    let y = full.center().y - galley.size().y / 2.0;
+    let (lo, hi) = (from + TITLE_GAP, to - TITLE_GAP - galley.size().x);
+    // `hi >= lo` is false when either is NaN, so the clamp below has a valid range.
+    if hi >= lo {
+        // The window's centre: the bar's own rect starts after its left margin.
+        let x = (ui.ctx().content_rect().center().x - galley.size().x / 2.0).clamp(lo, hi);
+        ui.painter().galley(egui::pos2(x, y), galley, t.text_dim);
+    } else if to - from - 2.0 * TITLE_GAP > 80.0 {
+        let mut job = egui::text::LayoutJob::simple_singleline(title, font, t.text_dim);
+        job.wrap = egui::text::TextWrapping::truncate_at_width(to - from - 2.0 * TITLE_GAP);
+        let g = ui.painter().layout_job(job);
+        ui.painter().galley(egui::pos2(lo, y), g, t.text_dim);
+    }
+}
+
 /// Whether the window shows the system's title bar instead of the app drawing its own: Windows
 /// and Linux with Preferences › User Interface › System Title Bar. macOS always has system
 /// decorations, but keeps the in-app brand mark, so it never counts as system mode here; nor
diff --git a/crates/ui-egui/src/community.rs b/crates/ui-egui/src/community.rs
--- a/crates/ui-egui/src/community.rs
+++ b/crates/ui-egui/src/community.rs
@@ -12,8 +12,9 @@ const DISCORD: Color32 = Color32::from_rgb(0x58, 0x65, 0xF2);
 
 /// The Discord button's label, height and width.
 fn discord_layout(ui: &Ui, large: bool) -> (std::sync::Arc<egui::Galley>, f32, f32) {
-    let (h, font, label) =
-        if large { (36.0, theme::semibold(14.0), tl!("Join our Discord")) } else { (24.0, egui::FontId::proportional(12.0), "Discord") };
+    // Classic OG: VectorCraft 0.11's semibold label in the app bar.
+    let bar_font = if Tokens::get(ui.ctx()).flat { theme::semibold(12.0) } else { egui::FontId::proportional(12.0) };
+    let (h, font, label) = if large { (36.0, theme::semibold(14.0), tl!("Join our Discord")) } else { (24.0, bar_font, "Discord") };
     let galley = ui.painter().layout_no_wrap(label.to_string(), font, Color32::PLACEHOLDER);
     let w = galley.size().x + h * 0.55 + h * 0.9;
     (galley, h, w)
@@ -24,14 +25,15 @@ pub fn discord_width(ui: &Ui, large: bool) -> f32 {
     discord_layout(ui, large).2
 }
 
-/// The Discord button: compact and quiet for the app bar, large for About / Home.
+/// The Discord button: compact and quiet for the app bar, large for About / Home. Classic OG keeps
+/// VectorCraft 0.11's Discord-coloured pill in the app bar too.
 pub fn discord_button(app: &mut VectorcraftApp, ui: &mut Ui, large: bool) -> egui::Response {
     let (galley, h, w) = discord_layout(ui, large);
     let icon = h * 0.55;
     let (r, resp) = ui.allocate_exact_size(vec2(w, h), Sense::click());
     let at = egui::pos2(0.0, r.center().y - galley.size().y / 2.0);
     let ir = egui::Rect::from_center_size(r.left_center() + vec2(h * 0.35 + icon / 2.0, 0.0), vec2(icon, icon));
-    if large {
+    if large || Tokens::get(ui.ctx()).flat {
         let fill = if resp.hovered() { DISCORD.gamma_multiply(0.85) } else { DISCORD };
         ui.painter().rect_filled(r, CornerRadius::same((h / 2.0) as u8), fill);
         icons::paint(ui, "message-circle", ir, Color32::WHITE);
diff --git a/crates/ui-egui/src/dock.rs b/crates/ui-egui/src/dock.rs
--- a/crates/ui-egui/src/dock.rs
+++ b/crates/ui-egui/src/dock.rs
@@ -95,6 +95,10 @@ pub fn set_collapsed(app: &mut VectorcraftApp, collapsed: bool) {
 pub(crate) fn collapse_header(ui: &mut Ui, collapsed: bool) -> bool {
     let t = Tokens::get(ui.ctx());
     let (hdr, resp) = ui.allocate_exact_size(vec2(ui.available_width(), HEADER), Sense::click());
+    if t.flat {
+        // Classic OG: VectorCraft 0.11's strip, in the tab strips' grey.
+        ui.painter().rect_filled(hdr, 0.0, t.tab_strip);
+    }
     let arrow = Rect::from_min_size(hdr.right_top() + vec2(-16.0, 1.0), vec2(12.0, 12.0));
     if resp.hovered() {
         ui.painter().rect_filled(arrow, CornerRadius::same(2), t.hover);
@@ -175,7 +179,8 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
             if collapsed && collapse_header(ui, true) {
                 toggle(app, false);
             }
-            egui::Frame::NONE.inner_margin(egui::Margin::symmetric(1, 4)).show(ui, |ui| {
+            let margin = if t.flat { egui::Margin::symmetric(4, 6) } else { egui::Margin::symmetric(1, 4) };
+            egui::Frame::NONE.inner_margin(margin).show(ui, |ui| {
                 ui.spacing_mut().item_spacing.y = 2.0;
                 // The icons scroll in a window too short for them.
                 widgets::strip_scroll(ui, "icon_column", |ui| {
@@ -226,7 +231,13 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
 fn tab_strip(app: &mut VectorcraftApp, ui: &mut Ui, active: DockTab) -> bool {
     let t = Tokens::get(ui.ctx());
     let (strip, _) = ui.allocate_exact_size(vec2(ui.available_width(), 33.0), Sense::hover());
-    ui.painter().line_segment([strip.left_bottom(), strip.right_bottom()], Stroke::new(1.0, t.divider));
+    if t.flat {
+        // Classic OG: VectorCraft 0.11's tab strip, the shown tab in the panel's grey on it.
+        ui.painter().rect_filled(strip, 0.0, t.tab_strip);
+        ui.painter().line_segment([strip.left_bottom(), strip.right_bottom()], Stroke::new(1.0, t.border));
+    } else {
+        ui.painter().line_segment([strip.left_bottom(), strip.right_bottom()], Stroke::new(1.0, t.divider));
+    }
     let outside = ui.input(|i| i.pointer.interact_pos()).is_some_and(|p| !strip.contains(p));
     // What a drag out of the strip floats: the panels, the one shown, where its tab began.
     let mut tear: Option<(Vec<&'static str>, &'static str, f32)> = None;
@@ -315,11 +326,16 @@ pub(crate) fn panel_tabs(
             .sense(Sense::click_and_drag())
             .focus_stroke(Stroke::new(1.0, t.accent))
             .show_at(ui, rect, |ui, _| {
-                if i == selected {
+                if i == selected && t.flat {
+                    // Classic OG: VectorCraft 0.11's tabs, the shown one in the panel's grey.
+                    ui.painter().rect_filled(rect, 0.0, t.panel);
+                } else if i == selected {
                     ui.painter().rect_filled(rect.shrink2(vec2(3.0, 5.0)), 6.0, t.button);
                 }
                 ui.painter().with_clip_rect(rect.intersect(ui.clip_rect())).galley(rect.center() - galley.size() / 2.0, galley, t.text);
-                let _ = separators;
+                if separators && t.flat {
+                    ui.painter().line_segment([rect.right_top(), rect.right_bottom()], Stroke::new(1.0, t.border));
+                }
             });
         out.responses.push((i, if shrunk { resp.on_hover_text(*label) } else { resp }));
         out.right = rect.right();
diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv
--- a/crates/ui-egui/src/i18n/bg.tsv
+++ b/crates/ui-egui/src/i18n/bg.tsv
@@ -5159,6 +5159,7 @@ theme	Appearance	Външен вид
 # User Interface › Color Scheme
 	Color Scheme	Цветова схема
 	Modern	Модерна
+	Classic OG	Класически OG
 
 # Symbol Options › Export Type
 	Export Type:	Тип на експортиране:
diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv
--- a/crates/ui-egui/src/i18n/cs.tsv
+++ b/crates/ui-egui/src/i18n/cs.tsv
@@ -5186,6 +5186,7 @@ mark-style	Japanese	Japonské
 # User Interface › Color Scheme
 	Color Scheme	Barevné schéma
 	Modern	Moderní
+	Classic OG	Klasické OG
 
 # Symbol Options › Export Type
 	Export Type:	Typ exportu:
diff --git a/crates/ui-egui/src/i18n/da.tsv b/crates/ui-egui/src/i18n/da.tsv
--- a/crates/ui-egui/src/i18n/da.tsv
+++ b/crates/ui-egui/src/i18n/da.tsv
@@ -4947,6 +4947,7 @@ mark-style	Japanese	Japansk
 	Violet	Violet
 	Color Scheme	Farveskema
 	Modern	Moderne
+	Classic OG	Klassisk OG
 @msg	`{key}` must be an array of these names: {_1}	`{key}` skal være et array af disse navne: {_1}
 	Mode	Tilstand
 	Blur Mode	Slørtilstand
diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv
--- a/crates/ui-egui/src/i18n/de.tsv
+++ b/crates/ui-egui/src/i18n/de.tsv
@@ -4765,6 +4765,7 @@ effect	Soft	Weich
 # User Interface › Color Scheme
 	Color Scheme	Farbschema
 	Modern	Modern
+	Classic OG	Klassisch OG
 
 # Symbol Options › Export Type
 	Export Type:	Exporttyp:
diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv
--- a/crates/ui-egui/src/i18n/el.tsv
+++ b/crates/ui-egui/src/i18n/el.tsv
@@ -5138,6 +5138,7 @@ effect	Points	Σημεία
 # User Interface › Color Scheme
 	Color Scheme	Χρωματικός συνδυασμός
 	Modern	Σύγχρονο
+	Classic OG	Κλασικό OG
 
 # Symbol Options › Export Type
 	Export Type:	Τύπος εξαγωγής:
diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv
--- a/crates/ui-egui/src/i18n/es.tsv
+++ b/crates/ui-egui/src/i18n/es.tsv
@@ -5128,6 +5128,7 @@ mark-style	Japanese	Japonesas
 # User Interface › Color Scheme
 	Color Scheme	Esquema de color
 	Modern	Moderno
+	Classic OG	Clásico OG
 
 # Symbol Options › Export Type
 	Export Type:	Tipo de exportación:
diff --git a/crates/ui-egui/src/i18n/fi.tsv b/crates/ui-egui/src/i18n/fi.tsv
--- a/crates/ui-egui/src/i18n/fi.tsv
+++ b/crates/ui-egui/src/i18n/fi.tsv
@@ -5150,6 +5150,7 @@ mark-style	Japanese	Japanilainen
 # User Interface › Color Scheme
 	Color Scheme	Väriteema
 	Modern	Moderni
+	Classic OG	Perinteinen OG
 
 # Symbol Options › Export Type
 	Export Type:	Vientityyppi:
diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv
--- a/crates/ui-egui/src/i18n/fr.tsv
+++ b/crates/ui-egui/src/i18n/fr.tsv
@@ -5185,6 +5185,7 @@ theme	Appearance	Apparence
 # User Interface › Color Scheme
 	Color Scheme	Jeu de couleurs
 	Modern	Moderne
+	Classic OG	Classique OG
 
 # Symbol Options › Export Type
 	Export Type:	Type d'exportation :
diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv
--- a/crates/ui-egui/src/i18n/hu.tsv
+++ b/crates/ui-egui/src/i18n/hu.tsv
@@ -5172,6 +5172,7 @@ imageTrace	Outline	Körvonal
 # User Interface › Color Scheme
 	Color Scheme	Színséma
 	Modern	Modern
+	Classic OG	Klasszikus OG
 
 # Symbol Options › Export Type
 	Export Type:	Exportálás típusa:
diff --git a/crates/ui-egui/src/i18n/id.tsv b/crates/ui-egui/src/i18n/id.tsv
--- a/crates/ui-egui/src/i18n/id.tsv
+++ b/crates/ui-egui/src/i18n/id.tsv
@@ -4947,6 +4947,7 @@ mark-style	Japanese	Jepang
 	Violet	Violet
 	Color Scheme	Skema warna
 	Modern	Modern
+	Classic OG	Klasik OG
 @msg	`{key}` must be an array of these names: {_1}	`{key}` harus berupa array dari nama-nama ini: {_1}
 	Mode	Mode
 	Blur Mode	Mode blur
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
@@ -4799,6 +4799,7 @@ lighting	Light	Luce
 # User Interface › Color Scheme
 	Color Scheme	Combinazione colori
 	Modern	Moderno
+	Classic OG	Classico OG
 
 # Symbol Options › Export Type
 	Export Type:	Tipo di esportazione:
diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv
--- a/crates/ui-egui/src/i18n/ja.tsv
+++ b/crates/ui-egui/src/i18n/ja.tsv
@@ -4839,6 +4839,7 @@ lighting	Light	ライト
 # User Interface › Color Scheme
 	Color Scheme	配色
 	Modern	モダン
+	Classic OG	クラシック OG
 
 # Symbol Options › Export Type
 	Export Type:	書き出しタイプ:
diff --git a/crates/ui-egui/src/i18n/ka.tsv b/crates/ui-egui/src/i18n/ka.tsv
--- a/crates/ui-egui/src/i18n/ka.tsv
+++ b/crates/ui-egui/src/i18n/ka.tsv
@@ -4917,6 +4917,7 @@ imageTrace	Outline	კონტური
 	Dot	წერტილი
 	Color Scheme	ფერის სქემა
 	Modern	თანამედროვე
+	Classic OG	კლასიკური OG
 	Export Type:	ექსპორტის ტიპი:
 	Movie Clip	ანიმაციური კლიპი
 @msg	name is empty	სახელი ცარიელია
diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv
--- a/crates/ui-egui/src/i18n/ko.tsv
+++ b/crates/ui-egui/src/i18n/ko.tsv
@@ -4762,6 +4762,7 @@ lighting	Light	광원
 # User Interface › Color Scheme
 	Color Scheme	색 구성표
 	Modern	모던
+	Classic OG	클래식 OG
 
 # Symbol Options › Export Type
 	Export Type:	내보내기 유형:
diff --git a/crates/ui-egui/src/i18n/nl.tsv b/crates/ui-egui/src/i18n/nl.tsv
--- a/crates/ui-egui/src/i18n/nl.tsv
+++ b/crates/ui-egui/src/i18n/nl.tsv
@@ -4959,6 +4959,7 @@ mark-style	Japanese	Japans
 	Violet	Violet
 	Color Scheme	Kleurschema
 	Modern	Modern
+	Classic OG	Klassiek OG
 @msg	`{key}` must be an array of these names: {_1}	`{key}` moet een matrix van deze namen zijn: {_1}
 	Mode	Modus
 	Blur Mode	Vervagingsmodus
diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv
--- a/crates/ui-egui/src/i18n/pl.tsv
+++ b/crates/ui-egui/src/i18n/pl.tsv
@@ -5038,6 +5038,7 @@ mark-style	Japanese	Japońskie
 # User Interface › Color Scheme
 	Color Scheme	Schemat kolorów
 	Modern	Nowoczesny
+	Classic OG	Klasyczne OG
 
 # Symbol Options › Export Type
 	Export Type:	Typ eksportu:
diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv
--- a/crates/ui-egui/src/i18n/pt-br.tsv
+++ b/crates/ui-egui/src/i18n/pt-br.tsv
@@ -5105,6 +5105,7 @@ mark-style	Japanese	Japonesas
 # User Interface › Color Scheme
 	Color Scheme	Esquema de cores
 	Modern	Moderno
+	Classic OG	Clássico OG
 
 # Symbol Options › Export Type
 @msg	name is empty	o nome está vazio
diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv
--- a/crates/ui-egui/src/i18n/ru.tsv
+++ b/crates/ui-egui/src/i18n/ru.tsv
@@ -4795,6 +4795,7 @@ lighting	Light	Свет
 # User Interface › Color Scheme
 	Color Scheme	Цветовая схема
 	Modern	Современная
+	Classic OG	Классический OG
 
 # Symbol Options › Export Type
 	Export Type:	Тип экспорта:
diff --git a/crates/ui-egui/src/i18n/sv.tsv b/crates/ui-egui/src/i18n/sv.tsv
--- a/crates/ui-egui/src/i18n/sv.tsv
+++ b/crates/ui-egui/src/i18n/sv.tsv
@@ -5142,6 +5142,7 @@ theme	Appearance	Utseende
 # User Interface › Color Scheme
 	Color Scheme	Färgschema
 	Modern	Modern
+	Classic OG	Klassisk OG
 
 # Symbol Options › Export Type
 	Export Type:	Exporttyp:
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
@@ -4779,6 +4779,7 @@ lighting	Light	Світло
 # User Interface › Color Scheme
 	Color Scheme	Колірна схема
 	Modern	Сучасна
+	Classic OG	Класичний OG
 
 # Symbol Options › Export Type
 	Export Type:	Тип експорту:
diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv
--- a/crates/ui-egui/src/i18n/vi.tsv
+++ b/crates/ui-egui/src/i18n/vi.tsv
@@ -4779,6 +4779,7 @@ theme	Appearance	Giao diện
 # User Interface › Color Scheme
 	Color Scheme	Bảng phối màu
 	Modern	Hiện đại
+	Classic OG	Cổ điển OG
 
 # Symbol Options › Export Type
 	Export Type:	Kiểu xuất:
diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv
--- a/crates/ui-egui/src/i18n/zh-hans.tsv
+++ b/crates/ui-egui/src/i18n/zh-hans.tsv
@@ -5019,6 +5019,7 @@ lighting	Light	光源
 # User Interface › Color Scheme
 	Color Scheme	配色方案
 	Modern	现代
+	Classic OG	经典 OG
 
 # Symbol Options › Export Type
 	Export Type:	导出类型:
diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv
--- a/crates/ui-egui/src/i18n/zh-hant.tsv
+++ b/crates/ui-egui/src/i18n/zh-hant.tsv
@@ -4829,6 +4829,7 @@ lighting	Light	光源
 # User Interface › Color Scheme
 	Color Scheme	配色方案
 	Modern	現代
+	Classic OG	經典 OG
 
 # Symbol Options › Export Type
 	Export Type:	轉存類型:
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
@@ -299,6 +299,10 @@ pub struct Services {
     /// default one would never come back once the canvas hid it: the canvas keeps the system
     /// pointer visible next to the tool glyph ([`cursors::hide_system_pointer`], #2450).
     pub no_cursor_theme: bool,
+    /// The desktop's caption buttons (Linux: GNOME's, KDE's or plain round ones, in the desktop's
+    /// button layout), which the custom title bar draws with the Classic OG colour scheme
+    /// ([`titlebar`]). Without it, Windows' buttons.
+    pub caption_style: Option<titlebar::CaptionStyle>,
 }
 
 /// Cached canvas raster.
diff --git a/crates/ui-egui/src/panel_docking.rs b/crates/ui-egui/src/panel_docking.rs
--- a/crates/ui-egui/src/panel_docking.rs
+++ b/crates/ui-egui/src/panel_docking.rs
@@ -680,11 +680,19 @@ pub(crate) fn show(app: &mut VectorcraftApp, ui: &mut egui::Ui) -> bool {
         }
         let mut style = craft_ui::docking::DockStyle::from_ui(ui);
         style.tab_height = 33.0;
-        // The dock is a card (`theme::card`): its panes and tab strip lie on the card's colour
-        // (square fills would cover its rounded corners). Floating groups get the fills below.
-        style.background = egui::Color32::TRANSPARENT;
-        style.tab_background = egui::Color32::TRANSPARENT;
-        style.active_background = t.button;
+        if t.flat {
+            // Classic OG: VectorCraft 0.11's dock, the shown tab in the panel's grey on a darker
+            // tab strip.
+            style.background = t.panel;
+            style.tab_background = t.tab_strip;
+            style.active_background = t.panel;
+        } else {
+            // The dock is a card (`theme::card`): its panes and tab strip lie on the card's colour
+            // (square fills would cover its rounded corners). Floating groups get the fills below.
+            style.background = egui::Color32::TRANSPARENT;
+            style.tab_background = egui::Color32::TRANSPARENT;
+            style.active_background = t.button;
+        }
         style.text = t.text_strong;
         style.inactive_text = t.text_dim;
         style.border = egui::Stroke::new(1.0, t.border);
diff --git a/crates/ui-egui/src/theme.rs b/crates/ui-egui/src/theme.rs
--- a/crates/ui-egui/src/theme.rs
+++ b/crates/ui-egui/src/theme.rs
@@ -85,22 +85,26 @@ impl Accent {
     }
 }
 
-/// User Interface › Color Scheme: Modern (the default: slate greys and the chosen accent) or
-/// Classic (the neutral greys and blue accent of earlier versions). Both keep the same layout,
-/// cards and corners; only the colours differ.
+/// User Interface › Color Scheme: Modern (the default: slate greys and the chosen accent), Classic
+/// (the neutral greys and blue accent of earlier versions) or Classic OG (VectorCraft 0.11's look,
+/// with the desktop's window buttons as in PhotoCraft). Modern and Classic keep the same layout,
+/// cards and corners; only the colours differ. Classic OG also brings back 0.11's chrome
+/// ([`Tokens::flat`]).
 #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
 pub enum Scheme {
     #[default]
     Modern,
     Classic,
+    ClassicOg,
 }
 
 impl Scheme {
-    pub const ALL: [Scheme; 2] = [Scheme::Modern, Scheme::Classic];
+    pub const ALL: [Scheme; 3] = [Scheme::Modern, Scheme::Classic, Scheme::ClassicOg];
     pub fn id(self) -> &'static str {
         match self {
             Scheme::Modern => "modern",
             Scheme::Classic => "classic",
+            Scheme::ClassicOg => "classicOg",
         }
     }
     pub fn parse(s: &str) -> Option<Self> {
@@ -164,6 +168,12 @@ pub struct Tokens {
     pub axis_x: Color32,
     pub axis_y: Color32,
     pub axis_z: Color32,
+    /// VectorCraft 0.11's chrome (Classic OG): flat panels edged in the `border` colour instead of
+    /// [`card`]s, panel tabs on a `tab_strip` with the shown tab in the panel colour, pill buttons
+    /// ([`crate::widgets::primary_button`]), Source Sans 3 for the interface, and, as in PhotoCraft,
+    /// caption buttons that follow the desktop ([`crate::titlebar`]), a boxed workspace switcher and
+    /// a centred title ([`crate::chrome::app_bar`]).
+    pub flat: bool,
 }
 
 impl Tokens {
@@ -214,6 +224,7 @@ impl Tokens {
             axis_x: hex(0xef6058),
             axis_y: hex(0x45af63),
             axis_z: hex(0x378ef0),
+            flat: false,
         };
         match b {
             Brightness::Dark => base,
@@ -418,14 +429,40 @@ impl Tokens {
         }
     }
 
-    /// The tokens for `b` in `scheme`; Modern takes `accent`, Classic has its own blue.
+    /// The Classic OG colour scheme at brightness `b`: VectorCraft 0.11's look, from before the
+    /// cards and soft corners came. Classic's greys and blue (which are 0.11's), with the title bar the
+    /// panels' grey at Medium Dark as it was then, 0.11's small corners and Source Sans 3 for the
+    /// interface; [`Self::flat`] brings back its flat panels, tab strips and pill buttons, and the
+    /// caption buttons follow the desktop as PhotoCraft's do.
+    pub fn classic_og(b: Brightness) -> Self {
+        let hex = |s: u32| Color32::from_rgb((s >> 16) as u8, (s >> 8) as u8, s as u8);
+        let classic = Self::classic(b);
+        Tokens {
+            flat: true,
+            radius: 2,
+            // Classic sets the frame a shade below the panels at Medium Dark so its cards stand out;
+            // without cards the title bar is the panels' grey again.
+            app_bar: if b == Brightness::MediumDark { hex(0x535353) } else { classic.app_bar },
+            ..classic
+        }
+    }
+
+    /// The tokens for `b` in `scheme`; Modern takes `accent`, Classic and Classic OG have their own
+    /// blue.
     pub fn for_scheme(scheme: Scheme, b: Brightness, accent: Accent) -> Self {
         match scheme {
             Scheme::Modern => Self::for_brightness(b).with_accent(accent),
             Scheme::Classic => Self::classic(b),
+            Scheme::ClassicOg => Self::classic_og(b),
         }
     }
 
+    /// The corners of a dialog button `height` points tall: a pill (Classic OG, as in 0.11 and
+    /// PhotoCraft) or the usual soft corners (`radius`).
+    pub fn button_radius(&self, height: f32, radius: u8) -> CornerRadius {
+        if self.flat { CornerRadius::same((height / 2.0).clamp(0.0, f32::from(u8::MAX)) as u8) } else { CornerRadius::same(radius) }
+    }
+
     /// These tokens with `accent`: the accent, buttons filled with it, the active item in flyouts and
     /// selected rows (the panel tinted towards it).
     pub fn with_accent(self, accent: Accent) -> Self {
@@ -455,8 +492,20 @@ impl Tokens {
 }
 
 /// The toolbar, the panel dock and the icon rail are cards: `body` drawn on the panel colour with
-/// soft corners, `margin` of the window's frame colour around it.
+/// soft corners, `margin` of the window's frame colour around it. With [`Tokens::flat`] (Classic
+/// OG) they are VectorCraft 0.11's flat panels instead: square, edged with the frame colour and no
+/// margin.
 pub fn card<R>(ui: &mut egui::Ui, t: &Tokens, margin: egui::Margin, body: impl FnOnce(&mut egui::Ui) -> R) -> R {
+    if t.flat {
+        return egui::Frame::NONE
+            .fill(t.panel)
+            .stroke(Stroke::new(1.5, t.border))
+            .show(ui, |ui| {
+                ui.set_min_size(ui.available_size());
+                body(ui)
+            })
+            .inner;
+    }
     egui::Frame::NONE
         .fill(t.app_bar)
         .inner_margin(margin)
@@ -495,6 +544,19 @@ pub fn mono(size: f32) -> FontId {
     FontId::new(size, FontFamily::Name(FONT_MONO.into()))
 }
 
+/// Where [`apply_with`] notes whether the interface is in Source Sans 3 (Classic OG, as VectorCraft
+/// 0.11 was) rather than Inter.
+fn classic_face_id() -> egui::Id {
+    egui::Id::new("vc-ui-face-classic")
+}
+
+/// Whether [`install_fonts`] puts Source Sans 3 first (Classic OG).
+pub(crate) fn classic_face(ctx: &egui::Context) -> bool {
+    ctx.data(|d| d.get_temp::<bool>(classic_face_id())).unwrap_or(false)
+}
+
+/// The app's fonts: Inter for the interface (Source Sans 3 with Classic OG, [`classic_face`]),
+/// JetBrains Mono for numbers, the craft-fonts faces as fallbacks.
 pub fn install_fonts(ctx: &egui::Context) {
     let mut fonts = FontDefinitions::default();
     let add = |f: &mut FontDefinitions, name: &str, bytes: &'static [u8]| {
@@ -510,13 +572,18 @@ pub fn install_fonts(ctx: &egui::Context) {
     add(&mut fonts, "SourceSans3-Semibold", include_bytes!("../../../assets/fonts/SourceSans3-Semibold.ttf"));
     add(&mut fonts, "JetBrainsMono", include_bytes!("../../../assets/fonts/JetBrainsMono-Regular.ttf"));
     let fallback: Vec<String> = fonts.families.get(&FontFamily::Proportional).cloned().unwrap_or_default();
-    let mut prop = vec!["Inter".to_string(), "SourceSans3".to_string()];
+    let (regular, semibold) = if classic_face(ctx) {
+        (["SourceSans3", "Inter"], ["SourceSans3-Semibold", "Inter-SemiBold"])
+    } else {
+        (["Inter", "SourceSans3"], ["Inter-SemiBold", "SourceSans3-Semibold"])
+    };
+    let mut prop: Vec<String> = regular.map(str::to_string).to_vec();
     prop.extend(fallback.clone());
     fonts.families.insert(FontFamily::Proportional, prop);
-    let mut semi = vec!["Inter-SemiBold".to_string(), "SourceSans3-Semibold".to_string()];
+    let mut semi: Vec<String> = semibold.map(str::to_string).to_vec();
     semi.extend(fallback.clone());
     fonts.families.insert(FontFamily::Name(FONT_UI_SEMIBOLD.into()), semi);
-    let mut ui = vec!["Inter".to_string(), "SourceSans3".to_string()];
+    let mut ui: Vec<String> = regular.map(str::to_string).to_vec();
     ui.extend(fallback.clone());
     fonts.families.insert(FontFamily::Name(FONT_UI.into()), ui);
     let mut mono = vec!["JetBrainsMono".to_string()];
@@ -555,6 +622,11 @@ pub fn apply(ctx: &egui::Context, b: Brightness) {
 pub fn apply_with(ctx: &egui::Context, b: Brightness, accent: Accent, scheme: Scheme) {
     let t = Tokens::for_scheme(scheme, b, accent);
     ctx.data_mut(|d| d.insert_temp(egui::Id::NULL, t));
+    // Classic OG sets the interface in Source Sans 3, as VectorCraft 0.11 did; the others in Inter.
+    if classic_face(ctx) != t.flat {
+        ctx.data_mut(|d| d.insert_temp(classic_face_id(), t.flat));
+        install_fonts(ctx);
+    }
     let mut v = if t.dark { Visuals::dark() } else { Visuals::light() };
     v.panel_fill = t.panel;
     v.window_fill = t.panel;
@@ -579,6 +651,11 @@ pub fn apply_with(ctx: &egui::Context, b: Brightness, accent: Accent, scheme: Sc
     w.inactive.bg_stroke = Stroke::NONE;
     w.hovered.bg_stroke = Stroke::new(1.0, t.input_border);
     w.active.bg_stroke = Stroke::new(1.0, t.accent);
+    if t.flat {
+        // VectorCraft 0.11's windows and menus.
+        v.window_corner_radius = CornerRadius::same(6);
+        v.menu_corner_radius = CornerRadius::same(6);
+    }
     ctx.set_visuals(v);
     ctx.global_style_mut(|s| {
         s.spacing.item_spacing = egui::vec2(6.0, 5.0);
@@ -635,6 +712,84 @@ mod tests {
             assert_ne!(classic.app_bar, classic.panel, "{b:?}: the cards need a frame");
             assert_eq!(classic.accent, Color32::from_rgb(0x37, 0x8e, 0xf0));
             assert_eq!(Scheme::parse(Scheme::Classic.id()), Some(Scheme::Classic));
+            assert!(!classic.flat && !modern.flat, "{b:?}: only Classic OG is flat");
         }
     }
+
+    /// Classic OG is VectorCraft 0.11's look: Classic's greys and blue whatever accent is chosen,
+    /// the title bar the panels' grey at Medium Dark, 0.11's small corners, on flat chrome with pill
+    /// buttons.
+    #[test]
+    fn classic_og_is_0_11s_look() {
+        assert_eq!(Scheme::parse("classicOg"), Some(Scheme::ClassicOg));
+        assert_eq!(Scheme::parse("CLASSICOG"), Some(Scheme::ClassicOg));
+        let hex = |s: u32| Color32::from_rgb((s >> 16) as u8, (s >> 8) as u8, s as u8);
+        // 0.11's Medium Dark, the default brightness: one grey for the title bar and the panels.
+        let medium = Tokens::for_scheme(Scheme::ClassicOg, Brightness::MediumDark, Accent::Green);
+        assert_eq!((medium.app_bar, medium.panel, medium.tab_strip, medium.border), (hex(0x535353), hex(0x535353), hex(0x424242), hex(0x383838)));
+        // 0.11's Dark: a darker title bar over #323232 panels.
+        let dark = Tokens::for_scheme(Scheme::ClassicOg, Brightness::Dark, Accent::Pink);
+        assert_eq!((dark.app_bar, dark.panel), (hex(0x262626), hex(0x323232)));
+        for b in Brightness::ALL {
+            let (og, classic) = (Tokens::for_scheme(Scheme::ClassicOg, b, Accent::Violet), Tokens::classic(b));
+            assert!(og.flat, "{b:?}");
+            assert_eq!((og.radius, og.dark), (2, classic.dark), "{b:?}");
+            assert_eq!((og.panel, og.text, og.accent, og.tab_strip), (classic.panel, classic.text, classic.accent, classic.tab_strip), "{b:?}");
+            assert_ne!(og.tab_strip, og.panel, "{b:?}: the shown tab stands out of its strip");
+            assert_eq!(og.button_radius(28.0, 6), CornerRadius::same(14), "a pill");
+            assert_eq!(Tokens::for_brightness(b).button_radius(28.0, 6), CornerRadius::same(6));
+        }
+    }
+
+    /// Classic OG's cards are 0.11's flat panels: square, edged with the frame colour, no margin.
+    #[test]
+    fn flat_cards_are_square_panels() {
+        let flat = Tokens::classic_og(Brightness::MediumDark);
+        let ctx = egui::Context::default();
+        let mut rects = vec![];
+        let mut out = ctx.run_ui(egui::RawInput::default(), |ui| {
+            card(ui, &flat, egui::Margin::same(6), |ui| {
+                ui.label("x");
+            });
+        });
+        out.textures_delta.clear();
+        fn walk(s: &egui::Shape, v: &mut Vec<egui::epaint::RectShape>) {
+            match s {
+                egui::Shape::Rect(r) => v.push(r.clone()),
+                egui::Shape::Vec(s) => s.iter().for_each(|s| walk(s, v)),
+                _ => {}
+            }
+        }
+        out.shapes.iter().for_each(|c| walk(&c.shape, &mut rects));
+        let panel = rects.iter().find(|r| r.fill == flat.panel).expect("the panel's fill");
+        assert_eq!((panel.corner_radius, panel.stroke.color), (CornerRadius::ZERO, flat.border));
+        assert_eq!(panel.rect.min, egui::Pos2::ZERO, "no margin");
+        assert!(!rects.iter().any(|r| r.fill == flat.app_bar && r.rect != panel.rect), "no frame around it: {rects:?}");
+    }
+
+    /// Classic OG sets the interface in Source Sans 3, as 0.11 did, and the other schemes in Inter.
+    #[test]
+    fn classic_og_switches_the_interface_font() {
+        let ctx = egui::Context::default();
+        install_fonts(&ctx);
+        apply(&ctx, Brightness::MediumDark);
+        let first = |ctx: &egui::Context| {
+            let mut first = String::new();
+            let mut out = ctx.run_ui(egui::RawInput::default(), |ui| {
+                first =
+                    ui.ctx().fonts(|f| f.definitions().families.get(&FontFamily::Proportional).and_then(|v| v.first().cloned()).unwrap_or_default());
+            });
+            out.textures_delta.clear();
+            first
+        };
+        // set_fonts lands with the next pass.
+        let _ = first(&ctx);
+        assert_eq!(first(&ctx), "Inter");
+        apply_with(&ctx, Brightness::MediumDark, Accent::default(), Scheme::ClassicOg);
+        let _ = first(&ctx);
+        assert_eq!(first(&ctx), "SourceSans3");
+        apply_with(&ctx, Brightness::MediumDark, Accent::default(), Scheme::Classic);
+        let _ = first(&ctx);
+        assert_eq!(first(&ctx), "Inter");
+    }
 }
diff --git a/crates/ui-egui/src/titlebar.rs b/crates/ui-egui/src/titlebar.rs
--- a/crates/ui-egui/src/titlebar.rs
+++ b/crates/ui-egui/src/titlebar.rs
@@ -3,8 +3,14 @@
 //! double-click there maximizes it, caption buttons at the far right minimize, maximize/restore and
 //! close, and thin invisible zones along the window's edges resize it. On macOS the app bar sits
 //! under the system's transparent title bar and only its [`drag_area`] is used.
+//!
+//! The caption buttons are Windows' 46 pt buttons. With the Classic OG colour scheme they follow the
+//! desktop instead, as PhotoCraft's do ([`CaptionStyle`], from [`crate::Services::caption_style`]):
+//! on Linux libadwaita's round buttons (GNOME), Breeze's (KDE, SteamOS) or a plain round set
+//! elsewhere, with the set of buttons and their side taken from the desktop's button layout. The
+//! shapes are drawn here.
 
-use egui::{CursorIcon, Id, LayerId, Order, PointerButton, Rect, ResizeDirection, Sense, Stroke, Ui, ViewportCommand, pos2, vec2};
+use egui::{Color32, CursorIcon, Id, LayerId, Order, PointerButton, Pos2, Rect, ResizeDirection, Sense, Stroke, Ui, ViewportCommand, pos2, vec2};
 use serde_json::json;
 
 use crate::VectorcraftApp;
@@ -12,22 +18,160 @@ use crate::theme::Tokens;
 
 /// One caption button: as wide as the native Windows ones.
 pub const BUTTON_WIDTH: f32 = 46.0;
-/// The three caption buttons together.
+/// The three Windows caption buttons together.
 pub const WIDTH: f32 = 3.0 * BUTTON_WIDTH;
 /// Resize zone thickness along an edge, and the side of a corner zone.
 const EDGE: f32 = 5.0;
 const CORNER: f32 = 12.0;
+/// Breeze's Close button on hover and press (KDE's Breeze decoration colours).
+const BREEZE_CLOSE_HOVER: Color32 = Color32::from_rgb(0xff, 0x98, 0xa2);
+const BREEZE_CLOSE_PRESSED: Color32 = Color32::from_rgb(0x6d, 0x22, 0x29);
 
-#[derive(Clone, Copy, PartialEq, Debug)]
-enum Caption {
+#[derive(Clone, Copy, PartialEq, Eq, Debug)]
+pub enum Caption {
     Minimize,
     Maximize,
     Close,
 }
 
-/// Left to right.
+/// Windows' order, left to right.
 const CAPTIONS: [Caption; 3] = [Caption::Minimize, Caption::Maximize, Caption::Close];
 
+/// Which desktop's caption buttons to look like (Classic OG).
+#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
+pub enum CaptionFamily {
+    /// Windows 10/11: full-height buttons 46 pt wide; Close turns red on hover.
+    #[default]
+    Windows,
+    /// GNOME (libadwaita): 24 pt round buttons on a faint disc that darkens on hover and press.
+    Adwaita,
+    /// KDE (Breeze decoration): smaller round buttons, bare at rest, inverted on hover.
+    Breeze,
+    /// Other Linux desktops: libadwaita's buttons without the disc at rest.
+    Generic,
+}
+
+impl CaptionFamily {
+    /// `windows`, `adwaita`, `breeze` or `generic`, in any case.
+    pub fn parse(name: &str) -> Option<Self> {
+        match name.trim().to_ascii_lowercase().as_str() {
+            "windows" => Some(Self::Windows),
+            "adwaita" => Some(Self::Adwaita),
+            "breeze" => Some(Self::Breeze),
+            "generic" => Some(Self::Generic),
+            _ => None,
+        }
+    }
+
+    /// The button side, gap on each side of it, and gap between the outermost button and the
+    /// window's edge. libadwaita: 24 pt buttons 13 pt apart, 10 pt from the edge (the gap is split
+    /// between neighbours, so the hit areas touch). Breeze sizes buttons by the font (about 1.3 em,
+    /// 18 pt at KDE's default 10 pt font), half a button apart, 8 pt from the edge.
+    fn metrics(self) -> (f32, f32, f32) {
+        match self {
+            Self::Windows => (BUTTON_WIDTH, 0.0, 0.0),
+            Self::Adwaita | Self::Generic => (24.0, 6.5, 10.0),
+            Self::Breeze => (18.0, 4.5, 8.0),
+        }
+    }
+}
+
+/// The caption buttons' look and placement.
+#[derive(Clone, PartialEq, Debug)]
+pub struct CaptionStyle {
+    pub family: CaptionFamily,
+    /// The buttons at the bar's left end, left to right.
+    pub left: Vec<Caption>,
+    /// The buttons at the bar's right end, left to right.
+    pub right: Vec<Caption>,
+}
+
+impl Default for CaptionStyle {
+    fn default() -> Self {
+        Self { family: CaptionFamily::Windows, left: vec![], right: CAPTIONS.to_vec() }
+    }
+}
+
+impl CaptionStyle {
+    /// `family` with the buttons a GTK decoration layout names (`gtk-decoration-layout`, GNOME's
+    /// `button-layout`, e.g. `appmenu:minimize,maximize,close`): those before the colon go left,
+    /// those after it right; without a colon all go left, as in GTK. Names other than `minimize`,
+    /// `maximize` and `close` (`appmenu`, `icon`, `spacer`, …) are skipped, and a button named
+    /// twice is placed once.
+    pub fn from_gtk_layout(family: CaptionFamily, layout: &str) -> Self {
+        let (left, right) = layout.split_once(':').unwrap_or((layout, ""));
+        let mut seen = vec![];
+        let mut side = |names: &str| {
+            let mut v = vec![];
+            for name in names.split(',') {
+                let c = match name.trim() {
+                    "minimize" => Caption::Minimize,
+                    "maximize" => Caption::Maximize,
+                    "close" => Caption::Close,
+                    _ => continue,
+                };
+                if !seen.contains(&c) {
+                    seen.push(c);
+                    v.push(c);
+                }
+            }
+            v
+        };
+        let left = side(left);
+        let right = side(right);
+        Self { family, left, right }
+    }
+}
+
+/// The caption style in use: the desktop's (as the host reports it) with Classic OG, Windows'
+/// otherwise and when the host reports none.
+fn style(app: &VectorcraftApp, ctx: &egui::Context) -> CaptionStyle {
+    if Tokens::get(ctx).flat { app.services.caption_style.clone().unwrap_or_default() } else { CaptionStyle::default() }
+}
+
+/// The width the caption buttons take at the bar's (left, right) end: zero for a side without
+/// buttons, and for both without the custom title bar.
+pub fn reserved(app: &VectorcraftApp, ctx: &egui::Context) -> (f32, f32) {
+    if !app.custom_titlebar {
+        return (0.0, 0.0);
+    }
+    let s = style(app, ctx);
+    let width = |side: &[Caption]| slots(s.family, side, 0.0, true).last().map_or(0.0, |(_, r, _)| r.1);
+    (width(&s.left), width(&s.right))
+}
+
+/// Each button's horizontal extent (left, right) and face centre on a side starting at `from`, in
+/// order. `outer_left` puts the edge gap on the first button's left (the left end) instead of the
+/// last button's right.
+fn slots(family: CaptionFamily, side: &[Caption], from: f32, outer_left: bool) -> Vec<(Caption, (f32, f32), f32)> {
+    let (size, gap, end) = family.metrics();
+    let mut x = from;
+    let n = side.len();
+    side.iter()
+        .enumerate()
+        .map(|(i, &c)| {
+            let before = if outer_left && i == 0 { end } else { gap };
+            let after = if !outer_left && i + 1 == n { end } else { gap };
+            let slot = (x, x + before + size + after);
+            let centre = x + before + size / 2.0;
+            x = slot.1;
+            (c, slot, centre)
+        })
+        .collect()
+}
+
+/// Every caption button's hit rect (full bar height down to its bottom border, flush with the
+/// window's edge) and face centre, for a bar `bar` between the window's `left` and `right` edges.
+fn layout(s: &CaptionStyle, bar: Rect, left: f32, right: f32) -> Vec<(Caption, Rect, Pos2)> {
+    let rect = |(c, (l, r), centre): (Caption, (f32, f32), f32)| {
+        (c, Rect::from_min_max(pos2(l, bar.top()), pos2(r, bar.bottom() - 1.0)), pos2(centre, bar.center().y))
+    };
+    let lefts = slots(s.family, &s.left, left, true);
+    let width = slots(s.family, &s.right, 0.0, false).last().map_or(0.0, |(_, r, _)| r.1);
+    let rights = slots(s.family, &s.right, right - width, false);
+    lefts.into_iter().chain(rights).map(rect).collect()
+}
+
 /// Caption button ids (stable, so tests and agents can find them).
 fn caption_id(c: Caption) -> Id {
     Id::new(("titlebar-caption", c as u8))
@@ -69,33 +213,47 @@ fn handed_to_os(ctx: &egui::Context) {
     ctx.input_mut(|i| i.pointer = Default::default());
 }
 
-/// Minimize, Maximize/Restore and Close at the right end of `bar` (the app bar's outer rect), full
-/// bar height down to its bottom border, flush with the window's edge so a maximized window's
-/// corner hits Close. Close runs `app.quit`, which asks about unsaved documents first.
+/// Minimize, Maximize/Restore and Close at the ends of `bar` (the app bar's outer rect), full bar
+/// height down to its bottom border, flush with the window's edges so a maximized window's corner
+/// hits the outermost one: Windows' buttons at the right end, or with Classic OG the desktop's
+/// ([`style`]). Close runs `app.quit`, which asks about unsaved documents first. The desktop's round
+/// buttons drag the window too, as GTK's and KDE's do.
 pub fn caption_buttons(app: &mut VectorcraftApp, ui: &mut Ui, bar: Rect) {
     let t = Tokens::get(ui.ctx());
+    let s = style(app, ui.ctx());
     let max = maximized(ui.ctx());
+    let content = ui.ctx().content_rect();
+    let windows = s.family == CaptionFamily::Windows;
     let mut clicked = None;
-    for (i, c) in CAPTIONS.into_iter().enumerate() {
-        let r = Rect::from_min_size(pos2(bar.right() - WIDTH + i as f32 * BUTTON_WIDTH, bar.top()), vec2(BUTTON_WIDTH, bar.height() - 1.0));
-        let resp = ui.interact(r, caption_id(c), Sense::click());
-        let close = c == Caption::Close;
-        let fill = match (resp.is_pointer_button_down_on(), resp.hovered()) {
-            (true, _) if close => t.caption_close.gamma_multiply(0.8),
-            (_, true) if close => t.caption_close,
-            (true, _) => t.tool_active,
-            (_, true) => t.hover,
-            _ => egui::Color32::TRANSPARENT,
-        };
-        ui.painter().rect_filled(r, 0.0, fill);
-        let ink = if close && resp.hovered() { t.caption_close_text } else { t.icon };
-        paint_glyph(ui, c, max, r.center(), Stroke::new(1.0, ink));
+    for (c, r, centre) in layout(&s, bar, bar.left().max(content.left()), bar.right().min(content.right())) {
+        let resp = ui.interact(r, caption_id(c), if windows { Sense::click() } else { Sense::click_and_drag() });
+        if resp.drag_started_by(PointerButton::Primary) {
+            ui.ctx().send_viewport_cmd(ViewportCommand::StartDrag);
+            handed_to_os(ui.ctx());
+        }
+        if windows {
+            let close = c == Caption::Close;
+            let fill = match (resp.is_pointer_button_down_on(), resp.hovered()) {
+                (true, _) if close => t.caption_close.gamma_multiply(0.8),
+                (_, true) if close => t.caption_close,
+                (true, _) => t.tool_active,
+                (_, true) => t.hover,
+                _ => Color32::TRANSPARENT,
+            };
+            ui.painter().rect_filled(r, 0.0, fill);
+            let ink = if close && resp.hovered() { t.caption_close_text } else { t.icon };
+            paint_glyph(ui, c, max, r.center(), Stroke::new(1.0, ink));
+        } else {
+            let pressed = resp.is_pointer_button_down_on() && resp.hovered();
+            paint_round(ui, &t, s.family, c, max, centre, resp.hovered(), pressed);
+        }
         let tip = match c {
             Caption::Minimize => tl!("Minimize"),
             Caption::Maximize if max => tl!("Restore"),
             Caption::Maximize => tl!("Maximize"),
             Caption::Close => tl!("Close"),
         };
+        resp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, true, tip));
         if resp.on_hover_text(tip).clicked() {
             clicked = Some(c);
         }
@@ -110,7 +268,7 @@ pub fn caption_buttons(app: &mut VectorcraftApp, ui: &mut Ui, bar: Rect) {
     }
 }
 
-/// The 10 pt line glyphs of the caption buttons.
+/// The 10 pt line glyphs of the Windows caption buttons.
 fn paint_glyph(ui: &Ui, c: Caption, maximized: bool, at: egui::Pos2, s: Stroke) {
     let p = ui.painter();
     let g = Rect::from_center_size(at, vec2(10.0, 10.0));
@@ -134,6 +292,84 @@ fn paint_glyph(ui: &Ui, c: Caption, maximized: bool, at: egui::Pos2, s: Stroke)
     }
 }
 
+/// A round Linux caption button centred at `centre`: its disc, then its glyph on a 16 pt grid. The
+/// disc is the icon colour at a fraction of its opacity (libadwaita's `alpha(currentColor)`), so it
+/// suits light and dark themes alike; Breeze instead fills it with the icon colour on hover and
+/// draws the glyph in the bar's colour. The same shapes as PhotoCraft's.
+#[allow(clippy::too_many_arguments)]
+fn paint_round(ui: &Ui, t: &Tokens, family: CaptionFamily, c: Caption, maximized: bool, centre: Pos2, hovered: bool, pressed: bool) {
+    let (size, _, _) = family.metrics();
+    // Whole pixels, so the glyphs' edges stay sharp.
+    let ppp = ui.ctx().pixels_per_point();
+    let centre = pos2((centre.x * ppp).round() / ppp, (centre.y * ppp).round() / ppp);
+    let close = c == Caption::Close;
+    let (fill, ink) = if family == CaptionFamily::Breeze {
+        match (hovered, pressed) {
+            (_, true) if close => (BREEZE_CLOSE_PRESSED, t.app_bar),
+            (true, _) if close => (BREEZE_CLOSE_HOVER, t.app_bar),
+            (_, true) => (t.icon.gamma_multiply(0.5), t.app_bar),
+            (true, _) => (t.icon, t.app_bar),
+            _ => (Color32::TRANSPARENT, t.icon),
+        }
+    } else {
+        let rest = if family == CaptionFamily::Adwaita { 0.10 } else { 0.0 };
+        let target = if pressed {
+            0.30
+        } else if hovered {
+            0.15
+        } else {
+            rest
+        };
+        // libadwaita eases the disc over 0.2 s.
+        let a = ui.ctx().animate_value_with_time(caption_id(c).with("disc"), target, 0.2);
+        (t.icon.gamma_multiply(a), t.icon)
+    };
+    let p = ui.painter();
+    if fill.a() > 0 {
+        p.circle_filled(centre, size / 2.0, fill);
+    }
+    // The glyph grid: 16 pt, or 2 pt less than a smaller button.
+    let k = (size - 2.0).min(16.0) / 16.0;
+    let at = |x: f32, y: f32| centre + vec2(x - 8.0, y - 8.0) * k;
+    if family == CaptionFamily::Breeze {
+        // Breeze: 1 pt chevrons (down to minimize, up to maximize), a diamond to restore, an X.
+        let s = Stroke::new(1.0, ink);
+        match c {
+            Caption::Minimize => {
+                p.line(vec![at(2.5, 5.5), at(8.0, 11.0), at(13.5, 5.5)], s);
+            }
+            Caption::Maximize if maximized => {
+                p.add(egui::Shape::closed_line(vec![at(8.0, 3.0), at(13.0, 8.0), at(8.0, 13.0), at(3.0, 8.0)], s));
+            }
+            Caption::Maximize => {
+                p.line(vec![at(2.5, 10.5), at(8.0, 5.0), at(13.5, 10.5)], s);
+            }
+            Caption::Close => {
+                let s = Stroke::new(1.2, ink);
+                p.line_segment([at(3.6, 3.6), at(12.4, 12.4)], s);
+                p.line_segment([at(12.4, 3.6), at(3.6, 12.4)], s);
+            }
+        }
+    } else {
+        // libadwaita: a 2 pt bar low down, a 2 pt square outline (smaller to restore), an X.
+        let frame = |a: f32, b: f32| Rect::from_min_max(at(a, a), at(b, b));
+        match c {
+            Caption::Minimize => {
+                p.rect_filled(Rect::from_min_max(at(4.0, 10.0), at(12.0, 12.0)), 0.0, ink);
+            }
+            Caption::Maximize => {
+                let square = if maximized { frame(5.0, 11.0) } else { frame(4.0, 12.0) };
+                p.rect_stroke(square, 0.0, Stroke::new(2.0 * k, ink), egui::StrokeKind::Inside);
+            }
+            Caption::Close => {
+                let s = Stroke::new(1.5 * k, ink);
+                p.line_segment([at(4.5, 4.5), at(11.5, 11.5)], s);
+                p.line_segment([at(11.5, 4.5), at(4.5, 11.5)], s);
+            }
+        }
+    }
+}
+
 /// The edge and corner zones of a window with content rect `w`: (zone, direction, cursor). Corners
 /// come last so they win where they overlap the edges.
 fn zones(w: Rect) -> [(Rect, ResizeDirection, CursorIcon); 8] {
@@ -419,4 +655,127 @@ mod tests {
         cmds.extend(w.click(moved));
         assert!(cmds.is_empty(), "{cmds:?}");
     }
+
+    /// A [`Win`] in the Classic OG colour scheme on a desktop whose caption buttons are `style`.
+    fn classic_og(width: f32, style: CaptionStyle) -> Win {
+        let mut w = Win::new(width);
+        crate::theme::apply_with(&w.ctx, Default::default(), Default::default(), crate::theme::Scheme::ClassicOg);
+        w.app.services.caption_style = Some(style);
+        w
+    }
+
+    /// Classic OG draws the desktop's caption buttons, as PhotoCraft does: round buttons where the
+    /// desktop's layout puts them, flush with the window's edges, the bar's widgets clear of them;
+    /// they click, and a drag that starts on one moves the window.
+    #[test]
+    fn classic_og_draws_the_desktops_buttons_where_its_layout_puts_them() {
+        use Caption::*;
+        for (family, size, gap, end) in [(CaptionFamily::Adwaita, 24.0, 6.5, 10.0), (CaptionFamily::Breeze, 18.0, 4.5, 8.0)] {
+            let width = 1100.0;
+            let mut w = classic_og(width, CaptionStyle { family, left: vec![Close], right: vec![Minimize, Maximize] });
+            w.frame(vec![]);
+            w.frame(vec![]);
+            let [min, max, close] = CAPTIONS.map(|c| w.caption(c));
+            assert_eq!((close.left(), close.width()), (0.0, end + size + gap), "{family:?}");
+            assert_eq!((max.right(), max.width()), (width, gap + size + end), "{family:?}");
+            assert_eq!((min.right(), min.width()), (max.left(), 2.0 * gap + size), "{family:?}");
+            assert_eq!(reserved(&w.app, &w.ctx), (end + size + gap, 2.0 * gap + size + gap + size + end), "{family:?}");
+            for a in w.bar_widgets() {
+                assert!(a.left() >= close.right() && a.right() <= min.left(), "{a:?} runs into the caption buttons ({family:?})");
+            }
+            assert!(w.click(max.center()).contains(&ViewportCommand::Maximized(true)));
+            assert!(w.click(min.center()).contains(&ViewportCommand::Minimized(true)));
+            // A drag that starts on a button moves the window instead of pressing it.
+            let at = min.center();
+            let cmds = w.gesture(&[at, at + vec2(12.0, 4.0), at + vec2(30.0, 8.0)]);
+            assert_eq!(cmds.iter().filter(|c| matches!(c, ViewportCommand::StartDrag)).count(), 1, "{cmds:?}");
+            assert!(!cmds.contains(&ViewportCommand::Minimized(true)), "{cmds:?}");
+            // Close quits, asking first about unsaved work.
+            w.click(close.center());
+            assert_eq!(w.app.ui.status, "quit", "{family:?}");
+        }
+    }
+
+    /// KDE's default layout (SteamOS's desktop mode): Breeze's three buttons at the right end, and
+    /// the bar's controls stop before them.
+    #[test]
+    fn classic_og_on_kde_puts_breezes_buttons_at_the_right() {
+        let style = CaptionStyle::from_gtk_layout(CaptionFamily::Breeze, ":minimize,maximize,close");
+        let mut w = classic_og(1440.0, style);
+        w.frame(vec![]);
+        w.frame(vec![]);
+        let [min, max, close] = CAPTIONS.map(|c| w.caption(c));
+        assert_eq!(close.right(), 1440.0);
+        assert!(min.right() <= max.left() && max.right() <= close.left());
+        assert!([min, max, close].iter().all(|r| r.width() < BUTTON_WIDTH), "round buttons, not Windows' 46 pt ones");
+        for a in w.bar_widgets() {
+            assert!(a.right() <= min.left() - 8.0, "{a:?} runs into the caption buttons");
+        }
+    }
+
+    /// The other colour schemes keep Windows' buttons whatever the desktop.
+    #[test]
+    fn other_schemes_keep_windows_buttons() {
+        let mut w = Win::new(1100.0);
+        w.app.services.caption_style = Some(CaptionStyle::from_gtk_layout(CaptionFamily::Breeze, "close:minimize"));
+        w.frame(vec![]);
+        w.frame(vec![]);
+        let [min, max, close] = CAPTIONS.map(|c| w.caption(c));
+        assert!([min, max, close].iter().all(|r| r.width() == BUTTON_WIDTH));
+        assert_eq!(close.right(), 1100.0);
+        assert_eq!(reserved(&w.app, &w.ctx), (0.0, WIDTH));
+        // Without the custom title bar no side is reserved.
+        w.app.custom_titlebar = false;
+        assert_eq!(reserved(&w.app, &w.ctx), (0.0, 0.0));
+    }
+
+    /// Classic OG's bar shows the title in the room between the menus and the controls, as
+    /// PhotoCraft's does: centred on the window, slid along near the menus, shortened, or left out,
+    /// and never over them.
+    #[test]
+    fn classic_og_centres_the_title() {
+        let mut slid = false;
+        for width in (700..=1440).step_by(20) {
+            let width = width as f32;
+            let mut w = classic_og(width, CaptionStyle::from_gtk_layout(CaptionFamily::Breeze, ":minimize,maximize,close"));
+            w.frame(vec![]);
+            let out = w.frame(vec![]);
+            // The title, whole or shortened ("Vect…"); never over the menus or the controls.
+            let titles: Vec<(String, Rect)> = texts(&out)
+                .into_iter()
+                .filter(|(t, _)| {
+                    let start = t.trim_end_matches('…');
+                    !start.is_empty() && "VectorCraft".starts_with(start)
+                })
+                .collect();
+            for (t, r) in &titles {
+                for a in w.bar_widgets() {
+                    assert!(!a.intersects(r.shrink(0.5)), "{width}: {t} over {a:?}");
+                }
+            }
+            let whole = titles.iter().find(|(t, _)| t == "VectorCraft").map(|(_, r)| *r);
+            if width == 1440.0 {
+                let title = whole.expect("the whole title");
+                assert!((title.center().x - 720.0).abs() < 1.0, "centred on the window: {title:?}");
+            }
+            // Where centring would bring it near the menus, PhotoCraft's title slides along whole.
+            slid |= whole.is_some_and(|r| (r.center().x - width / 2.0).abs() > 1.0);
+        }
+        assert!(slid, "some width slides the title along instead of shortening it");
+    }
+
+    #[test]
+    fn gtk_layouts_pick_the_buttons_and_their_sides() {
+        use Caption::*;
+        let s = CaptionStyle::from_gtk_layout(CaptionFamily::Adwaita, "appmenu:minimize,maximize,close");
+        assert_eq!((s.left, s.right), (vec![], vec![Minimize, Maximize, Close]));
+        // Unknown names are skipped, spaces trimmed, a repeat placed once, a second colon is a name.
+        let s = CaptionStyle::from_gtk_layout(CaptionFamily::Adwaita, "close,close , minimize:icon,maximize:close");
+        assert_eq!((s.left, s.right), (vec![Close, Minimize], vec![]));
+        let s = CaptionStyle::from_gtk_layout(CaptionFamily::Breeze, "minimize,close");
+        assert_eq!((s.left, s.right), (vec![Minimize, Close], vec![]), "no colon: GTK puts them all at the start");
+        assert_eq!(CaptionFamily::parse(" Breeze "), Some(CaptionFamily::Breeze));
+        assert_eq!(CaptionFamily::parse("aqua"), None);
+        assert_eq!(CaptionStyle::default(), CaptionStyle { family: CaptionFamily::Windows, left: vec![], right: CAPTIONS.to_vec() });
+    }
 }
diff --git a/crates/ui-egui/src/toolbar.rs b/crates/ui-egui/src/toolbar.rs
--- a/crates/ui-egui/src/toolbar.rs
+++ b/crates/ui-egui/src/toolbar.rs
@@ -105,6 +105,14 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
     let edge = ui.available_rect_before_wrap();
     ui.ctx().data_mut(|d| d.insert_temp(crate::floating::tools_zone_id(), egui::Rect::from_min_size(edge.min, vec2(WIDTH, edge.height()))));
     match app.ui.toolbar_pos {
+        None if t.flat => {
+            // Classic OG: VectorCraft 0.11's flat toolbar, edged like the panels.
+            let frame = egui::Frame::NONE
+                .fill(t.panel)
+                .inner_margin(egui::Margin { left: 0, right: 0, top: 0, bottom: 4 })
+                .stroke(Stroke::new(1.5, t.border));
+            egui::Panel::left("toolbar").resizable(false).exact_size(w).frame(frame).show(ui, |ui| body(app, ui, cols, None));
+        }
         None => {
             let margin = egui::Margin { left: 6, right: 0, top: 6, bottom: 6 };
             egui::Panel::left("toolbar").resizable(false).exact_size(w + 6.0).frame(egui::Frame::NONE).show(ui, |ui| {
@@ -224,11 +232,13 @@ fn body(app: &mut VectorcraftApp, ui: &mut Ui, cols: usize, floating: Option<egu
                     let Some(shown) = tool_info(&shown_id).or_else(|| tool_info(slot[0])) else { continue };
                     let is_active = slot.contains(&active);
                     let (rect, resp) = ui.allocate_exact_size(vec2(36.0, PITCH - 1.0), Sense::click_and_drag());
-                    let well = egui::Rect::from_center_size(rect.center(), vec2(30.0, 27.5));
+                    // Classic OG: VectorCraft 0.11's wells, the slot's width with square corners.
+                    let (well_w, corners) = if t.flat { (35.5, 1) } else { (30.0, 6) };
+                    let well = egui::Rect::from_center_size(rect.center(), vec2(well_w, 27.5));
                     if is_active {
-                        ui.painter().rect_filled(well, CornerRadius::same(6), t.tool_active);
+                        ui.painter().rect_filled(well, CornerRadius::same(corners), t.tool_active);
                     } else if resp.hovered() {
-                        ui.painter().rect_filled(well, CornerRadius::same(6), t.hover);
+                        ui.painter().rect_filled(well, CornerRadius::same(corners), t.hover);
                     }
                     let ir = egui::Rect::from_center_size(rect.center(), vec2(18.0, 18.0));
                     icons::paint(ui, icons::tool_icon(shown.icon), ir, if is_active { t.text_strong } else { t.icon });
@@ -697,10 +707,11 @@ fn tool_strip(ui: &mut Ui, t: &Tokens, active: &str, tools: &[String]) -> Option
     for tool in tools.iter().filter_map(|id| tool_info(id)) {
         let (r, resp) = ui.allocate_exact_size(STRIP_BUTTON, Sense::click());
         let well = r.shrink(1.0);
+        let corners = CornerRadius::same(if t.flat { 1 } else { 6 });
         if active == tool.id {
-            ui.painter().rect_filled(well, CornerRadius::same(6), t.tool_active);
+            ui.painter().rect_filled(well, corners, t.tool_active);
         } else if resp.hovered() {
-            ui.painter().rect_filled(well, CornerRadius::same(6), t.hover);
+            ui.painter().rect_filled(well, corners, t.hover);
         }
         let ir = egui::Rect::from_center_size(r.center(), vec2(18.0, 18.0));
         icons::paint(ui, icons::tool_icon(tool.icon), ir, if active == tool.id { t.text_strong } else { t.icon });
diff --git a/crates/ui-egui/src/ui_fonts.rs b/crates/ui-egui/src/ui_fonts.rs
--- a/crates/ui-egui/src/ui_fonts.rs
+++ b/crates/ui-egui/src/ui_fonts.rs
@@ -32,6 +32,12 @@ impl UiFonts {
     /// After a frame is laid out: add the fallback fonts found since the last frame, and look for
     /// fonts covering the characters this frame painted that the UI fonts lack.
     pub(crate) fn frame(&mut self, ctx: &egui::Context) {
+        // A change to or from the Classic OG colour scheme installs the UI fonts again
+        // (`theme::apply_with`), without the fallbacks added here: look for them again.
+        if !self.added.is_empty() && ctx.fonts(|f| self.added.iter().any(|name| !f.definitions().font_data.contains_key(name))) {
+            self.added.clear();
+            self.seen.clear();
+        }
         #[cfg(not(target_arch = "wasm32"))]
         if let Some(faces) = self.pending.as_ref().and_then(|rx| rx.try_recv().ok()) {
             self.pending = None;
diff --git a/crates/ui-egui/src/widgets.rs b/crates/ui-egui/src/widgets.rs
--- a/crates/ui-egui/src/widgets.rs
+++ b/crates/ui-egui/src/widgets.rs
@@ -25,25 +25,42 @@ pub fn paint_icon_button(ui: &mut Ui, resp: Response, icon: &str, tip: &str, sel
     } else {
         Color32::TRANSPARENT
     };
-    ui.painter().rect_filled(rect, CornerRadius::same(5), bg);
+    // Classic OG: VectorCraft 0.11's smaller corners and lettering.
+    ui.painter().rect_filled(rect, CornerRadius::same(if t.flat { 3 } else { 5 }), bg);
     let pad = (size * 0.2).round();
-    icons::paint(ui, icon, rect.shrink(pad), if selected { t.text_strong } else { t.icon });
+    let selected_ink = if t.flat { t.text } else { t.text_strong };
+    icons::paint(ui, icon, rect.shrink(pad), if selected { selected_ink } else { t.icon });
     resp.widget_info(|| icon_button_info(ui.is_enabled(), selected, tl!(tip)));
     if !tip.is_empty() { resp.on_hover_text(tl!(tip)) } else { resp }
 }
 
-/// Small flat text button (Quick Actions style).
+/// Small flat text button (Quick Actions style). Classic OG draws VectorCraft 0.11's outlined one.
 pub fn flat_button(ui: &mut Ui, text: &str, width: f32) -> Response {
     let t = Tokens::get(ui.ctx());
     let (rect, resp) = ui.allocate_exact_size(vec2(width, 24.0), Sense::click());
     let rect = rect.shrink2(vec2(0.0, 0.5));
-    ui.painter().rect_filled(rect, CornerRadius::same(5), t.button);
-    flat_fill(ui, rect, &resp);
+    if t.flat {
+        flat_fill(ui, rect, &resp);
+        outline(ui, &t, rect, resp.hovered());
+    } else {
+        ui.painter().rect_filled(rect, CornerRadius::same(5), t.button);
+        flat_fill(ui, rect, &resp);
+    }
     ui.painter().with_clip_rect(rect).text(rect.center(), egui::Align2::CENTER_CENTER, tl!(text), egui::FontId::proportional(12.5), t.text_strong);
     resp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, ui.is_enabled(), tl!(text)));
     resp
 }
 
+/// The outline of VectorCraft 0.11's flat buttons (Classic OG): brighter under the pointer.
+fn outline(ui: &Ui, t: &Tokens, rect: Rect, hovered: bool) {
+    ui.painter().rect_stroke(rect, flat_corners(t), Stroke::new(1.0, if hovered { t.text } else { t.button_border }), StrokeKind::Inside);
+}
+
+/// The corners of flat buttons and their fills: 0.11's 2 pt with Classic OG.
+fn flat_corners(t: &Tokens) -> CornerRadius {
+    CornerRadius::same(if t.flat { 2 } else { 5 })
+}
+
 /// Width of a [`split_button`]'s arrow.
 pub const SPLIT_ARROW: f32 = 18.0;
 
@@ -64,10 +81,18 @@ pub fn split_button_named(ui: &mut Ui, text: &str, arrow_tip: &str, width: f32)
     let arrow_rect = Rect::from_min_max(pos2(split, rect.top()), rect.max);
     let main = ui.interact(main_rect, ui.id().with((text, "split")), Sense::click());
     let arrow = ui.interact(arrow_rect, ui.id().with((text, "split-arrow")), Sense::click());
-    ui.painter().rect_filled(rect, CornerRadius::same(5), t.button);
-    flat_fill(ui, main_rect, &main);
-    flat_fill(ui, arrow_rect, &arrow);
-    ui.painter().vline(split, rect.y_range().shrink(5.0), Stroke::new(1.0, t.panel));
+    if t.flat {
+        // VectorCraft 0.11's outlined button, its arrow set off by a line in the outline's colour.
+        flat_fill(ui, main_rect, &main);
+        flat_fill(ui, arrow_rect, &arrow);
+        outline(ui, &t, rect, main.hovered() || arrow.hovered());
+        ui.painter().vline(split, rect.y_range().shrink(4.0), Stroke::new(1.0, t.button_border));
+    } else {
+        ui.painter().rect_filled(rect, CornerRadius::same(5), t.button);
+        flat_fill(ui, main_rect, &main);
+        flat_fill(ui, arrow_rect, &arrow);
+        ui.painter().vline(split, rect.y_range().shrink(5.0), Stroke::new(1.0, t.panel));
+    }
     ui.painter().with_clip_rect(main_rect).text(
         main_rect.center(),
         egui::Align2::CENTER_CENTER,
@@ -87,10 +112,11 @@ pub fn split_button_named(ui: &mut Ui, text: &str, arrow_tip: &str, width: f32)
 /// The pressed or hovered fill of a flat button part.
 fn flat_fill(ui: &Ui, rect: Rect, resp: &Response) {
     let t = Tokens::get(ui.ctx());
+    let r = flat_corners(&t);
     if resp.is_pointer_button_down_on() {
-        ui.painter().rect_filled(rect, CornerRadius::same(5), t.tool_active);
+        ui.painter().rect_filled(rect, r, t.tool_active);
     } else if resp.hovered() {
-        ui.painter().rect_filled(rect, CornerRadius::same(5), t.hover);
+        ui.painter().rect_filled(rect, r, t.hover);
     }
 }
 
@@ -117,21 +143,22 @@ fn primary_button_sized(ui: &mut Ui, text: &str, enabled: bool, min_width: f32)
     let galley = ui.painter().layout_no_wrap(tl!(text).to_string(), theme::semibold(12.5), fg);
     let w = galley.size().x + 32.0;
     let (rect, resp) = ui.allocate_exact_size(vec2(w.max(min_width), 28.0), if enabled { Sense::click() } else { Sense::hover() });
+    let r = t.button_radius(rect.height(), 6);
     let bg = match (enabled, resp.hovered()) {
         (false, _) => t.input,
         (true, true) => t.accent,
         (true, false) => t.accent_strong,
     };
-    ui.painter().rect_filled(rect, CornerRadius::same(6), bg);
+    ui.painter().rect_filled(rect, r, bg);
     if !enabled {
-        ui.painter().rect_stroke(rect, CornerRadius::same(6), egui::Stroke::new(1.0, t.button_border), egui::StrokeKind::Inside);
+        ui.painter().rect_stroke(rect, r, egui::Stroke::new(1.0, t.button_border), egui::StrokeKind::Inside);
     }
     ui.painter().galley(rect.center() - galley.size() / 2.0, galley, fg);
     resp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, enabled && ui.is_enabled(), tl!(text)));
     resp
 }
 
-/// The quiet secondary button (dialog Cancel).
+/// The quiet secondary button (dialog Cancel). Classic OG draws it as an outlined pill.
 pub fn secondary_button(ui: &mut Ui, text: &str) -> Response {
     secondary_button_width(ui, text, 72.0)
 }
@@ -142,7 +169,16 @@ pub(crate) fn secondary_button_width(ui: &mut Ui, text: &str, min_width: f32) ->
     let galley = ui.painter().layout_no_wrap(tl!(text).to_string(), theme::semibold(12.5), t.text);
     let w = galley.size().x + 32.0;
     let (rect, resp) = ui.allocate_exact_size(vec2(w.max(min_width), 28.0), Sense::click());
-    ui.painter().rect_filled(rect, CornerRadius::same(6), if resp.hovered() { t.hover } else { t.button });
+    if t.flat {
+        // VectorCraft 0.11's outlined pill.
+        let r = t.button_radius(rect.height(), 6);
+        if resp.hovered() {
+            ui.painter().rect_filled(rect, r, t.hover);
+        }
+        ui.painter().rect_stroke(rect, r, Stroke::new(1.5, t.text_dim), StrokeKind::Inside);
+    } else {
+        ui.painter().rect_filled(rect, CornerRadius::same(6), if resp.hovered() { t.hover } else { t.button });
+    }
     ui.painter().galley(rect.center() - galley.size() / 2.0, galley, t.text);
     resp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, ui.is_enabled(), tl!(text)));
     resp
@@ -152,7 +188,9 @@ pub(crate) fn secondary_button_width(ui: &mut Ui, text: &str, min_width: f32) ->
 pub fn section_header(ui: &mut Ui, text: &str) {
     let t = Tokens::get(ui.ctx());
     ui.add_space(2.0);
-    ui.label(egui::RichText::new(tl!(text)).size(12.5).color(t.text_strong));
+    // Classic OG: VectorCraft 0.11's headers.
+    let (size, color) = if t.flat { (13.0, t.text) } else { (12.5, t.text_strong) };
+    ui.label(egui::RichText::new(tl!(text)).size(size).color(color));
     ui.add_space(2.0);
 }
 
@@ -573,7 +611,8 @@ fn recessed_text(ui: &mut Ui, id: egui::Id, shown: &str, width: f32, rows: usize
         .allocate_ui_with_layout(vec2(width, height), layout, |ui| {
             ui.set_min_size(vec2(width, height));
             let rect = ui.max_rect();
-            ui.painter().rect(rect, 5.0, t.input, Stroke::new(1.0, if editing { t.accent } else { t.input_border }), StrokeKind::Inside);
+            let corners = if t.flat { 2.0 } else { 5.0 };
+            ui.painter().rect(rect, corners, t.input, Stroke::new(1.0, if editing { t.accent } else { t.input_border }), StrokeKind::Inside);
             let margin = egui::Margin { left: 7, right: 7, top: if rows > 1 { 4 } else { 0 }, bottom: 0 };
             let resp = egui::Frame::NONE
                 .inner_margin(margin)
@@ -1881,7 +1920,7 @@ pub fn hex_field(ui: &mut Ui, id: impl std::hash::Hash + std::fmt::Debug, hex: &
     let resp = egui::Frame::NONE
         .fill(t.input)
         .stroke(Stroke::new(1.0, if editing { t.accent } else { t.input_border }))
-        .corner_radius(5)
+        .corner_radius(if t.flat { 2 } else { 5 })
         .inner_margin(egui::Margin::symmetric(6, 4))
         .show(ui, |ui| {
             // As tall as the other fields (26 points with the margin), whatever the font's line height.
diff --git a/docs/control-protocol.md b/docs/control-protocol.md
--- a/docs/control-protocol.md
+++ b/docs/control-protocol.md
@@ -278,7 +278,12 @@ not commands, so drive them with `ui.click` if needed. Close runs `app.quit` (th
 modified documents), and empty bar space and the 5 pt window edges move and resize the window. Preferences › User
 Interface › System Title Bar (`systemTitleBar`, read when the app starts) gives the window its OS decorations back
 instead: the OS title then shows the active document (`*name — VectorCraft` with unsaved changes) and the app bar
-drops its brand mark. On macOS the window buttons are centred on the app bar; the web build is unchanged.
+drops its brand mark. On macOS the window buttons are centred on the app bar; the web build is unchanged. With the
+Classic OG colour scheme (`prefs.set {key: "uiColorScheme", value: "classicOg"}`) the caption buttons look like the
+desktop's, as PhotoCraft's do: Linux draws GNOME's (libadwaita), KDE's (Breeze, also SteamOS) or plain round buttons, in
+the desktop's button layout (either end of the bar), and `VECTORCRAFT_CAPTION_STYLE` (`windows`, `adwaita`, `breeze`,
+`generic`) and `VECTORCRAFT_CAPTION_LAYOUT` (a GTK layout such as `close,minimize:`) override what is found. The bar
+then also centres the active document's name (or VectorCraft) between the menus and the controls.
 
 Fill/Stroke chips and panel shortcuts: the Control bar's and Properties' Fill and Stroke chips bring their proxy
 forward (`paint.toggleActive {fill}`) and open a popover with the Swatches panel (Shift-click: the Color panel's mixer);
