diff --git a/public/docs-static/img/manage/control-center/control-center-draft-add-group.mp4 b/public/docs-static/img/manage/control-center/control-center-draft-add-group.mp4
new file mode 100644
index 000000000..0739dbd5f
Binary files /dev/null and b/public/docs-static/img/manage/control-center/control-center-draft-add-group.mp4 differ
diff --git a/public/docs-static/img/manage/control-center/control-center-draft-add-server.mp4 b/public/docs-static/img/manage/control-center/control-center-draft-add-server.mp4
new file mode 100644
index 000000000..d098dcefe
Binary files /dev/null and b/public/docs-static/img/manage/control-center/control-center-draft-add-server.mp4 differ
diff --git a/public/docs-static/img/manage/control-center/control-center-draft-components-panel.png b/public/docs-static/img/manage/control-center/control-center-draft-components-panel.png
new file mode 100644
index 000000000..e47acd735
Binary files /dev/null and b/public/docs-static/img/manage/control-center/control-center-draft-components-panel.png differ
diff --git a/public/docs-static/img/manage/control-center/control-center-draft-connect-policy.mp4 b/public/docs-static/img/manage/control-center/control-center-draft-connect-policy.mp4
new file mode 100644
index 000000000..113889b1c
Binary files /dev/null and b/public/docs-static/img/manage/control-center/control-center-draft-connect-policy.mp4 differ
diff --git a/public/docs-static/img/manage/control-center/control-center-draft-discard.png b/public/docs-static/img/manage/control-center/control-center-draft-discard.png
new file mode 100644
index 000000000..728ba823c
Binary files /dev/null and b/public/docs-static/img/manage/control-center/control-center-draft-discard.png differ
diff --git a/public/docs-static/img/manage/control-center/control-center-draft-drag-to-group.mp4 b/public/docs-static/img/manage/control-center/control-center-draft-drag-to-group.mp4
new file mode 100644
index 000000000..e3a81cef6
Binary files /dev/null and b/public/docs-static/img/manage/control-center/control-center-draft-drag-to-group.mp4 differ
diff --git a/public/docs-static/img/manage/control-center/control-center-draft-enter.mp4 b/public/docs-static/img/manage/control-center/control-center-draft-enter.mp4
new file mode 100644
index 000000000..be124f814
Binary files /dev/null and b/public/docs-static/img/manage/control-center/control-center-draft-enter.mp4 differ
diff --git a/public/docs-static/img/manage/control-center/control-center-draft-group-from-selection.mp4 b/public/docs-static/img/manage/control-center/control-center-draft-group-from-selection.mp4
new file mode 100644
index 000000000..821c44b0a
Binary files /dev/null and b/public/docs-static/img/manage/control-center/control-center-draft-group-from-selection.mp4 differ
diff --git a/public/docs-static/img/manage/control-center/control-center-draft-install-modal.png b/public/docs-static/img/manage/control-center/control-center-draft-install-modal.png
new file mode 100644
index 000000000..26f633cc2
Binary files /dev/null and b/public/docs-static/img/manage/control-center/control-center-draft-install-modal.png differ
diff --git a/public/docs-static/img/manage/control-center/control-center-draft-rename.png b/public/docs-static/img/manage/control-center/control-center-draft-rename.png
new file mode 100644
index 000000000..7467ffa62
Binary files /dev/null and b/public/docs-static/img/manage/control-center/control-center-draft-rename.png differ
diff --git a/public/docs-static/img/manage/control-center/control-center-draft-review-issues.png b/public/docs-static/img/manage/control-center/control-center-draft-review-issues.png
new file mode 100644
index 000000000..5766504e3
Binary files /dev/null and b/public/docs-static/img/manage/control-center/control-center-draft-review-issues.png differ
diff --git a/public/docs-static/img/manage/control-center/control-center-focus-active.png b/public/docs-static/img/manage/control-center/control-center-focus-active.png
new file mode 100644
index 000000000..a9dd81c6f
Binary files /dev/null and b/public/docs-static/img/manage/control-center/control-center-focus-active.png differ
diff --git a/public/docs-static/img/manage/control-center/control-center-focus-context-menu.png b/public/docs-static/img/manage/control-center/control-center-focus-context-menu.png
new file mode 100644
index 000000000..e497b38cb
Binary files /dev/null and b/public/docs-static/img/manage/control-center/control-center-focus-context-menu.png differ
diff --git a/src/components/NavigationDocs.jsx b/src/components/NavigationDocs.jsx
index 7463b2287..327e55f81 100644
--- a/src/components/NavigationDocs.jsx
+++ b/src/components/NavigationDocs.jsx
@@ -81,7 +81,14 @@ export const docsNavigation = [
{
title: 'MANAGE NETBIRD',
links: [
- { title: 'Control Center', href: '/manage/control-center' },
+ {
+ title: 'Control Center',
+ isOpen: false,
+ links: [
+ { title: 'Overview', href: '/manage/control-center' },
+ { title: 'Draft Mode', href: '/manage/control-center/draft-mode' },
+ ],
+ },
{
title: 'Peers',
isOpen: false,
diff --git a/src/components/Video.jsx b/src/components/Video.jsx
new file mode 100644
index 000000000..c9e51e256
--- /dev/null
+++ b/src/components/Video.jsx
@@ -0,0 +1,74 @@
+import { useEffect, useRef } from 'react'
+import clsx from 'clsx'
+
+/**
+ * Looping screen-recording embed for docs pages.
+ *
+ * Recordings load lazily and play only while on screen: `preload="metadata"`
+ * avoids buffering full files up front, and an IntersectionObserver starts
+ * playback when the video scrolls into view and pauses it when it leaves.
+ * Controls stay visible so the loop can be paused (WCAG 2.2.2), and autoplay
+ * is skipped entirely for users who prefer reduced motion.
+ *
+ * Usage:
+ *
+ */
+export function Video({ src, label, className, ...props }) {
+ const ref = useRef(null)
+
+ useEffect(() => {
+ const video = ref.current
+ if (!video) return
+
+ const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)')
+ let observer = null
+
+ const observe = () => {
+ observer = new IntersectionObserver(
+ ([entry]) => {
+ if (entry.isIntersecting) {
+ video.play().catch(() => {})
+ } else {
+ video.pause()
+ }
+ },
+ { threshold: 0.25 }
+ )
+ observer.observe(video)
+ }
+
+ // React to the OS setting changing while the page is open.
+ const handleMotionChange = () => {
+ if (reducedMotion.matches) {
+ observer?.disconnect()
+ observer = null
+ video.pause()
+ } else if (!observer) {
+ observe()
+ }
+ }
+
+ if (!reducedMotion.matches) observe()
+ reducedMotion.addEventListener('change', handleMotionChange)
+
+ return () => {
+ reducedMotion.removeEventListener('change', handleMotionChange)
+ observer?.disconnect()
+ }
+ }, [])
+
+ return (
+
+ )
+}
diff --git a/src/components/mdx.jsx b/src/components/mdx.jsx
index 4172ce6de..636adb60d 100644
--- a/src/components/mdx.jsx
+++ b/src/components/mdx.jsx
@@ -9,6 +9,7 @@ export { Button } from '@/components/Button'
export { CodeGroup, Code as code, Pre as pre } from '@/components/Code'
export { Badge } from '@/components/Badge'
export { YouTube }
+export { Video } from '@/components/Video'
export const h2 = function H2(props) {
return
diff --git a/src/pages/manage/control-center/draft-mode.mdx b/src/pages/manage/control-center/draft-mode.mdx
new file mode 100644
index 000000000..6ee5ac828
--- /dev/null
+++ b/src/pages/manage/control-center/draft-mode.mdx
@@ -0,0 +1,229 @@
+import {Note, Warning, Video} from "@/components/mdx"
+
+export const description =
+ 'Stage network changes on a working copy of the Control Center canvas, review the exact API requests, and apply everything as one batch with Draft Mode.'
+
+# Plan and Deploy Changes with Draft Mode
+
+Every edit in the NetBird dashboard applies the moment you save it. That is fine for a single tweak, but a real change, like onboarding a new server, spans a peer, a group, and a policy, and a slip lands on production immediately.
+
+**Draft Mode** removes that risk. It turns the [Control Center](/manage/control-center) canvas into a local working copy: sketch the whole change, review the exact API requests, and apply everything as one batch.
+
+
+ **Availability**: Draft Mode is in **beta**, available on NetBird Cloud (all plans) and self-hosted.
+
+
+ **Permissions:** Deploying a draft performs the same operations as making those changes on their regular dashboard pages, so it requires the same permissions. Learn more about [user roles](/manage/team/user-roles).
+
+
+A draft moves through three stages: **build** on the canvas, **review** the change list, and **deploy** as one batch. Nothing touches your account until you click **Approve & Deploy**.
+
+One example carries through the whole page: giving `DevOps` HTTPS access to `staging-01`, a new staging server that is not even installed yet.
+
+## Enter Draft Mode
+
+In Control Center, switch the **Live | Draft** toggle in the top-right corner to **Draft**, then select either **New Empty Draft** or **From Current View**:
+
+- **From Current View**: start from a copy of the canvas you are looking at. Pick this when you are modifying things that already exist; the draft reproduces the live view exactly, network nodes included.
+- **New Empty Draft**: start from a blank canvas. Pick this when you are building something new from scratch, like our `staging-01` setup.
+
+
+
+
+
+
+ A draft lives only in your browser tab. **Reloading or closing the page discards it**; the browser warns you first, but there is no way to recover a discarded draft. Build and deploy a draft in one sitting.
+
+
+## Canvas Toolbar
+
+The toolbar at the bottom of the canvas holds the select and hand (pan) tools, zoom controls, and the actions below. Every edit you make in a draft is tracked as a change and applied only at deploy.
+
+### Select and Pan
+
+- **Select** (`V`) is the default tool: click a node to select it, drag it to move it, or drag across empty canvas to select several nodes at once.
+- **Hand** (`H`) pans the canvas as you drag, so you can move around a large draft without touching any nodes.
+
+### Add Nodes
+
+Click **Add** in the toolbar (or press `C`) to open the components panel. It covers both kinds of adding:
+
+- **Add new**: drag-and-drop templates for components that do not exist yet (Server, Agent, Policy, Group, Network, Resource). Server and Agent both create placeholder peers and are installed the same way, with a setup key; pick whichever matches the machine's role.
+- **Add existing**: searchable lists of the peers, groups, networks, and policies already in your account. Components already on the canvas are marked accordingly.
+
+You can also right-click the canvas to pick a component from the context menu, or press `Alt/⌥+1` through `Alt/⌥+6` to create a new Server, Agent, Policy, Group, Network, or Resource at the center of the view.
+
+
+
+
+
+For our example, drag a **Server** template onto the canvas. It arrives as a **placeholder peer**: a stand-in for a machine that has not registered yet (see [Install peers that don't exist yet](#install-peers-that-dont-exist-yet)).
+
+
+
+
+
+Then drag a **Group** onto the canvas for the access we are about to grant.
+
+
+
+
+
+**Network** and **Resource** templates work the same way and integrate with [Networks](/manage/networks); connecting a policy to a network node asks which of its resources the policy should target.
+
+### Undo and Redo
+
+`Ctrl/⌘+Z` undoes any canvas action, `Shift+Ctrl/⌘+Z` redoes it.
+
+### Fit to View
+
+**Fit to view** (`1`) zooms and centers the canvas so the whole draft is on screen.
+
+### Auto-Arrange
+
+**Auto-arrange** (`A`) reflows the nodes into a tidy layout, handy after a lot of dragging. Layout changes are visual only; they are never part of the deployed changes.
+
+## Interact with Nodes
+
+We carry the `staging-01` example through each action below.
+
+### Rename Nodes
+
+Right-click a node and choose **Rename**. We rename the server to `staging-01` and the group to `Staging`. Names must be unique, and the built-in `All` group cannot be renamed.
+
+
+
+
+
+### Assign Peers to Groups
+
+Drag the `staging-01` node onto the `Staging` group. The peer is absorbed into the group and the group's member count goes up. Membership travels with the group's change, so you never manage it separately.
+
+
+
+
+
+To group several peers at once, drag a selection across them, then click **Create Group** in the selection menu (or press `G`) and name the group:
+
+
+
+
+
+### Connect Nodes to Create Policies
+
+Drag the existing `DevOps` group from the components panel onto the canvas, then drag a connection from `DevOps` to `Staging`. The standard policy editor opens with both sides prefilled. Restrict it to what the team actually needs: TCP port `443` for the web app, not all traffic. The first policy you deploy should already be the least-privilege one.
+
+
+
+
+
+That is the whole change: a new server, its group, and one policy, and nothing has touched your network yet. The **Review & Deploy** counter now reflects each tracked change.
+
+A few more ways to build policies:
+
+- **The side you drag from decides the direction.** Dragging a connection from a node's right handle makes that node the policy's **source**; dragging from the left handle makes it the **destination**.
+- **Policies without the editor**: drop a blank **Policy** node and connect a source and a destination to it. The policy is tracked as soon as both sides are set, no dialog involved.
+- **Groups on the fly**: typing a new name into the policy editor's source or destination selector creates that group as part of the draft.
+
+### Install Peers That Don't Exist Yet
+
+`staging-01` does not exist yet, and Draft Mode does not need it to. A placeholder peer holds the machine's place on the canvas: you can group it and write policies against it, and when the real machine registers, it inherits everything you prepared.
+
+To turn the placeholder into a real peer, hover it and click **Install**:
+
+1. The **Install NetBird** dialog opens with instructions per operating system. Click **Generate a setup key** and run the shown command on the machine.
+
+
+
+
+
+2. The placeholder switches to **Waiting** while the machine registers.
+3. When it connects, the placeholder is replaced in place: the real `staging-01` takes over its name, its `Staging` membership, and the policy you drew. The temporary setup key is deleted automatically.
+
+
+ Installing a peer is a real-world step, not an API call: a draft cannot be deployed until every placeholder has registered or been removed.
+ Generating the setup key requires a role that can create setup keys; a [Network Admin](/manage/team/user-roles) cannot, so they need an Admin to run the install step or must remove the placeholder before deploying.
+
+
+### Remove and Delete Nodes
+
+**Remove** and **Delete** do different things:
+
+- **Remove** takes a component off the canvas and nothing more. It never asks for confirmation because it never touches your network; removing something also cancels any pending change it was part of. `Backspace` on a selected node does the same.
+- **Delete** is only offered for things that actually exist in your account. It always asks for confirmation, shows up as a red **Delete** entry in the review list, and the deletion happens at deploy.
+
+
+ Removing an existing group from the canvas does **not** delete it; it only leaves your draft. If your intent is "this group should be gone from my network", you want **Delete**.
+
+
+## Review and Deploy
+
+Click **Review & Deploy**. The review dialog lists every change in the exact order it will be applied, one row per operation:
+
+- Each row shows the affected component and the HTTP request that will be sent (like `POST /groups`). Clicking the request copies it as a `cURL` command; expanding the row shows the request body as a diff.
+- For our example you would see the `Staging` group creation, the `DevOps → Staging` policy creation, and the `staging-01` install step.
+
+If something is unfinished, the row carries an amber issue badge you can click to fix it in place: **Install** for a placeholder that has no machine yet, or **No Network** for a resource that is not assigned to any network. **Approve & Deploy** stays disabled until every blocking issue is resolved.
+
+
+
+
+
+To drop a single change without discarding the whole draft, open the row's menu and choose **Remove**; a confirmation lists exactly what that reverts on the canvas before you commit to it.
+
+When everything is green, click **Approve & Deploy**:
+
+- Changes apply in dependency order: groups first, then networks, resources, routing peers, and policies, with deletions last. References resolve as the batch runs, so the new policy points at the `Staging` group created moments earlier.
+- If a request fails, the deploy stops with the error. Changes that already applied stay applied and are marked with a check; fix the failing one and click **Approve & Deploy** again to resume from where it stopped.
+- On success you get a **Deploy complete** confirmation, the draft closes, and the live canvas rebuilds with your changes in place.
+
+## Leaving a Draft
+
+Click **Cancel** (or switch back to **Live**) to abandon a draft. With pending changes you are asked to confirm discarding them; navigating to any other dashboard page asks the same question first, and a page reload triggers the browser's own warning. Temporary artifacts a draft created for peer installation, like setup keys, are cleaned up automatically when the draft is discarded.
+
+
+
+
+
+## When Not to Use Draft Mode
+
+For a single, self-contained edit, like widening one policy's port range, Draft Mode is overhead: edit the policy directly from the live Control Center view (the dashboard confirms before applying live changes) or from its regular page. Drafts are also never stored, so Draft Mode is not a place to park a half-finished configuration and come back to it another day: it fits changes you can build and deploy in one sitting. Draft Mode earns its keep when a change spans several components, needs a machine that is not installed yet, or deserves a review before it goes live. For automated or repeated changes, use the [NetBird API](/api) instead.
+
+## Keyboard Shortcuts
+
+| Key | Action |
+|---|---|
+| `C` | Open or close the components panel |
+| `V` / `H` | Select tool / Hand (pan) tool |
+| `Alt/⌥+1` … `Alt/⌥+6` | New Server / Agent / Policy / Group / Network / Resource |
+| `G` | Create a group from the selected nodes |
+| `Ctrl/⌘+Z`, `Shift+Ctrl/⌘+Z` | Undo / Redo |
+| `1` / `A` | Fit to view / Auto-arrange the layout |
+| `Backspace` | Remove the selected node from the canvas |
+| `F` | Focus a node and dim everything unrelated |
+
+## Recap
+
+- A draft is a working copy of your network: build on the canvas, review the change list, deploy as one batch. Nothing is written until you click **Approve & Deploy**.
+- Our example never left that path: we placed `staging-01`, grouped it into `Staging`, and drew a least-privilege HTTPS policy from `DevOps`, all before the server even existed.
+- Installing a placeholder slots the real machine into everything you prepared for it.
+- Remove edits the canvas; Delete edits your network.
+- The review shows the literal API requests in deploy order, and a failed deploy resumes where it stopped.
+
+## Related Docs
+
+- [Control Center overview](/manage/control-center)
+- [Manage network access with Groups and Access Policies](/manage/access-control/manage-network-access)
+- [Networks and routing peers](/manage/networks)
+- [Register machines using setup keys](/manage/peers/register-machines-using-setup-keys)
diff --git a/src/pages/manage/control-center/index.mdx b/src/pages/manage/control-center/index.mdx
index 865d43fd5..75825f2de 100644
--- a/src/pages/manage/control-center/index.mdx
+++ b/src/pages/manage/control-center/index.mdx
@@ -1,21 +1,29 @@
import {Note} from "@/components/mdx"
+import {HashRedirect} from "@/components/HashRedirect"
export const description =
- 'Control Center provides a topological view of your NetBird network, visualizing access relationships between peers, groups, and networks through an interactive graph interface.'
+ 'Control Center provides a topological view of your NetBird network, visualizing access relationships between peers, users, groups, and networks. Edit policies in place, or stage larger changes with Draft Mode.'
+
+
# Visualize Remote Access with Control Center
-Control Center is a topological view in the NetBird dashboard that visualizes who can access what across your environment. It maps **Peers**, **Groups**, and **Networks** to the resources they can reach and shows the access control policies that permit those connections.
+Control Center is a topological view in the NetBird dashboard that visualizes who can access what across your environment. It maps **Peers**, **Users**, **Groups**, and **Networks** to the resources they can reach and shows the access control policies that permit those connections.
**Availability**: NetBird Cloud (all plans) and self-hosted.
- **Permissions:** The Admin and Network Admin user roles can edit policies from Control Center. Learn more about
- [user roles](/manage/team/user-roles).
+ **Permissions:** The Admin and Network Admin user roles can make changes from Control Center, in the live view and
+ in Draft Mode. The exception is installing placeholder peers in a draft, which creates a setup key and is therefore
+ not available to Network Admins. Learn more about [user roles](/manage/team/user-roles).
-## How it helps
+## How It Helps
- **Faster audits:** Confirm a device, group, or network's effective access at a glance.
- **Quicker troubleshooting:** Follow the policy path that grants access to a resource.
@@ -23,7 +31,7 @@ Control Center is a topological view in the NetBird dashboard that visualizes wh
## Views
-### Peers view
+### Peers View
Use this to understand what a specific machine can reach.
@@ -36,7 +44,7 @@ Use this to understand what a specific machine can reach.
- The graph shows the peer's access control policy nodes and the reachable resources.
- Click a policy chip to open the standard policy editor. Changes you save are reflected in the graph immediately.
-### Users view
+### Users View
Use this view to see what resources a specific user can access.
@@ -50,7 +58,7 @@ Use this view to see what resources a specific user can access.
- Click a peer to switch focus only to that specific peer.
- Click a policy chip to open the standard policy editor. Changes you save are reflected in the graph immediately.
-### Groups view
+### Groups View
Use this to validate team-level access.
@@ -67,7 +75,7 @@ Common checks:
- Confirm that "DevOps" can reach RDS on TCP 5432, or that "Support" only reaches SSH on TCP 22.
-### Networks view
+### Networks View
Use this to see who can access resources in your routed [networks](/manage/networks).
@@ -80,27 +88,46 @@ Use this to see who can access resources in your routed [networks](/manage/netwo
- The network node shows its resources. Connecting lines display the port allowed by the policy and which groups have access.
- Click any policy chip to edit it in the standard editor. NetBird Networks and routing peers enable access to private subnets and IP resources.
-## Editing policies from the graph
+## Focus Mode
+
+When the graph gets busy, focus a single node to see only what is connected to it. Right-click a node and choose **Focus**, or press `F` and then select the node.
+
+
+
+
+
+Everything unrelated dims, leaving only the focused node and its connections highlighted. A "Focusing on" pill at the top of the canvas shows what is in focus; close it to return to the full view.
+
+
+
+
+
+## Edit Nodes
+
+Control Center is not just a viewer. In the live view you can make changes in place, like editing a policy or changing a peer's group assignments:
+
+- **Edit a policy:** Click an access control policy chip in any view to open the standard policy editor. You can change the usual policy fields as documented in [Access Control](/manage/access-control/manage-network-access), including sources, destinations, protocols, ports, and posture checks. The dashboard asks you to confirm a live change; once confirmed, it applies immediately.
+- **Create new components:** Creating policies, groups, and other components happens in **Draft Mode** or on their regular dashboard pages, not in the live view.
-- **Open editor:** Click an access control policy chip in any view to open the standard policy editor.
-- **What you can change:** Use the editor to modify the usual policy fields as documented in [Access Control](/manage/access-control/manage-network-access), including sources, destinations, protocols, ports, and posture checks.
-- **Create vs edit:** You can edit existing policies from Control Center. Creating a new policy still happens in the Access Control section.
+For changes that span several components, **Draft Mode** lets you stage the whole set on a working copy of the canvas, review the exact API requests, and deploy everything as one batch. See [Plan and Deploy Changes with Draft Mode](/manage/control-center/draft-mode).
-## Quick start
+## Quick Start
1. Open **Control Center** in the NetBird dashboard.
-2. Pick a tab: **Peers**, **Groups**, or **Networks**.
+2. Pick a tab: **Peers**, **Users**, **Groups**, or **Networks**.
3. Click a node to focus, then follow the policy chips to the target resource.
4. Click a policy chip to edit it, then save. The graph updates right away.
-## Use cases
+## Use Cases
- **Sanity-check a team:** In **Groups** view, select a group and verify the resources and ports granted by its policies match your intent. Adjust policies in place if needed.
- **Prepare a change:** In **Networks** view, review which groups reach a sensitive subnet before tightening ports or destinations.
- **Investigate access:** In **Peers** view, confirm why a host can reach a database by following the policy path and port labels, then narrow the rule if required.
- **MSPs:** Switch tenants in the MSP portal to repeat the same checks per customer environment.
-## Related docs
+## Related Docs
- [Manage network access with Groups and Access Policies](/manage/access-control/manage-network-access)
- [Apply posture checks to policies](/manage/access-control/posture-checks)
diff --git a/src/styles/tailwind.css b/src/styles/tailwind.css
index 0fd509631..8d9e330af 100644
--- a/src/styles/tailwind.css
+++ b/src/styles/tailwind.css
@@ -106,6 +106,11 @@
cursor: zoom-in;
}
+/* ImageZoom only attaches to images, so videos get no zoom affordance. */
+video.imagewrapper-big {
+ cursor: default;
+}
+
.image-zoom-overlay {
position: fixed;
inset: 0;
@@ -119,7 +124,10 @@
}
.image-zoom-overlay.closing {
- animation: fadeOut 0.2s ease-out;
+ /* forwards holds opacity 0 after the animation ends, covering the gap
+ until React's unmount timeout fires (otherwise the overlay snaps back
+ to full opacity for a frame). */
+ animation: fadeOut 0.2s ease-out forwards;
}
.image-zoom-content {
@@ -133,7 +141,7 @@
}
.image-zoom-content.closing {
- animation: zoomOut 0.2s ease-out;
+ animation: zoomOut 0.2s ease-out forwards;
}
.image-zoom-close {