diff --git a/.github/workflows/render_nanomaps.yml b/.github/workflows/render_nanomaps.yml
new file mode 100644
index 0000000000000..532cfa7f169c7
--- /dev/null
+++ b/.github/workflows/render_nanomaps.yml
@@ -0,0 +1,46 @@
+# GitHub action to autorender nanomaps outside the game
+# This kills off the awful verb we have that takes a full 50 seconds and hangs the whole server
+# The file names and locations are VERY important here
+# DO NOT EDIT THIS UNLESS YOU KNOW WHAT YOU ARE DOING
+# -aa
+name: 'Render Nanomaps'
+on:
+ schedule:
+ - cron: "0 0 * * *"
+ workflow_dispatch:
+
+jobs:
+ generate_maps:
+ name: 'Generate NanoMaps'
+ runs-on: ubuntu-22.04
+ steps:
+ - id: create_token
+ uses: actions/create-github-app-token@v1
+ with:
+ app-id: ${{ secrets.APP_ID }}
+ private-key: ${{ secrets.PRIVATE_KEY }}
+
+ - run: echo "GH_TOKEN=${{ steps.create_token.outputs.token }}" >> "$GITHUB_ENV"
+
+ - name: 'Update Branch'
+ uses: actions/checkout@v4
+ with:
+ token: ${{ steps.create_token.outputs.token }}
+
+ - name: Branch
+ run: |
+ git branch -f nanomap-render
+ git checkout nanomap-render
+ git reset --hard origin/master
+
+ - name: 'Generate Maps'
+ run: './tools/nanomap_renderer/nanomap-renderer-invoker.sh'
+
+ - name: 'Commit Maps and open PR'
+ run: |
+ git config --local user.email "action@github.com"
+ git config --local user.name "NanoMap Generation"
+ git pull origin master
+ git commit -m "NanoMap Auto-Update (`date`)" -a || true
+ git push -f -u origin nanomap-render
+ gh pr create -t "Automatic NanoMap Update" -b "This pull request updates the server NanoMaps. Please review the diff images before merging." -l "NanoMaps" -H "nanomap-render" -B "master"
diff --git a/tgui/packages/tgui/components/NanoMap.js b/tgui/packages/tgui/components/NanoMap.js
new file mode 100644
index 0000000000000..f06141047238b
--- /dev/null
+++ b/tgui/packages/tgui/components/NanoMap.js
@@ -0,0 +1,224 @@
+// BANDASTATION ADDITION
+
+import { resolveAsset } from '../assets';
+import { useBackend } from '../backend';
+import { Box, Button, Icon, Tooltip } from '.';
+import { LabeledList } from './LabeledList';
+import { Slider } from './Slider';
+
+const pauseEvent = (e) => {
+ if (e.stopPropagation) {
+ e.stopPropagation();
+ }
+ if (e.preventDefault) {
+ e.preventDefault();
+ }
+ e.cancelBubble = true;
+ e.returnValue = false;
+ return false;
+};
+
+export class NanoMap extends Component {
+ constructor(props) {
+ super(props);
+
+ // Auto center based on window size
+ const Xcenter = window.innerWidth / 2 - 256;
+ const Ycenter = window.innerHeight / 2 - 256;
+
+ this.state = {
+ offsetX: 128,
+ offsetY: 48,
+ transform: 'none',
+ dragging: false,
+ originX: null,
+ originY: null,
+ zoom: 1,
+ };
+
+ // Dragging
+ this.handleDragStart = (e) => {
+ this.ref = e.target;
+ this.setState({
+ dragging: false,
+ originX: e.screenX,
+ originY: e.screenY,
+ });
+ document.addEventListener('mousemove', this.handleDragMove);
+ document.addEventListener('mouseup', this.handleDragEnd);
+ pauseEvent(e);
+ };
+
+ this.handleDragMove = (e) => {
+ this.setState((prevState) => {
+ const state = { ...prevState };
+ const newOffsetX = e.screenX - state.originX;
+ const newOffsetY = e.screenY - state.originY;
+ if (prevState.dragging) {
+ state.offsetX += newOffsetX;
+ state.offsetY += newOffsetY;
+ state.originX = e.screenX;
+ state.originY = e.screenY;
+ } else {
+ state.dragging = true;
+ }
+ return state;
+ });
+ pauseEvent(e);
+ };
+
+ this.handleDragEnd = (e) => {
+ this.setState({
+ dragging: false,
+ originX: null,
+ originY: null,
+ });
+ document.removeEventListener('mousemove', this.handleDragMove);
+ document.removeEventListener('mouseup', this.handleDragEnd);
+ pauseEvent(e);
+ };
+
+ this.handleZoom = (_e, value) => {
+ this.setState((state) => {
+ const newZoom = Math.min(Math.max(value, 1), 8);
+ let zoomDiff = (newZoom - state.zoom) * 1.5;
+ state.zoom = newZoom;
+ state.offsetX = state.offsetX - 262 * zoomDiff;
+ state.offsetY = state.offsetY - 256 * zoomDiff;
+ if (props.onZoom) {
+ props.onZoom(state.zoom);
+ }
+ return state;
+ });
+ };
+ }
+
+ render() {
+ const { config } = useBackend(this.context);
+ const { dragging, offsetX, offsetY, zoom = 1 } = this.state;
+ const { children } = this.props;
+
+ const mapUrl = config.map + '_nanomap_z1.png';
+ const mapSize = 510 * zoom + 'px';
+ const newStyle = {
+ width: mapSize,
+ height: mapSize,
+ 'margin-top': offsetY + 'px',
+ 'margin-left': offsetX + 'px',
+ overflow: 'hidden',
+ position: 'relative',
+ 'background-size': 'cover',
+ 'background-repeat': 'no-repeat',
+ 'text-align': 'center',
+ cursor: dragging ? 'move' : 'auto',
+ };
+ const mapStyle = {
+ width: '100%',
+ height: '100%',
+ position: 'absolute',
+ top: '50%',
+ left: '50%',
+ transform: 'translate(-50%, -50%)',
+ '-ms-interpolation-mode': 'nearest-neighbor',
+ };
+
+ return (
+