diff --git a/contributors/people.toml b/contributors/people.toml
--- a/contributors/people.toml
+++ b/contributors/people.toml
@@ -67,3 +67,7 @@ sync_github_name = true
 
 [people.AganzeFelicite]
 sync_github_name = true
+
+[people.amorsoftlab]
+real_name = "Jaseem"
+sync_github_name = true
diff --git a/crates/ui-egui/locales/en.toml b/crates/ui-egui/locales/en.toml
--- a/crates/ui-egui/locales/en.toml
+++ b/crates/ui-egui/locales/en.toml
@@ -96,6 +96,7 @@ name = "English"
 "Alpha" = "Alpha"
 "Alphabetical" = "Alphabetical"
 "Alt+drag" = "Alt+drag"
+"Amber" = "Amber"
 "An analytics screen on a grid: figures, charts drawn from vectors and a table" = "An analytics screen on a grid: figures, charts drawn from vectors and a table"
 "An invite link that opens FigCraft and joins" = "An invite link that opens FigCraft and joins"
 "Anchor" = "Anchor"
@@ -112,6 +113,7 @@ name = "English"
 "Anyone with this code can look at the file, not change it." = "Anyone with this code can look at the file, not change it."
 "Appearance" = "Appearance"
 "Apply a component property" = "Apply a component property"
+"Apply to all main frames" = "Apply to all main frames"
 "Apply variable" = "Apply variable"
 "Apply variable to text" = "Apply variable to text"
 "Apply variable to visibility" = "Apply variable to visibility"
@@ -588,6 +590,7 @@ name = "English"
 "Hide section contents" = "Hide section contents"
 "Hide these tips (Getting started in the Help menu shows them again)" = "Hide these tips (Getting started in the Help menu shows them again)"
 "Hide when publishing" = "Hide when publishing"
+"Highlight color" = "Highlight color"
 "Highlight layers on hover" = "Highlight layers on hover"
 "Highlights" = "Highlights"
 "Hold Alt" = "Hold Alt"
@@ -989,6 +992,7 @@ name = "English"
 "Pick one font family first" = "Pick one font family first"
 "Pick the Frame tool, then drag on the canvas or choose a device size" = "Pick the Frame tool, then drag on the canvas or choose a device size"
 "Pin a property" = "Pin a property"
+"Pink" = "Pink"
 "Pinch" = "Pinch"
 "Pixel grid" = "Pixel grid"
 "Pixel preview 1x" = "Pixel preview 1x"
@@ -1047,6 +1051,7 @@ name = "English"
 "Publishing to a server needs the desktop app; in the browser you can join a server's files" = "Publishing to a server needs the desktop app; in the browser you can join a server's files"
 "Publishing with the server's key." = "Publishing with the server's key."
 "Publishing…" = "Publishing…"
+"Purple" = "Purple"
 "Push" = "Push"
 "Push changes to main component" = "Push changes to main component"
 "Put text on path" = "Put text on path"
@@ -1120,6 +1125,7 @@ name = "English"
 "Reset all overrides on this layer" = "Reset all overrides on this layer"
 "Reset overrides" = "Reset overrides"
 "Reset slot" = "Reset slot"
+"Reset to default" = "Reset to default"
 "Reset this override" = "Reset this override"
 "Reset to style" = "Reset to style"
 "Reset to the inherited value" = "Reset to the inherited value"
@@ -1357,6 +1363,7 @@ name = "English"
 "Take the values from the first selected layer" = "Take the values from the first selected layer"
 "Taper" = "Taper"
 "Taper both ends" = "Taper both ends"
+"Teal" = "Teal"
 "Tell the server's operator what is wrong with it" = "Tell the server's operator what is wrong with it"
 "Temperature" = "Temperature"
 "Template" = "Template"
diff --git a/crates/ui-egui/locales/es.toml b/crates/ui-egui/locales/es.toml
--- a/crates/ui-egui/locales/es.toml
+++ b/crates/ui-egui/locales/es.toml
@@ -99,6 +99,7 @@ status = "draft — needs native review"
 "Alpha" = "Alfa"
 "Alphabetical" = "Alfabético"
 "Alt+drag" = "Alt+arrastrar"
+"Amber" = "Ámbar"
 "An analytics screen on a grid: figures, charts drawn from vectors and a table" = "Una pantalla de analítica en cuadrícula: cifras, gráficos dibujados con vectores y una tabla"
 "An invite link that opens FigCraft and joins" = "Un enlace de invitación que abre FigCraft y se une"
 "Anchor" = "Ancla"
@@ -115,6 +116,7 @@ status = "draft — needs native review"
 "Anyone with this code can look at the file, not change it." = "Cualquiera con este código puede ver el archivo, no cambiarlo."
 "Appearance" = "Apariencia"
 "Apply a component property" = "Aplicar una propiedad de componente"
+"Apply to all main frames" = "Aplicar a todos los marcos principales"
 "Apply variable" = "Aplicar variable"
 "Apply variable to text" = "Aplicar variable al texto"
 "Apply variable to visibility" = "Aplicar variable a la visibilidad"
@@ -589,6 +591,7 @@ status = "draft — needs native review"
 "Hide section contents" = "Ocultar contenido de la sección"
 "Hide these tips (Getting started in the Help menu shows them again)" = "Ocultar estos consejos (Primeros pasos en el menú Ayuda los vuelve a mostrar)"
 "Hide when publishing" = "Ocultar al publicar"
+"Highlight color" = "Color de resaltado"
 "Highlight layers on hover" = "Resaltar capas al pasar el cursor"
 "Highlights" = "Luces"
 "Hold Alt" = "Mantén Alt"
@@ -990,6 +993,7 @@ status = "draft — needs native review"
 "Pick one font family first" = "Elige primero una familia de fuentes"
 "Pick the Frame tool, then drag on the canvas or choose a device size" = "Elige la herramienta Marco y arrastra en el lienzo o escoge un tamaño de dispositivo"
 "Pin a property" = "Fijar una propiedad"
+"Pink" = "Rosa"
 "Pinch" = "Pellizco"
 "Pixel grid" = "Cuadrícula de píxeles"
 "Pixel preview 1x" = "Vista previa de píxeles 1x"
@@ -1048,6 +1052,7 @@ status = "draft — needs native review"
 "Publishing to a server needs the desktop app; in the browser you can join a server's files" = "Para publicar en un servidor necesitas la app de escritorio; en el navegador puedes unirte a archivos de un servidor"
 "Publishing with the server's key." = "Se publica con la clave del servidor."
 "Publishing…" = "Publicando…"
+"Purple" = "Morado"
 "Push" = "Empujar"
 "Push changes to main component" = "Enviar cambios al componente principal"
 "Put text on path" = "Poner texto en trazado"
@@ -1121,6 +1126,7 @@ status = "draft — needs native review"
 "Reset all overrides on this layer" = "Restablecer todas las modificaciones de esta capa"
 "Reset overrides" = "Restablecer modificaciones"
 "Reset slot" = "Restablecer ranura"
+"Reset to default" = "Restablecer a predeterminado"
 "Reset this override" = "Restablecer esta modificación"
 "Reset to style" = "Restablecer al estilo"
 "Reset to the inherited value" = "Restablecer al valor heredado"
@@ -1358,6 +1364,7 @@ status = "draft — needs native review"
 "Take the values from the first selected layer" = "Tomar los valores de la primera capa seleccionada"
 "Taper" = "Afinado"
 "Taper both ends" = "Afinar ambos extremos"
+"Teal" = "Verde azulado"
 "Tell the server's operator what is wrong with it" = "Explica al operador del servidor qué tiene de malo"
 "Temperature" = "Temperatura"
 "Template" = "Plantilla"
diff --git a/crates/ui-egui/src/layers.rs b/crates/ui-egui/src/layers.rs
--- a/crates/ui-egui/src/layers.rs
+++ b/crates/ui-egui/src/layers.rs
@@ -690,6 +690,8 @@ fn search_field(ui: &mut Ui, t: &Tokens, w: f32, id: Id, hint: &str, text: &mut
     }
 }
 
+/// The layers list panel: tree view of nodes under the current page with search, expand/collapse,
+/// visibility and lock toggles, renaming, reordering, and context menu dispatch.
 fn layers(app: &mut FigApp, ui: &mut Ui) {
     let t = Tokens::get(ui.ctx());
     let w = ui.available_width();
@@ -710,6 +712,7 @@ fn layers(app: &mut FigApp, ui: &mut Ui) {
         search_field(ui, &t, w, search_id, tr("Find layers"), &mut app.ui.layer_filter, &mut app.ui.layer_search, sresp.clicked());
     }
     let Ok(ds) = app.session.ds() else { return };
+    let doc_uid = ds.uid;
     let doc = ds.doc.clone();
     let page = ds.page;
     let sel = ds.selection.clone();
@@ -849,6 +852,13 @@ fn layers(app: &mut FigApp, ui: &mut Ui) {
             if over {
                 hovered_row = Some(row.id);
             }
+            let is_main_group = crate::menus::is_main_level_node(&doc, row.id);
+            let custom_color = app
+                .ui
+                .layer_colors
+                .get(&(doc_uid, row.id))
+                .copied()
+                .or_else(|| is_main_group.then(|| app.ui.main_group_color.get(&doc_uid).copied().unwrap_or(t.accent_text)));
             let x0 = row_x0(rr, row.depth);
             // Expand triangle.
             if row.has_kids {
@@ -856,7 +866,8 @@ fn layers(app: &mut FigApp, ui: &mut Ui) {
                 let tresp = ui.interact(tri, ui.id().with(("tri", row.id)), Sense::click());
                 let open = app.ui.expanded.contains(&row.id);
                 a11y::button(&tresp, if open { tr("Collapse") } else { tr("Expand") });
-                icons::paint(ui.painter(), tri.shrink(1.0), if open { Icon::Chevron } else { Icon::ChevronRight }, t.icon_secondary);
+                let tri_col = custom_color.unwrap_or(t.icon_secondary);
+                icons::paint(ui.painter(), tri.shrink(1.0), if open { Icon::Chevron } else { Icon::ChevronRight }, tri_col);
                 if tresp.clicked() {
                     if ui.input(|i| i.modifiers.alt) {
                         deep = Some((row.id, !open));
@@ -867,8 +878,12 @@ fn layers(app: &mut FigApp, ui: &mut Ui) {
             }
             let icon = node_icon(&doc, row.id);
             let shown = n.visible && !row.dimmed;
-            let icon_col = if is_comp || icon == Icon::Slot {
+            let icon_col = if let Some(col) = app.ui.layer_colors.get(&(doc_uid, row.id)).copied() {
+                col.gamma_multiply(if shown { 1.0 } else { 0.5 })
+            } else if is_comp || icon == Icon::Slot {
                 COMPONENT.gamma_multiply(if shown { 1.0 } else { 0.5 })
+            } else if let Some(col) = custom_color {
+                col.gamma_multiply(if shown { 1.0 } else { 0.5 })
             } else if shown {
                 t.icon
             } else {
@@ -881,20 +896,18 @@ fn layers(app: &mut FigApp, ui: &mut Ui) {
                     rename_done = Some((row.id, new));
                 }
             } else {
-                let col = if is_comp {
+                let col = if let Some(col) = app.ui.layer_colors.get(&(doc_uid, row.id)).copied() {
+                    col.gamma_multiply(if shown { 1.0 } else { 0.5 })
+                } else if is_comp {
                     COMPONENT.gamma_multiply(if shown { 1.0 } else { 0.5 })
+                } else if let Some(col) = custom_color {
+                    col.gamma_multiply(if shown { 1.0 } else { 0.5 })
                 } else if shown {
                     t.text
                 } else {
                     t.text_tertiary
                 };
-                let font = if doc.node(n.parent.unwrap_or(page)).is_some_and(|p| p.is_page())
-                    && (n.kind.is_frame_like() || matches!(n.kind, NodeKind::Section { .. }))
-                {
-                    semibold(11.0)
-                } else {
-                    regular(11.0)
-                };
+                let font = if custom_color.is_some() || is_comp { semibold(11.0) } else { regular(11.0) };
                 let name = if n.name.is_empty() { tr(n.kind.label()).to_string() } else { n.name.clone() };
                 // Room left of the lock and eye buttons.
                 let max_w = (rr.max.x - 56.0 - name_x).max(20.0);
@@ -949,7 +962,8 @@ fn layers(app: &mut FigApp, ui: &mut Ui) {
                     clicked = Some((row.id, egui::Modifiers::NONE));
                 }
                 let pos = ui.input(|i| i.pointer.hover_pos()).unwrap_or(r.center());
-                app.ui.context_menu = Some((pos, vec![row.id]));
+                let menu_ids = if sel.contains(&row.id) { sel.clone() } else { vec![row.id] };
+                app.ui.context_menu = Some((pos, menu_ids));
             }
             // Drag to reorder.
             if resp.drag_started() {
diff --git a/crates/ui-egui/src/layers_behaviour_tests.rs b/crates/ui-egui/src/layers_behaviour_tests.rs
--- a/crates/ui-egui/src/layers_behaviour_tests.rs
+++ b/crates/ui-egui/src/layers_behaviour_tests.rs
@@ -507,3 +507,25 @@ fn the_pages_list_keeps_three_rows_and_leaves_the_layers_room() {
     // A panel too short for both keeps the three rows.
     assert_eq!(pages_list_height(Some(500.0), 100.0), 3.0 * ROW);
 }
+
+#[test]
+fn layer_highlight_colors_are_scoped_by_document_and_main_level() {
+    let mut s = Session::new();
+    let rect = id(s.execute("shape.rectangle", &json!({"x": 10, "y": 10, "width": 50, "height": 50})).unwrap());
+    let frame = id(s.execute("object.frameSelection", &json!({"ids": [rect]})).unwrap());
+    let doc = s.doc().unwrap();
+    assert!(crate::menus::is_main_level_node(&doc, frame));
+    assert!(!crate::menus::is_main_level_node(&doc, rect));
+
+    let uid1 = s.docs[s.active].uid;
+    let mut u = Ui::new(s);
+    let color = egui::Color32::from_rgb(0x3B, 0x82, 0xF6);
+    u.app.ui.layer_colors.insert((uid1, rect), color);
+    u.app.ui.main_group_color.insert(uid1, color);
+
+    let uid2 = uid1 + 999;
+    assert_eq!(u.app.ui.layer_colors.get(&(uid1, rect)).copied(), Some(color));
+    assert_eq!(u.app.ui.layer_colors.get(&(uid2, rect)).copied(), None);
+    assert_eq!(u.app.ui.main_group_color.get(&uid1).copied(), Some(color));
+    assert_eq!(u.app.ui.main_group_color.get(&uid2).copied(), None);
+}
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
@@ -551,6 +551,10 @@ pub struct UiState {
     pub preview: Option<present::Pane>,
     pub picker: Option<colorpicker::PickerTarget>,
     pub context_menu: Option<(egui::Pos2, Vec<NodeId>)>,
+    /// Custom layer highlight colors assigned by the user in the context menu, per open file (tab `uid`).
+    pub layer_colors: std::collections::HashMap<(u64, NodeId), egui::Color32>,
+    /// Highlight color for the top-level frames, sections and groups of each open file (tab `uid`).
+    pub main_group_color: std::collections::HashMap<u64, egui::Color32>,
     /// The context menu was asked for from the keyboard (`view.contextMenu`): it opens at the
     /// focused layer row or the canvas selection and takes the keyboard.
     pub key_menu: bool,
@@ -700,6 +704,8 @@ impl Default for UiState {
             preview: None,
             picker: None,
             context_menu: None,
+            layer_colors: std::collections::HashMap::new(),
+            main_group_color: std::collections::HashMap::new(),
             key_menu: false,
             key_box: None,
             layer_search: false,
diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs
--- a/crates/ui-egui/src/menus.rs
+++ b/crates/ui-egui/src/menus.rs
@@ -757,6 +757,7 @@ const TOP_MENUS: &[&str] = &[
     tr_noop("Help"),
 ];
 
+/// Draws active popups: the main menu, the layer and canvas context menus, and the Actions palette.
 pub fn popups(app: &mut FigApp, ctx: &egui::Context) {
     let t = Tokens::get(ctx);
     // A closed menu forgets its open submenus (and a context menu opened somewhere else starts
@@ -910,6 +911,9 @@ pub fn popups(app: &mut FigApp, ctx: &egui::Context) {
                 {
                     entries.insert(at + 1, entry);
                 }
+                if has_sel && let Some(at) = entries.iter().rposition(|e| e.1 == "object.toggleLock" || e.1 == "section.toggleContents") {
+                    entries.insert(at + 1, (tr("Highlight color"), "ui.highlightColor"));
+                }
                 // A right-clicked ruler guide (selected, with no layers) can be removed.
                 if !has_sel && app.session.ds().is_ok_and(|d| d.selected_guide().is_some()) {
                     entries.splice(0..0, [(tr("Remove guide"), "guide.remove"), ("-", "")]);
@@ -919,6 +923,12 @@ pub fn popups(app: &mut FigApp, ctx: &egui::Context) {
                         separator(ui);
                         continue;
                     }
+                    if *id == "ui.highlightColor" {
+                        if highlight_color_menu(app, ui, &ids, &t) {
+                            close = true;
+                        }
+                        continue;
+                    }
                     let sc = crate::shortcuts::shortcut(id).unwrap_or("");
                     if item(ui, label, sc, available(id), false, &t) {
                         if *id == "edit.paste" {
@@ -994,6 +1004,124 @@ fn select_layer_menu(app: &mut FigApp, ui: &mut Ui, pos: egui::Pos2, t: &Tokens)
     }
 }
 
+/// A menu item with a color swatch indicator on the left.
+fn color_item(ui: &mut Ui, label: &str, color: Color32, checked: bool, t: &Tokens) -> bool {
+    let w = ui.available_width();
+    let (r, resp) = ui.allocate_exact_size(vec2(w, 28.0), Sense::click());
+    a11y::menu_item(&resp, label, "", checked.then_some(true));
+    a11y::own_focus(&resp);
+    let hov = plain_row_hover(ui, &resp);
+    if hov {
+        ui.painter().rect_filled(r, CornerRadius::same(5), t.menu_hover);
+    }
+    let swatch_rect = Rect::from_center_size(pos2(r.min.x + 14.0, r.center().y), vec2(12.0, 12.0));
+    ui.painter().rect_filled(swatch_rect, CornerRadius::same(3), color);
+    if checked {
+        icons::paint(ui.painter(), swatch_rect.shrink(1.0), Icon::Check, Color32::WHITE);
+    }
+    let galley = ui.painter().layout_no_wrap(label.to_string(), regular(12.0), t.menu_text);
+    need_width(ui, 32.0 + galley.size().x + 20.0);
+    ui.painter().galley(pos2(r.min.x + 30.0, r.center().y - galley.size().y / 2.0), galley, t.menu_text);
+    resp.clicked()
+}
+
+/// Returns true if a node is a top-level frame-like, section, or group directly on a page.
+pub(crate) fn is_main_level_node(doc: &figcraft_doc::Document, id: figcraft_doc::NodeId) -> bool {
+    doc.node(id).is_some_and(|n| {
+        n.parent.and_then(|p| doc.node(p)).is_some_and(|p| p.is_page())
+            && (n.kind.is_frame_like() || matches!(n.kind, figcraft_doc::NodeKind::Section { .. } | figcraft_doc::NodeKind::Group))
+    })
+}
+
+/// The context menu's "Highlight color" submenu to customize layer / main frame colors.
+fn highlight_color_menu(app: &mut FigApp, ui: &mut Ui, ids: &[figcraft_doc::NodeId], t: &Tokens) -> bool {
+    enum Action {
+        Set(Option<Color32>),
+        ApplyAll(Option<Color32>),
+        Reset,
+    }
+    let doc_uid = app.session.ds().ok().map(|ds| ds.uid).unwrap_or(0);
+    let first_id =
+        ids.iter().copied().find(|id| app.session.doc().ok().is_some_and(|doc| is_main_level_node(doc, *id))).or_else(|| ids.first().copied());
+    let first_is_main = first_id.is_some_and(|id| app.session.doc().ok().is_some_and(|doc| is_main_level_node(doc, id)));
+    let current_custom = first_id.and_then(|id| app.ui.layer_colors.get(&(doc_uid, id)).copied()).or(if first_is_main {
+        app.ui.main_group_color.get(&doc_uid).copied()
+    } else {
+        None
+    });
+
+    let picked = submenu_at(ui, Id::new("ctx_highlight_color"), "highlight_color", tr("Highlight color"), 4.0, true, t, |ui| {
+        let mut act = None;
+        let colors: [(&str, Option<Color32>, Color32); 10] = [
+            (tr("Default"), None, t.accent_text),
+            (tr("Blue"), Some(Color32::from_rgb(0x3B, 0x82, 0xF6)), Color32::from_rgb(0x3B, 0x82, 0xF6)),
+            (tr("Purple"), Some(Color32::from_rgb(0xA8, 0x55, 0xF7)), Color32::from_rgb(0xA8, 0x55, 0xF7)),
+            (tr("Pink"), Some(Color32::from_rgb(0xEC, 0x48, 0x99)), Color32::from_rgb(0xEC, 0x48, 0x99)),
+            (tr("Red"), Some(Color32::from_rgb(0xEF, 0x44, 0x44)), Color32::from_rgb(0xEF, 0x44, 0x44)),
+            (tr("Orange"), Some(Color32::from_rgb(0xF9, 0x73, 0x16)), Color32::from_rgb(0xF9, 0x73, 0x16)),
+            (tr("Amber"), Some(Color32::from_rgb(0xF5, 0x9E, 0x0B)), Color32::from_rgb(0xF5, 0x9E, 0x0B)),
+            (tr("Green"), Some(Color32::from_rgb(0x10, 0xB9, 0x81)), Color32::from_rgb(0x10, 0xB9, 0x81)),
+            (tr("Teal"), Some(Color32::from_rgb(0x14, 0xB8, 0xA6)), Color32::from_rgb(0x14, 0xB8, 0xA6)),
+            (tr("Gray"), Some(Color32::from_rgb(0x94, 0xA3, 0xB8)), Color32::from_rgb(0x94, 0xA3, 0xB8)),
+        ];
+        for (name, val, swatch_col) in colors {
+            let is_checked = current_custom == val;
+            if color_item(ui, name, swatch_col, is_checked, t) {
+                act = Some(Action::Set(val));
+            }
+        }
+        separator(ui);
+        if item(ui, tr("Apply to all main frames"), "", true, false, t) {
+            act = Some(Action::ApplyAll(current_custom));
+        }
+        if item(ui, tr("Reset to default"), "", true, false, t) {
+            act = Some(Action::Reset);
+        }
+        act
+    });
+
+    if let Some(act) = picked {
+        match act {
+            Action::Set(val) => {
+                for id in ids {
+                    if let Some(col) = val {
+                        app.ui.layer_colors.insert((doc_uid, *id), col);
+                    } else {
+                        app.ui.layer_colors.remove(&(doc_uid, *id));
+                    }
+                }
+            }
+            Action::ApplyAll(val) => {
+                if let Some(col) = val {
+                    app.ui.main_group_color.insert(doc_uid, col);
+                } else {
+                    app.ui.main_group_color.remove(&doc_uid);
+                }
+                if let Ok(doc) = app.session.doc() {
+                    app.ui.layer_colors.retain(|(uid, id), _| {
+                        if *uid != doc_uid {
+                            return true;
+                        }
+                        !is_main_level_node(doc, *id)
+                    });
+                }
+            }
+            Action::Reset => {
+                for id in ids {
+                    app.ui.layer_colors.remove(&(doc_uid, *id));
+                }
+                let touches_main = app.session.doc().ok().is_some_and(|doc| ids.iter().any(|i| is_main_level_node(doc, *i)));
+                if touches_main {
+                    app.ui.main_group_color.remove(&doc_uid);
+                }
+            }
+        }
+        true
+    } else {
+        false
+    }
+}
+
 /// The Actions palette's content height (the frame adds its own 8 px margins).
 const PALETTE_H: f32 = 450.0;
 
