           (.attr "font-weight" (fn [node]
                                  (if (identical? node selected-node) "bold" "normal")))
           (.text #(:title %))))
-    (.classed detail-button "invisible" (or (nil? selected-node)
-                                            (= projection :zoom-out)))
     (when animate?
       (.onNext animation-subject :animation-start)
       (js/setTimeout

(defn swap-state! [current-state f & args]
  (apply swap! current-state f args)
  (enforce-state-invariants! current-state)
  (sync-graph! @current-state))

(defn swap-state-animated! [current-state f & args]
  (apply swap! current-state f args)
  (enforce-state-invariants! current-state)
  (sync-graph! @current-state :animate? true))

(defn move-camera! [current-state d]
  (swap-state! current-state update :camera-pos v/add

(defn projectors-for-state [state]
  (if (= (:projection state) :fisheye) fisheye-projectors zoom-out-projectors))

;; Button syncing

(defn center-detail-button! [detail-button]
  (let [width (-> detail-button .node .-offsetWidth)]
    (.style detail-button "margin-left" (str (/ width -2) "px"))))

(defn sync-detail-button! [state]
  (let [{:keys [detail-button selected-node]} state]
    (.classed detail-button "hidden" (nil? selected-node))
    (when-not (nil? selected-node)
      (.text detail-button (str "View \"" (:title selected-node) "\""))
      (center-detail-button! detail-button))))

;; D3 magic

(defn sync-graph! [state & {:keys [animate?]}]
  (let [{:keys [svg
