diff --git a/app-proto/main.css b/app-proto/main.css
index e41fcd9..f544e19 100644
--- a/app-proto/main.css
+++ b/app-proto/main.css
@@ -127,7 +127,7 @@ details[open]:has(li) .element-switch::after {
   font-style: italic;
 }
 
-.observable.invalid {
+.observable.invalid-constraint {
   color: var(--text-invalid);
 }
 
@@ -138,7 +138,7 @@ details[open]:has(li) .element-switch::after {
   border-radius: 2px;
 }
 
-.observable.invalid > input[type=text] {
+.observable.invalid-constraint > input[type=text] {
   border-color: var(--border-invalid);
 }
 
@@ -150,11 +150,11 @@ details[open]:has(li) .element-switch::after {
   font-style: normal;
 }
 
-.constrained > .status::after, details:has(.constrained):not([open]) .status::after {
+.valid-constraint > .status::after, details:has(.valid-constraint):not([open]) .status::after {
   content: '🔗';
 }
 
-.invalid > .status::after, details:has(.invalid):not([open]) .status::after {
+.invalid-constraint > .status::after, details:has(.invalid-constraint):not([open]) .status::after {
   content: 'âš ';
   color: var(--text-invalid);
 }
diff --git a/app-proto/src/add_remove.rs b/app-proto/src/add_remove.rs
index 07b4c28..640925a 100644
--- a/app-proto/src/add_remove.rs
+++ b/app-proto/src/add_remove.rs
@@ -210,7 +210,7 @@ pub fn AddRemove() -> View {
                         }
                     );
                     let desired = create_signal(0.0);
-                    let role = create_signal(ObservableRole::Measure);
+                    let role = create_signal(ObservableRole::Measurement);
                     state.assembly.insert_observable(Observable {
                         subjects: subjects,
                         measured: measured,
@@ -242,7 +242,7 @@ pub fn AddRemove() -> View {
                             format!("Updated constraint with subjects ({}, {})", subjects.0, subjects.1)
                         ));
                         desired.track();
-                        if role.with(|r| matches!(r, ObservableRole::Constrain)) {
+                        if role.with(|rl| rl.is_valid_constraint()) {
                             state.assembly.realize();
                         }
                     });
diff --git a/app-proto/src/assembly.rs b/app-proto/src/assembly.rs
index e438934..ba2f9c5 100644
--- a/app-proto/src/assembly.rs
+++ b/app-proto/src/assembly.rs
@@ -112,9 +112,17 @@ impl Element {
 }
 
 pub enum ObservableRole {
-    Measure,
-    Constrain,
-    Invalid
+    Measurement,
+    Constraint(bool)
+}
+
+impl ObservableRole {
+    pub fn is_valid_constraint(&self) -> bool {
+        match self {
+            ObservableRole::Measurement => false,
+            ObservableRole::Constraint(valid) => *valid
+        }
+    }
 }
 
 #[derive(Clone)]
@@ -126,6 +134,12 @@ pub struct Observable {
     pub role: Signal<ObservableRole>
 }
 
+impl Observable {
+    fn role_is_valid_constraint_untracked(&self) -> bool {
+        self.role.with_untracked(|role| role.is_valid_constraint())
+    }
+}
+
 // the velocity is expressed in uniform coordinates
 pub struct ElementMotion<'a> {
     pub key: ElementKey,
@@ -236,7 +250,7 @@ impl Assembly {
             let mut gram_to_be = PartialMatrix::new();
             self.observables.with_untracked(|obsls| {
                 for (_, obs) in obsls {
-                    if obs.role.with_untracked(|role| matches!(role, ObservableRole::Constrain)) {
+                    if obs.role_is_valid_constraint_untracked() {
                         let subjects = obs.subjects;
                         let row = elts[subjects.0].column_index.unwrap();
                         let col = elts[subjects.1].column_index.unwrap();
diff --git a/app-proto/src/outline.rs b/app-proto/src/outline.rs
index 5444974..d2f9058 100644
--- a/app-proto/src/outline.rs
+++ b/app-proto/src/outline.rs
@@ -31,14 +31,14 @@ fn ObservableInput(observable: Observable) -> View {
                 let target: HtmlInputElement = event.target().unwrap().unchecked_into();
                 let value = target.value();
                 if value.is_empty() {
-                    observable.role.set(Measure);
+                    observable.role.set(Measurement);
                 } else {
                     match target.value().parse::<f64>() {
                         Ok(desired) => batch(|| {
                             observable.desired.set(desired);
-                            observable.role.set(Constrain);
+                            observable.role.set(Constraint(true));
                         }),
-                        Err(_) => observable.role.set(Invalid)
+                        Err(_) => observable.role.set(Constraint(false))
                     };
                 }
             }
@@ -60,9 +60,9 @@ fn ObservableOutlineItem(observable_key: ObservableKey, element_key: ElementKey)
     let other_subject_label = assembly.elements.with(|elts| elts[other_subject].label.clone());
     let class = observable.role.map(
         |role| match role {
-            Measure => "observable",
-            Constrain => "observable constrained",
-            Invalid => "observable invalid"
+            Measurement => "observable",
+            Constraint(true) => "observable valid-constraint",
+            Constraint(false) => "observable invalid-constraint"
         }
     );
     view! {