{"id":51471,"date":"2026-09-20T18:16:01","date_gmt":"2026-09-20T17:16:01","guid":{"rendered":"https:\/\/cctvacademy.be\/?page_id=51471"},"modified":"2026-09-20T18:46:32","modified_gmt":"2026-09-20T17:46:32","slug":"cctv-night-watch3","status":"publish","type":"page","link":"https:\/\/cctvacademy.be\/nl\/cctv-night-watch3\/","title":{"rendered":"CCTV Night Watch3"},"content":{"rendered":"\t\t<div data-elementor-type=\"masteriyo-course-archive-page\" data-elementor-id=\"51471\" class=\"elementor elementor-51471\">\n\t\t\t\t<div class=\"elementor-element elementor-element-61ff1eb e-flex e-con-boxed e-con e-parent\" data-id=\"61ff1eb\" data-element_type=\"container\" data-e-type=\"container\">\n\t\t\t\t\t<div class=\"e-con-inner\">\n\t\t\t\t<div class=\"elementor-element elementor-element-051539b elementor-widget elementor-widget-spacer\" data-id=\"051539b\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"spacer.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t\t\t<div class=\"elementor-spacer\">\n\t\t\t<div class=\"elementor-spacer-inner\"><\/div>\n\t\t<\/div>\n\t\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t<div class=\"elementor-element elementor-element-b5436fd e-flex e-con-boxed e-con e-parent\" data-id=\"b5436fd\" data-element_type=\"container\" data-e-type=\"container\">\n\t\t\t\t\t<div class=\"e-con-inner\">\n\t\t\t\t<div class=\"elementor-element elementor-element-9b8fe6c elementor-align-center elementor-widget elementor-widget-button\" data-id=\"9b8fe6c\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"button.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t\t\t\t\t<div class=\"elementor-button-wrapper\">\n\t\t\t\t\t<a class=\"elementor-button elementor-button-link elementor-size-sm\" href=\"https:\/\/cctvacademy.be\">\n\t\t\t\t\t\t<span class=\"elementor-button-content-wrapper\">\n\t\t\t\t\t\t\t\t\t<span class=\"elementor-button-text\">Home<\/span>\n\t\t\t\t\t<\/span>\n\t\t\t\t\t<\/a>\n\t\t\t\t<\/div>\n\t\t\t\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t<div class=\"elementor-element elementor-element-8969003 e-flex e-con-boxed e-con e-parent\" data-id=\"8969003\" data-element_type=\"container\" data-e-type=\"container\">\n\t\t\t\t\t<div class=\"e-con-inner\">\n\t\t\t\t<div class=\"elementor-element elementor-element-f49f52d elementor-widget elementor-widget-spacer\" data-id=\"f49f52d\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"spacer.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t\t\t<div class=\"elementor-spacer\">\n\t\t\t<div class=\"elementor-spacer-inner\"><\/div>\n\t\t<\/div>\n\t\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t<div class=\"elementor-element elementor-element-3df00bd e-flex e-con-boxed e-con e-parent\" data-id=\"3df00bd\" data-element_type=\"container\" data-e-type=\"container\">\n\t\t\t\t\t<div class=\"e-con-inner\">\n\t\t\t\t<div class=\"elementor-element elementor-element-1c88fcd elementor-widget elementor-widget-video\" data-id=\"1c88fcd\" data-element_type=\"widget\" data-e-type=\"widget\" data-settings=\"{&quot;video_type&quot;:&quot;hosted&quot;,&quot;play_on_mobile&quot;:&quot;yes&quot;,&quot;autoplay&quot;:&quot;yes&quot;,&quot;controls&quot;:&quot;yes&quot;}\" data-widget_type=\"video.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t\t\t<div class=\"e-hosted-video elementor-wrapper elementor-open-inline\">\n\t\t\t\t\t<video class=\"elementor-video\" src=\"https:\/\/cctvacademy.be\/wp-content\/uploads\/2026\/09\/cctv-pacmaze-intro-movie-2-cctv-academy.mp4\" autoplay=\"\" controls=\"\" playsinline=\"\" controlsList=\"nodownload\"><\/video>\n\t\t\t\t<\/div>\n\t\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t<div class=\"elementor-element elementor-element-8b0f16f e-flex e-con-boxed e-con e-parent\" data-id=\"8b0f16f\" data-element_type=\"container\" data-e-type=\"container\">\n\t\t\t\t\t<div class=\"e-con-inner\">\n\t\t\t\t<div class=\"elementor-element elementor-element-b21b8b9 elementor-widget elementor-widget-spacer\" data-id=\"b21b8b9\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"spacer.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t\t\t<div class=\"elementor-spacer\">\n\t\t\t<div class=\"elementor-spacer-inner\"><\/div>\n\t\t<\/div>\n\t\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t<div class=\"elementor-element elementor-element-e4c2c16 e-flex e-con-boxed e-con e-parent\" data-id=\"e4c2c16\" data-element_type=\"container\" data-e-type=\"container\">\n\t\t\t\t\t<div class=\"e-con-inner\">\n\t\t\t\t<div class=\"elementor-element elementor-element-c2a60cb elementor-widget elementor-widget-html\" data-id=\"c2a60cb\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"html.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t<!DOCTYPE html>\n<html lang=\"nl\">\n<head>\n<meta charset=\"UTF-8\">\n<meta name=\"viewport\" content=\"width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no\">\n<title>CCTV PAC-MAZE v5.2 \u2014 Night Watch + Directed WebGL CCTV<\/title>\n<style>\n:root{\n  --bg:#020609;--panel:#07131a;--panel2:#0a1d27;--line:#20d8ff;--green:#8dff75;\n  --warning:#ffd166;--danger:#ff4d6d;--text:#eefcff;--muted:#7da4b3;\n}\n*{box-sizing:border-box}\nhtml,body{margin:0;min-height:100%;background:#020609;color:var(--text);font-family:Inter,ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}\nbody{padding:16px;background:radial-gradient(circle at 20% 0,rgba(32,216,255,.08),transparent 28%),radial-gradient(circle at 90% 90%,rgba(141,255,117,.05),transparent 30%),#020609}\n#cctvGame{max-width:1240px;margin:auto;outline:none}\n.brand{display:flex;align-items:flex-end;justify-content:space-between;gap:16px;margin-bottom:12px}\n.brand h1{margin:0;font-size:clamp(26px,4.4vw,48px);line-height:.95;letter-spacing:.07em}\n.brand .cctv{color:#fff;text-shadow:0 0 8px rgba(255,255,255,.75),0 0 22px rgba(255,255,255,.25)}\n.brand .pac{color:var(--green);text-shadow:0 0 18px rgba(141,255,117,.28)}\n.brand .version{display:inline-block;margin-left:10px;padding:4px 7px;border:1px solid rgba(32,216,255,.28);border-radius:7px;color:var(--line);font-size:clamp(9px,1.2vw,12px);line-height:1;letter-spacing:.08em;vertical-align:middle;text-shadow:none;background:rgba(4,16,24,.8)}\n.brandRight{display:flex;align-items:center;gap:12px}.brandMeta{font-size:11px;color:var(--muted);text-align:right;line-height:1.5}.langButtons{display:flex;gap:5px;flex-wrap:wrap;justify-content:flex-end}.langBtn{min-width:44px;min-height:36px;padding:6px 9px;border-radius:8px;border:1px solid rgba(32,216,255,.28);background:#061821;color:#cceef7;cursor:pointer;font-weight:900;font-size:10px;letter-spacing:.06em}.langBtn.active{background:linear-gradient(135deg,var(--line),var(--green));color:#001015;border-color:transparent}.langBtn:focus-visible{outline:2px solid #fff;outline-offset:2px}\n.layout{display:grid;grid-template-columns:minmax(0,1fr) 310px;gap:14px;align-items:start}\n.gamePanel,.card{background:linear-gradient(180deg,rgba(9,27,37,.96),rgba(3,12,17,.98));border:1px solid rgba(32,216,255,.24);border-radius:16px}\n.gamePanel{padding:10px;overflow:hidden}\n.hud{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:7px;margin-bottom:9px}\n.hudBox{min-width:0;padding:7px 8px;border:1px solid rgba(125,164,179,.17);border-radius:9px;background:#041018}\n.hudLabel{font-size:9px;text-transform:uppercase;letter-spacing:.13em;color:var(--muted);white-space:nowrap}\n.hudValue{font-weight:900;font-size:14px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-top:2px}\n.meter{height:4px;background:#102934;border-radius:99px;overflow:hidden;margin-top:5px}\n.meter>i{display:block;height:100%;width:100%;background:linear-gradient(90deg,var(--line),var(--green));transition:width .08s linear}\n.goalStrip{display:flex;align-items:center;justify-content:space-between;gap:10px;margin:0 0 7px;padding:9px 11px;border:1px solid rgba(255,209,102,.32);border-radius:10px;background:rgba(40,27,3,.46);color:#fff3c8;font-size:11px;font-weight:900;letter-spacing:.05em;animation:goalStripPulse 2.2s ease-in-out infinite}.goalStrip strong{color:var(--warning);font-size:13px}.goalCount{white-space:nowrap;color:#fff;font-size:14px}@keyframes goalStripPulse{0%,100%{box-shadow:0 0 0 rgba(255,209,102,0);border-color:rgba(255,209,102,.30)}50%{box-shadow:0 0 14px rgba(255,209,102,.20);border-color:rgba(255,209,102,.68)}}@media(prefers-reduced-motion:reduce){.goalStrip{animation:none}}.gameActionBar{display:flex;align-items:stretch;justify-content:space-between;gap:8px;margin:0 0 9px;flex-wrap:wrap}.levelHero{display:flex;align-items:center;justify-content:center;gap:8px;min-width:150px;padding:8px 13px;border:1px solid rgba(255,209,102,.52);border-radius:10px;background:linear-gradient(135deg,rgba(58,39,4,.92),rgba(20,15,4,.92));color:#fff3c8;font-size:12px;font-weight:950;letter-spacing:.12em;text-transform:uppercase}.levelHero strong{font-size:24px;line-height:1;color:var(--warning);text-shadow:0 0 12px rgba(255,209,102,.36)}.gameStage{position:relative;overflow:hidden;border:1px solid rgba(32,216,255,.32);border-radius:12px;background:#000;aspect-ratio:10\/7;touch-action:none}\n.gameStage:after{content:\"\";position:absolute;inset:0;z-index:4;pointer-events:none;background:repeating-linear-gradient(0deg,rgba(255,255,255,.017) 0,rgba(255,255,255,.017) 1px,transparent 1px,transparent 4px);mix-blend-mode:screen}\ncanvas{display:block;width:100%;height:100%;outline:none;background:#000}\n.viewPanBtn{position:absolute;z-index:9;display:grid;place-items:center;width:42px;height:42px;border:1px solid rgba(223,248,255,.28);border-radius:12px;background:rgba(2,11,16,.68);color:#dff8ff;font-size:20px;font-weight:950;cursor:pointer;backdrop-filter:blur(3px);opacity:.78;touch-action:manipulation;user-select:none}\n.viewPanBtn:active{transform:scale(.94);opacity:1}.viewPanBtn:focus-visible{outline:2px solid var(--line);outline-offset:2px}\n.viewPanUp{left:50%;top:7px;transform:translateX(-50%)}.viewPanUp:active{transform:translateX(-50%) scale(.94)}\n.viewPanDown{left:50%;bottom:7px;transform:translateX(-50%)}.viewPanDown:active{transform:translateX(-50%) scale(.94)}\n.viewPanLeft{left:7px;top:50%;transform:translateY(-50%)}.viewPanLeft:active{transform:translateY(-50%) scale(.94)}\n.viewPanRight{right:7px;top:50%;transform:translateY(-50%)}.viewPanRight:active{transform:translateY(-50%) scale(.94)}\n.volumeControl{position:static;z-index:auto;display:flex;align-items:center;justify-content:center;gap:4px;min-height:48px;padding:4px 6px;margin-left:auto;border:1px solid rgba(125,164,179,.24);border-radius:10px;background:#06151d;backdrop-filter:none}.volumeBtn,.volumeStep{min-width:38px;height:36px;padding:0 8px;border:1px solid rgba(255,255,255,.16);border-radius:7px;background:#071820;color:#fff;cursor:pointer;font-weight:900;font-size:15px}.volumeStep{font-size:19px}.volumeLevel{min-width:42px;text-align:center;color:#fff;font-size:10px;font-weight:900}.volumeBtn:focus-visible,.volumeStep:focus-visible{outline:2px solid var(--line);outline-offset:2px}.playArea{width:100%;min-width:0}\n.capturePopupRow{display:none!important}\n.capturePopup{position:absolute;z-index:14;display:none;left:8px;top:58px;max-width:min(260px,58%);padding:7px 10px;border:1px solid rgba(141,255,117,.45);border-radius:9px;background:rgba(3,20,15,.90);color:#efffed;text-align:left;pointer-events:none;box-shadow:0 0 16px rgba(141,255,117,.08);backdrop-filter:blur(4px)}\n.capturePopup.show{display:block}.capturePopupTitle{font-size:8px;letter-spacing:.10em;text-transform:uppercase;color:#bfffb0;font-weight:950}.capturePopupCount{margin-top:2px;font-size:18px;line-height:1;font-weight:1000;color:#fff}.stageLevelBadge{position:absolute;z-index:8;left:8px;top:8px;min-width:94px;padding:5px 9px;border:1px solid rgba(255,209,102,.38);border-radius:9px;background:rgba(26,18,3,.82);backdrop-filter:blur(4px);color:#fff0b7;font-size:9px;font-weight:950;letter-spacing:.11em;text-align:center;text-transform:uppercase}.stageLevelBadge strong{display:block;margin-top:1px;color:#ffd166;font-size:18px;line-height:1}.stageCameraBadge{position:absolute;z-index:8;right:8px;top:58px;min-width:92px;padding:5px 9px;border:1px solid rgba(32,216,255,.38);border-radius:9px;background:rgba(2,17,24,.84);backdrop-filter:blur(4px);color:#dffaff;font-size:8px;font-weight:950;letter-spacing:.10em;text-align:center;text-transform:uppercase;cursor:pointer;user-select:none;touch-action:manipulation;transition:transform .1s ease,filter .1s ease,border-color .1s ease;animation:cameraBadgePulse 2.2s ease-in-out infinite}.stageCameraBadge:hover{filter:brightness(1.12);border-color:rgba(32,216,255,.7)}.stageCameraBadge:active{transform:scale(.96)}.stageCameraBadge:focus-visible{outline:2px solid #20d8ff;outline-offset:2px}.stageCameraBadge strong{display:block;margin-top:1px;color:#8dff75;font-size:18px;line-height:1}@keyframes cameraBadgePulse{0%,100%{box-shadow:0 0 0 rgba(32,216,255,0);border-color:rgba(32,216,255,.34)}50%{box-shadow:0 0 14px rgba(32,216,255,.24);border-color:rgba(32,216,255,.72)}}@media(prefers-reduced-motion:reduce){.stageCameraBadge{animation:none}}.diamondGuardBadge{display:none;position:absolute;z-index:18;left:50%;top:8px;transform:translateX(-50%);min-width:142px;padding:6px 11px;border:1px solid rgba(125,232,255,.78);border-radius:9px;background:rgba(0,24,34,.96);backdrop-filter:blur(5px);color:#e9fcff;font-size:8px;font-weight:950;letter-spacing:.09em;text-align:center;text-transform:uppercase;pointer-events:none;box-shadow:0 0 18px rgba(32,216,255,.22)}.diamondGuardBadge.show{display:block}.diamondGuardBadge strong{display:block;margin-top:1px;color:#7de8ff;font-size:22px;line-height:1;text-shadow:0 0 14px rgba(125,232,255,.52)}\n.diamondIntruderBadge{display:none;position:absolute;z-index:10;left:50%;top:50px;transform:translateX(-50%);min-width:148px;padding:5px 10px;border:1px solid rgba(255,69,93,.68);border-radius:9px;background:rgba(35,3,8,.92);backdrop-filter:blur(4px);color:#ffdbe1;font-size:8px;font-weight:950;letter-spacing:.09em;text-align:center;text-transform:uppercase;pointer-events:none;box-shadow:0 0 18px rgba(255,45,75,.16)}\n.diamondIntruderBadge.show{display:block;animation:diamondThreatPulse .72s steps(2,end) infinite}\n.diamondIntruderBadge strong{display:block;margin-top:1px;color:#ff5a70;font-size:20px;line-height:1;text-shadow:0 0 14px rgba(255,45,75,.52)}\n@keyframes diamondThreatPulse{0%,49%{opacity:1}50%,100%{opacity:.56}}\n\n.overlay{position:absolute;inset:0;z-index:10;display:grid;place-items:center;padding:22px;overflow:auto;background:linear-gradient(rgba(0,7,10,.86),rgba(0,9,13,.97));transition:opacity .2s ease}\n.overlay.hidden{opacity:0;pointer-events:none}.overlay.celebrate{background:linear-gradient(rgba(0,7,10,.20),rgba(0,9,13,.58));backdrop-filter:blur(1px)}\n.intro{width:min(510px,100%);text-align:center}\n.eyebrow{font-size:10px;letter-spacing:.18em;color:var(--line);text-transform:uppercase}\n.intro h2{font-size:clamp(31px,7vw,58px);line-height:.93;margin:8px 0 10px}.intro h2 .white{color:#fff}.intro h2 b{color:var(--green)}\n.intro p{max-width:470px;margin:0 auto 14px;color:#b1d1db;font-size:13px;line-height:1.55}\n.tips{display:grid;grid-template-columns:1fr 1fr;gap:7px;text-align:left;margin:12px 0}\n.tip{padding:9px;border-radius:9px;background:rgba(7,25,34,.8);border:1px solid rgba(125,164,179,.15);font-size:11px;line-height:1.35;color:#b7d4dc}.tip b{color:#fff}\n.startRow{display:flex;gap:8px;margin-top:10px}\ninput,button{font:inherit}input{min-width:0;flex:1;padding:12px;border-radius:10px;border:1px solid rgba(32,216,255,.34);background:#020b10;color:#fff;outline:none;text-transform:uppercase;font-size:16px}input:focus{border-color:var(--line);box-shadow:0 0 0 3px rgba(32,216,255,.08)}\n.btn{min-height:46px;border:0;border-radius:10px;padding:11px 15px;cursor:pointer;font-weight:950;letter-spacing:.06em;color:#001015;background:linear-gradient(135deg,var(--line),var(--green))}.btn:active{transform:scale(.98)}.btn.secondary{margin-top:8px;color:var(--text);background:#0a202a;border:1px solid rgba(125,164,179,.25)}\n.startNote{font-size:9px;letter-spacing:.12em;color:var(--muted);margin-top:10px}\n.side{display:flex;flex-direction:column;gap:12px}.card{padding:14px}.card h3{font-size:12px;color:var(--line);letter-spacing:.12em;text-transform:uppercase;margin:0 0 10px}\n\n.protocolCard{padding:10px 12px}.protocolCard details{margin:0}.protocolCard summary{display:flex;align-items:center;justify-content:space-between;gap:8px;cursor:pointer;list-style:none;color:var(--line);font-size:11px;font-weight:900;letter-spacing:.11em;text-transform:uppercase}.protocolCard summary::-webkit-details-marker{display:none}.protocolCard summary:after{content:'+';display:grid;place-items:center;width:22px;height:22px;border:1px solid rgba(32,216,255,.25);border-radius:6px;color:#fff;font-size:15px}.protocolCard details[open] summary:after{content:'\u2212'}.protocolBody{padding-top:10px}.legend{display:grid;gap:7px;font-size:10px;line-height:1.35;color:#b3d2da}.legendRow{display:grid;grid-template-columns:24px 1fr;gap:6px;align-items:start}.key{display:grid;place-items:center;min-width:22px;height:22px;background:#071820;border:1px solid rgba(125,164,179,.25);border-bottom-width:3px;border-radius:5px;color:#fff;font-size:9px}.status{margin-top:9px;padding-top:8px;border-top:1px solid rgba(125,164,179,.12);font-size:10px;color:var(--muted);line-height:1.4}\n.touchControls{display:none!important}.dpad{display:none!important}.touchBtn{min-width:44px;min-height:44px;border-radius:11px;border:1px solid rgba(32,216,255,.28);background:#0a202b;color:#e7fbff;font-weight:900;font-size:20px}.flashWrap{display:none!important}.flashBtn{width:108px;height:108px;border-radius:50%;font-size:12px;letter-spacing:.08em;color:#1d1400;background:radial-gradient(circle at 35% 30%,#fff,#ffe78a 22%,#ffb13b 65%,#ff5f3d);border:4px solid rgba(255,255,255,.12)}.sentryBtn{min-width:108px;padding:10px 12px;font-size:11px;letter-spacing:.09em;color:#dffff2;background:#092419;border-color:rgba(141,255,117,.42)}.sentryBtn.active{color:#00150a;background:linear-gradient(135deg,#8dff75,#20d8ff);box-shadow:0 0 18px rgba(141,255,117,.28)}\n.footer{text-align:center;color:#42636d;font-size:9px;letter-spacing:.11em;margin-top:9px}\n@media (hover:hover) and (pointer:fine){.btn:hover{filter:brightness(1.08);transform:translateY(-1px)}.touchBtn:hover{background:#10303f}}\n@media(max-width:900px){\n  body{padding:10px}.layout{grid-template-columns:1fr}.side{display:grid;grid-template-columns:minmax(0,1.2fr) minmax(0,.8fr);gap:10px}.touchControls{display:none!important}.brandMeta{display:none}.topScoresCard{order:1}.protocolCard{order:2}.dpad{display:none!important}.touchBtn{min-width:50px;min-height:50px;touch-action:manipulation}.flashBtn{width:min(118px,29vw);height:min(118px,29vw)}\n}\n@media(max-width:620px){\n  body{padding:5px}.brand{margin-bottom:7px;align-items:center}.brand h1{font-size:clamp(21px,7vw,34px);letter-spacing:.035em}.brand .version{margin-left:5px;padding:3px 5px}.gamePanel{padding:5px;border-radius:11px}.hud{grid-template-columns:repeat(3,minmax(0,1fr));gap:4px;margin-bottom:5px}.hudBox{padding:5px 6px;border-radius:7px}.hudLabel{font-size:8px}.hudValue{font-size:12px}.goalStrip{padding:7px 8px;margin-bottom:5px;align-items:flex-start;font-size:9px}.goalStrip strong{font-size:10px}.goalCount{font-size:12px}.gameStage{border-radius:8px}.volumeControl{margin-left:0}.volumeBtn,.volumeStep{min-width:34px;height:34px;padding:0 6px}.volumeLevel{min-width:36px;font-size:9px}.overlay{padding:10px;align-items:start}.intro{padding:4px 0}.intro h2{font-size:clamp(25px,9vw,40px);margin:5px 0 7px}.intro p{font-size:11px;line-height:1.35;margin-bottom:8px}.tips{display:none!important}.startRow{display:block;margin-top:8px}.startRow .btn{width:100%}.touchControls{display:none!important}.dpad{display:none!important}.touchBtn{min-width:44px;min-height:44px;border-radius:9px}.flashWrap{display:none!important}.flashBtn{width:min(100px,31vw);height:min(100px,31vw);font-size:10px}.sentryBtn{min-width:96px;padding:8px 9px;font-size:9px}.side{grid-template-columns:1fr;gap:7px;margin-top:7px}.topScoresCard{padding:9px}.protocolCard{padding:8px 10px}.fullScores{margin-top:7px;padding:9px}.fullScoresHead{align-items:flex-start;flex-direction:column;gap:3px}.fullScoresScroll{overflow-x:auto}.qualifyPanel{padding:9px;margin-top:8px}.qualifyActions{grid-template-columns:1fr}.footer{margin-top:6px}\n}\n@media(max-width:410px){.gameActionBar{display:grid;grid-template-columns:1fr}.gameActionBar .volumeControl{width:100%;margin:0}.gameActionBar .surveillanceStartBtn{min-width:0}.levelHero{min-width:0}.hud{grid-template-columns:repeat(3,minmax(0,1fr))}.goalStrip{flex-direction:column;gap:3px}.touchControls{display:none!important}.dpad{display:none!important}.flashBtn{width:92px;height:92px}.scoreTable{font-size:10px}}\n@media(max-height:520px) and (orientation:landscape) and (max-width:900px){.brand{display:none}.hud{grid-template-columns:repeat(3,minmax(0,1fr))}.goalStrip{display:none}.touchControls{display:none!important}.dpad{display:none!important}.flashBtn{width:92px;height:92px}.side{grid-template-columns:1fr 1fr}}\n\n\n@media(max-width:900px){.brandRight{gap:7px}.langButtons{justify-content:flex-end}.langBtn{min-width:42px;min-height:34px}.volumeControl{transform:none}}\n@media(max-width:620px){.viewPanBtn{width:38px;height:38px;border-radius:10px;font-size:18px}.gameActionBar{gap:5px}.levelHero{min-width:112px;padding:7px 8px;font-size:9px}.levelHero strong{font-size:20px}.surveillanceStartBtn{min-height:44px;font-size:9px;padding:6px}.capturePopup{max-width:68%;padding:6px 8px}.stageLevelBadge{left:4px;top:4px;min-width:78px;padding:4px 6px}.brand{align-items:flex-start}.brandRight{flex-direction:column-reverse;align-items:flex-end;gap:5px}.brandMeta{display:none!important}.langBtn{min-width:42px;min-height:36px;font-size:10px}.gameStage{min-height:0}.touchControls{display:none!important}.flashWrap{display:none!important}.fullScores .scoreTable{min-width:390px}.topScoresCard .scoreTable{font-size:10px}}\n@media(max-width:370px){body{padding:3px}.brand h1{font-size:19px}.brand .version{font-size:8px}.langBtn{min-width:38px;padding:5px}.dpad{display:none!important}.flashBtn{width:86px;height:86px}.sentryBtn{min-width:86px}.volumeControl{transform:none}}\n\n\/* v4.8 \u2014 \u00e9\u00e9n verticale kolom: het labyrint gebruikt de volledige breedte *\/\n#cctvGame{width:100%;max-width:1600px;margin:0 auto}\n.layout{display:flex!important;flex-direction:column!important;gap:12px!important;width:100%}\n.gamePanel{width:100%}\n.side{display:flex!important;flex-direction:column!important;gap:10px!important;width:100%;grid-template-columns:none!important}\n.topScoresCard,.protocolCard,.fullScores{width:100%}\n.fullScores{margin-top:10px}\n@media(max-width:900px){\n  .layout{display:flex!important;flex-direction:column!important}\n  .side{display:flex!important;flex-direction:column!important;grid-template-columns:none!important}\n}\n@media(max-height:520px) and (orientation:landscape) and (max-width:900px){\n  .side{display:flex!important;flex-direction:column!important;grid-template-columns:none!important}\n}\n\n\n.fullscreenBtn{min-height:48px;padding:8px 14px;border:1px solid rgba(32,216,255,.38);border-radius:10px;background:#061a24;color:#e8fbff;cursor:pointer;font-weight:950;letter-spacing:.075em;font-size:11px}\n.fullscreenBtn:hover{filter:brightness(1.08)}.fullscreenBtn:active{transform:scale(.98)}\n.exitFullscreenBtn{display:none;position:absolute;z-index:40;right:9px;top:9px;min-width:40px;height:40px;padding:0 10px;border:1px solid rgba(255,255,255,.34);border-radius:9px;background:rgba(2,9,13,.78);color:#fff;cursor:pointer;font-size:11px;font-weight:950;letter-spacing:.04em;backdrop-filter:blur(5px)}\n.playArea:fullscreen,.playArea.fullscreenFallback{background:#000;display:flex;flex-direction:column;justify-content:center;align-items:center;width:100%;height:100%;padding:8px;box-sizing:border-box;overflow:hidden}\n.playArea:fullscreen .exitFullscreenBtn,.playArea.fullscreenFallback .exitFullscreenBtn{display:block}\n.playArea:fullscreen .capturePopupRow,.playArea.fullscreenFallback .capturePopupRow{flex:0 0 auto;width:min(100%,700px)}\n.playArea:fullscreen .gameStage,.playArea.fullscreenFallback .gameStage{flex:0 1 auto;width:min(100%,calc((100vh - 24px)*10\/7));max-width:100%;max-height:calc(100vh - 24px);aspect-ratio:10\/7;border-radius:8px}\n\n.playArea.fullscreenFallback{position:fixed;inset:0;z-index:99999}\nbody.fullscreenFallbackActive{overflow:hidden}\n@media(max-width:620px){.stageCameraBadge{right:4px;top:50px;min-width:76px;padding:4px 6px}.stageCameraBadge strong{font-size:16px}.diamondGuardBadge{top:4px;min-width:104px;padding:4px 7px;font-size:7px}.diamondGuardBadge strong{font-size:17px}.diamondIntruderBadge{top:40px;min-width:122px;padding:4px 7px;font-size:7px}.diamondIntruderBadge strong{font-size:17px}.fullscreenBtn{min-height:44px;font-size:10px;padding:7px 10px}.playArea:fullscreen,.playArea.fullscreenFallback{padding:4px}.exitFullscreenBtn{right:5px;top:5px;height:38px;min-width:38px;font-size:10px}}\n@media(orientation:portrait){.playArea:fullscreen .gameStage,.playArea.fullscreenFallback .gameStage{width:100%;max-height:none}}\n\n\n\/* v4.8 gameplay focus: legacy leaderboard\/qualification UI is permanently disabled *\/\n\n\/* v4.8 \u2014 compacte bediening in het labyrint *\/\n.gameActionBar{justify-content:flex-end}\n.gameActionBar .volumeControl{margin-left:auto}\n\n.stageControlGroup{\n  position:absolute;\n  z-index:22;\n  left:8px;\n  top:8px;\n  display:flex;\n  align-items:stretch;\n  gap:5px;\n  max-width:calc(100% - 16px);\n  padding:4px;\n  border:1px solid rgba(125,164,179,.22);\n  border-radius:11px;\n  background:rgba(1,11,16,.70);\n  backdrop-filter:blur(5px);\n}\n.stageControlGroup .stageLevelBadge,\n.stageControlGroup .stageCameraBadge,\n.stageControlGroup .stageEatenBadge{\n  position:static!important;\n  inset:auto!important;\n  min-width:74px;\n  height:42px;\n  padding:4px 7px;\n  box-sizing:border-box;\n  display:flex;\n  flex-direction:column;\n  justify-content:center;\n}\n.stageControlGroup .stageEatenBadge{\n  border:1px solid rgba(255,209,102,.38);\n  border-radius:9px;\n  background:rgba(28,20,4,.84);\n  color:#fff0b7;\n  font-size:8px;\n  font-weight:950;\n  letter-spacing:.08em;\n  text-align:center;\n  text-transform:uppercase;\n}\n.stageControlGroup .stageEatenBadge strong{display:block;margin-top:1px;color:#ffd166;font-size:16px;line-height:1}\n.stageControlGroup .stageCameraBadge{min-width:104px}\n.stageControlGroup .stageLevelBadge strong,\n.stageControlGroup .stageCameraBadge strong,\n.stageControlGroup .stageEatenBadge strong{font-size:16px}\n.stageControlGroup .stageFullscreenBtn{\n  position:static;\n  min-height:42px;\n  height:42px;\n  padding:5px 10px;\n  border-radius:8px;\n  font-size:9px;\n  white-space:nowrap;\n}\n\n\/* Start \u00e9n Opnieuw spelen staan altijd gecentreerd. *\/\n.overlay .startRow{\n  display:flex!important;\n  width:100%;\n  justify-content:center!important;\n  align-items:center;\n  margin-top:12px;\n}\n.overlay .startRow .btn{\n  width:auto!important;\n  min-width:190px;\n  max-width:min(100%,280px);\n  margin:0 auto;\n}\n.startNote{display:none!important}\n\n@media(max-width:620px){\n  .stageControlGroup{left:4px;top:4px;gap:2px;padding:3px;max-width:calc(100% - 8px)}\n  .stageControlGroup .stageLevelBadge,\n  .stageControlGroup .stageCameraBadge,\n  .stageControlGroup .stageEatenBadge{\n    min-width:60px;\n    height:38px;\n    padding:3px 5px;\n    font-size:7px;\n  }\n  .stageControlGroup .stageCameraBadge{min-width:86px}\n  .stageControlGroup .stageLevelBadge strong,\n  .stageControlGroup .stageCameraBadge strong,\n  .stageControlGroup .stageEatenBadge strong{font-size:14px}\n  .stageControlGroup .stageFullscreenBtn{\n    min-height:38px;\n    height:38px;\n    padding:4px 7px;\n    font-size:8px;\n  }\n  .overlay .startRow .btn{min-width:180px;width:auto!important}\n  .capturePopup{top:54px}\n}\n@media(max-width:390px){\n  .stageControlGroup .stageFullscreenBtn{padding:4px 6px;font-size:7px}\n  .stageControlGroup .stageLevelBadge{min-width:50px}\n  .stageControlGroup .stageCameraBadge{min-width:76px}\n  .stageControlGroup .stageEatenBadge{min-width:58px}\n}\n\n\n\/* v4.8 \u2014 de gemarkeerde bediening staat boven het labyrint, niet erop *\/\n.stageControlGroup{\n  position:static!important;\n  inset:auto!important;\n  z-index:auto;\n  display:flex;\n  align-items:stretch;\n  justify-content:flex-start;\n  gap:5px;\n  width:max-content;\n  max-width:100%;\n  margin:0 0 6px 0;\n  padding:4px;\n  border:1px solid rgba(125,164,179,.22);\n  border-radius:11px;\n  background:rgba(1,11,16,.88);\n  backdrop-filter:blur(5px);\n}\n.playArea:fullscreen,\n.playArea.fullscreenFallback{\n  justify-content:flex-start;\n}\n.playArea:fullscreen .stageControlGroup,\n.playArea.fullscreenFallback .stageControlGroup{\n  flex:0 0 auto;\n  align-self:flex-start;\n  margin:0 0 5px 0;\n}\n.playArea:fullscreen .gameStage,\n.playArea.fullscreenFallback .gameStage{\n  width:min(100%,calc((100vh - 78px)*10\/7));\n  max-height:calc(100vh - 78px);\n}\n@media(max-width:620px){\n  .stageControlGroup{\n    position:static!important;\n    left:auto!important;\n    top:auto!important;\n    gap:2px;\n    margin-bottom:4px;\n    max-width:100%;\n  }\n  .playArea:fullscreen .gameStage,\n  .playArea.fullscreenFallback .gameStage{\n    width:min(100%,calc((100vh - 66px)*10\/7));\n    max-height:calc(100vh - 66px);\n  }\n}\n@media(orientation:portrait){\n  .playArea:fullscreen .gameStage,\n  .playArea.fullscreenFallback .gameStage{\n    width:100%;\n    max-height:none;\n  }\n}\n\n\n\n\/* v5.1 \u2014 realtime WebGL CCTV monitor, realistische inbrekers + automatische event-cuts *\/\n.cctv3dMonitor{\n  position:absolute;\n  z-index:7;\n  right:10px;\n  bottom:10px;\n  width:clamp(250px,38%,430px);\n  aspect-ratio:16\/9;\n  padding:7px;\n  border:1px solid rgba(125,232,255,.62);\n  border-radius:11px;\n  background:linear-gradient(180deg,rgba(4,17,23,.96),rgba(0,4,7,.98));\n  box-shadow:0 0 0 2px rgba(0,0,0,.55),0 0 24px rgba(32,216,255,.16);\n  pointer-events:none;\n  overflow:hidden;\n}\n.cctv3dHead{display:flex;align-items:center;justify-content:space-between;gap:8px;height:22px;padding:0 3px 5px;color:#dffaff;font-size:8px;font-weight:950;letter-spacing:.12em;text-transform:uppercase}\n.cctv3dHead strong{color:#8dff75;font-size:9px;white-space:nowrap}.cctv3dLive{display:inline-flex;align-items:center;gap:5px}.cctv3dLive:before{content:\"\";width:6px;height:6px;border-radius:50%;background:#ff4d6d;box-shadow:0 0 8px rgba(255,77,109,.8);animation:cctvRecBlink 1.1s steps(2,end) infinite}@keyframes cctvRecBlink{50%{opacity:.34}}\n.cctv3dScreen{position:relative;height:calc(100% - 22px);overflow:hidden;border:1px solid rgba(32,216,255,.32);border-radius:6px;background:#000}\n#cctv3dCanvas{display:block;width:100%;height:100%;background:#000;image-rendering:auto}\n.cctv3dOsd{position:absolute;left:7px;right:7px;top:6px;z-index:2;display:flex;justify-content:space-between;gap:8px;color:#e9feff;font:700 7px\/1 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;letter-spacing:.06em;text-shadow:0 1px 3px #000;pointer-events:none}\n.cctv3dOsd b{color:#ff6a7e}.cctv3dBottom{position:absolute;left:7px;bottom:6px;z-index:2;color:#aaff9c;font:800 7px\/1 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;letter-spacing:.08em;text-shadow:0 1px 3px #000;pointer-events:none}\n.cctv3dReticle{position:absolute;left:50%;top:50%;z-index:2;width:18px;height:18px;transform:translate(-50%,-50%);opacity:.28;pointer-events:none}.cctv3dReticle:before,.cctv3dReticle:after{content:\"\";position:absolute;background:#dfffff}.cctv3dReticle:before{left:8px;top:0;width:1px;height:18px}.cctv3dReticle:after{left:0;top:8px;width:18px;height:1px}\n\n.cctv3dScreen:after{content:\"\";position:absolute;inset:0;pointer-events:none;background:repeating-linear-gradient(0deg,rgba(255,255,255,.028) 0,rgba(255,255,255,.028) 1px,transparent 1px,transparent 4px),radial-gradient(circle at center,transparent 45%,rgba(0,0,0,.30) 100%);mix-blend-mode:screen}\n.playArea:fullscreen .cctv3dMonitor,.playArea.fullscreenFallback .cctv3dMonitor{width:clamp(300px,36vw,520px);right:12px;bottom:12px}\n@media(max-width:620px){.cctv3dMonitor{right:5px;bottom:5px;width:48%;min-width:170px;padding:5px;border-radius:8px}.cctv3dHead{height:18px;font-size:6px;padding-bottom:3px}.cctv3dHead strong{font-size:7px}.cctv3dScreen{height:calc(100% - 18px)}}\n@media(max-width:390px){.cctv3dMonitor{width:47%;min-width:145px}}\n@media(prefers-reduced-motion:reduce){.cctv3dLive:before{animation:none}}\n\n\/* v5.2 \u2014 smartphone\/tablet: speelbeeld zo centraal mogelijk in fullscreen. *\/\n@media (pointer:coarse), (max-width:900px){\n  .playArea:fullscreen,.playArea.fullscreenFallback{\n    justify-content:center!important;\n    align-items:center!important;\n    min-height:100vh;\n    min-height:100dvh;\n    padding-top:max(4px,env(safe-area-inset-top));\n    padding-right:max(4px,env(safe-area-inset-right));\n    padding-bottom:max(4px,env(safe-area-inset-bottom));\n    padding-left:max(4px,env(safe-area-inset-left));\n  }\n  .playArea:fullscreen .stageControlGroup,.playArea.fullscreenFallback .stageControlGroup{\n    align-self:center!important;\n    margin:0 auto 5px!important;\n  }\n  .playArea:fullscreen .gameStage,.playArea.fullscreenFallback .gameStage{\n    margin-left:auto!important;\n    margin-right:auto!important;\n  }\n}\n@media (pointer:coarse) and (orientation:portrait), (max-width:620px) and (orientation:portrait){\n  .playArea:fullscreen .gameStage,.playArea.fullscreenFallback .gameStage{\n    width:min(100%,calc((100dvh - 58px)*10\/7));\n    max-width:100%;\n    max-height:calc(100dvh - 58px);\n  }\n}\n\n<\/style>\n<\/head>\n<body>\n<div id=\"cctvGame\" tabindex=\"0\">\n  <div class=\"brand\">\n    <h1><span class=\"cctv\">CCTV<\/span> <span class=\"pac\">PAC-MAZE<\/span><span class=\"version\">v5.2<\/span><\/h1>\n    <div class=\"brandRight\">\n      <div class=\"brandMeta\" id=\"brandMeta\">NIGHT WATCH \/\/ LIVE FEED<br>SECURITY MAZE CHASE<\/div>\n      <div class=\"langButtons\" aria-label=\"Language \/ Taal \/ Langue\">\n        <button type=\"button\" class=\"langBtn active\" data-lang=\"nl\">NL<\/button>\n        <button type=\"button\" class=\"langBtn\" data-lang=\"fr\">FR<\/button>\n        <button type=\"button\" class=\"langBtn\" data-lang=\"en\">ENG<\/button>\n      <\/div>\n    <\/div>\n  <\/div>\n\n  <div class=\"layout\">\n    <section class=\"gamePanel\">\n      <div class=\"hud\">\n        <div class=\"hudBox\"><div class=\"hudLabel\" id=\"hudLevelLabel\">Level<\/div><div class=\"hudValue\" id=\"levelEl\">01<\/div><\/div>\n        <div class=\"hudBox\"><div class=\"hudLabel\" id=\"hudCamsLabel\">Cams<\/div><div class=\"hudValue\" id=\"livesEl\">\ud83d\udcf9 \u00d71<\/div><\/div>\n        <div class=\"hudBox\"><div class=\"hudLabel\" id=\"hudGoalLabel\">Doel<\/div><div class=\"hudValue\" id=\"progressEl\">0 \/ 5<\/div><div class=\"meter\"><i id=\"progressFill\"><\/i><\/div><\/div>\n      <\/div>\n\n      <div class=\"goalStrip\" id=\"goalStrip\"><span id=\"goalText\"><strong>LEVEL 1:<\/strong> 1 CAMERA \u2022 EET 5 INBREKERS<\/span><span class=\"goalCount\" id=\"goalCount\">0 \/ 5<\/span><\/div>\n      <div class=\"gameActionBar\">\n        <div class=\"volumeControl\" id=\"volumeControl\" aria-label=\"Volume\">\n          <button type=\"button\" class=\"volumeStep\" id=\"volumeDownBtn\" aria-label=\"Volume lager\" title=\"Volume lager\">\u2212<\/button>\n          <button type=\"button\" class=\"volumeBtn\" id=\"volumeBtn\" aria-label=\"Geluid uitschakelen\" title=\"Geluid aan of uit\">\ud83d\udd0a<\/button>\n          <span class=\"volumeLevel\" id=\"volumeLevel\">500%<\/span>\n          <button type=\"button\" class=\"volumeStep\" id=\"volumeUpBtn\" aria-label=\"Volume hoger\" title=\"Volume hoger\">+<\/button>\n        <\/div>\n      <\/div>\n\n      <div class=\"playArea\" id=\"playArea\">\n        <div class=\"stageControlGroup\" id=\"stageControlGroup\">\n          <div class=\"stageLevelBadge\" id=\"stageLevelBadge\"><span id=\"stageLevelLabel\">LEVEL<\/span><strong id=\"stageLevelValue\">1<\/strong><\/div>\n          <div class=\"stageCameraBadge\" id=\"stageCameraBadge\" role=\"button\" tabindex=\"0\" aria-label=\"Volgende actieve camera\" title=\"Klik om naar de volgende actieve camera te gaan\"><span id=\"stageCameraLabel\">SWITCH CAMERA<\/span><strong id=\"stageCameraValue\">1\/1<\/strong><\/div>\n          <div class=\"stageEatenBadge\" id=\"stageEatenBadge\"><span id=\"stageEatenLabel\">GEGETEN<\/span><strong id=\"stageEatenValue\">0\/5<\/strong><\/div>\n          <button type=\"button\" class=\"fullscreenBtn stageFullscreenBtn\" id=\"fullscreenBtn\">\u26f6 FULLSCREEN<\/button>\n        <\/div>\n        <div class=\"gameStage\" id=\"stage\">\n          <button type=\"button\" class=\"exitFullscreenBtn\" id=\"exitFullscreenBtn\">\u2715 EXIT<\/button>\n          <div class=\"capturePopup\" id=\"capturePopup\" role=\"status\" aria-live=\"polite\" aria-atomic=\"true\">\n            <div class=\"capturePopupTitle\" id=\"capturePopupTitle\">INBREKER OPGEGETEN<\/div>\n            <div class=\"capturePopupCount\" id=\"capturePopupCount\">1 \/ 5<\/div>\n          <\/div>\n          <canvas id=\"gameCanvas\" width=\"800\" height=\"560\" aria-label=\"Interactief CCTV Pac-Maze spel\"><\/canvas>\n\n          <div class=\"cctv3dMonitor\" id=\"cctv3dMonitor\" aria-label=\"Live 3D CCTV beeld van de actieve camera\">\n            <div class=\"cctv3dHead\"><span class=\"cctv3dLive\">LIVE 3D<\/span><strong id=\"cctv3dLabel\">CAM 01<\/strong><\/div>\n            <div class=\"cctv3dScreen\"><canvas id=\"cctv3dCanvas\" width=\"480\" height=\"270\"><\/canvas><div class=\"cctv3dOsd\"><span><b>\u25cf REC<\/b> <span id=\"cctv3dMode\">WEBGL<\/span><\/span><span id=\"cctv3dClock\">00:00:00<\/span><\/div><div class=\"cctv3dReticle\"><\/div><div class=\"cctv3dBottom\" id=\"cctv3dSceneLabel\">MAZE FEED<\/div><\/div>\n          <\/div>\n          <div class=\"diamondGuardBadge\" id=\"diamondGuardBadge\" role=\"status\" aria-live=\"polite\"><span id=\"diamondGuardLabel\">DIAMANT BEWAKEN<\/span><strong id=\"diamondGuardValue\">60s<\/strong><\/div>\n          <div class=\"diamondIntruderBadge\" id=\"diamondIntruderBadge\" role=\"status\" aria-live=\"assertive\"><span id=\"diamondIntruderLabel\">INBREKER IN DIAMANTKAMER<\/span><strong id=\"diamondIntruderValue\">60s<\/strong><\/div>\n\n        <button type=\"button\" class=\"viewPanBtn viewPanUp\" data-pan=\"up\" aria-label=\"Beeld omhoog\" title=\"Beeld omhoog\">\u25b2<\/button>\n        <button type=\"button\" class=\"viewPanBtn viewPanDown\" data-pan=\"down\" aria-label=\"Beeld omlaag\" title=\"Beeld omlaag\">\u25bc<\/button>\n        <button type=\"button\" class=\"viewPanBtn viewPanLeft\" data-pan=\"left\" aria-label=\"Beeld links\" title=\"Beeld links\">\u25c0<\/button>\n        <button type=\"button\" class=\"viewPanBtn viewPanRight\" data-pan=\"right\" aria-label=\"Beeld rechts\" title=\"Beeld rechts\">\u25b6<\/button>\n\n        <div class=\"overlay\" id=\"overlay\">\n          <div class=\"intro\">\n            <div class=\"eyebrow\" id=\"overlayEyebrow\">SECURITY SYSTEM \/\/ STANDBY<\/div>\n            <h2 id=\"overlayTitle\"><span class=\"white\">CCTV<\/span> <b>NIGHT WATCH<\/b><\/h2>\n            <p id=\"overlayText\"><b>HOE SPEEL JE?<\/b> Kies een camera en klik waar hij naartoe moet. De camera doet de rest automatisch.<\/p>\n\n            <div class=\"tips\" id=\"introTips\">\n              <div class=\"tip\"><b>Besturing<\/b><br>Kies een camera en klik zijn bestemming.<\/div>\n              <div class=\"tip\"><b>Inbrekers<\/b><br>Flash en eet genoeg inbrekers om door te gaan.<\/div>\n              <div class=\"tip\"><b>Diamant<\/b><br>Bewaak de diamantkamer 60 seconden. Hoe langer je bewaakt, hoe meer inbrekers erop afkomen.<\/div>\n              <div class=\"tip\"><b>Geel bolletje<\/b><br>Pak het om een extra camera vrij te spelen.<\/div>\n            <\/div>\n\n            <div class=\"startRow\">\n              <button type=\"button\" class=\"btn\" id=\"startBtn\">\u25b6 START GAME<\/button>\n            <\/div>\n          <\/div>\n        <\/div>\n      <\/div>\n      <\/div>\n    <\/section>\n\n    <aside class=\"side\">\n      <section class=\"card protocolCard\">\n        <details id=\"protocolDetails\">\n          <summary id=\"protocolSummary\">Security Protocol<\/summary>\n          <div class=\"protocolBody\">\n            <div class=\"legend\" id=\"protocolLegend\"><\/div>\n          <\/div>\n        <\/details>\n        <div class=\"status\" id=\"statusLine\">Wacht op operator\u2026<\/div>\n      <\/section>\n    <\/aside>\n  <\/div>\n  <div class=\"footer\">CCTV NIGHT WATCH \/\/ v5.1 \/\/ 2D MAZE + DIRECTED REALTIME WEBGL CCTV<\/div>\n<\/div>\n\n<script>\n(() => {\n'use strict';\n\nconst root = document.getElementById('cctvGame');\nconst canvas = document.getElementById('gameCanvas');\nconst ctx = canvas.getContext('2d');\n\nconst cctv3dCanvas = document.getElementById('cctv3dCanvas');\nlet cctv3dCtx = null; \/\/ Alleen gebruikt als WebGL niet beschikbaar is.\nconst cctv3dLabel = document.getElementById('cctv3dLabel');\nconst cctv3dMode = document.getElementById('cctv3dMode');\nconst cctv3dClock = document.getElementById('cctv3dClock');\nconst cctv3dSceneLabel = document.getElementById('cctv3dSceneLabel');\nconst overlay = document.getElementById('overlay');\nconst overlayEyebrow = document.getElementById('overlayEyebrow');\nconst overlayTitle = document.getElementById('overlayTitle');\nconst overlayText = document.getElementById('overlayText');\nconst introTips = document.getElementById('introTips');\nconst startBtn = document.getElementById('startBtn');\nconst soundBtn = document.getElementById('soundBtn');\nconst volumeBtn = document.getElementById('volumeBtn');\nconst volumeDownBtn = document.getElementById('volumeDownBtn');\nconst volumeUpBtn = document.getElementById('volumeUpBtn');\nconst volumeLevel = document.getElementById('volumeLevel');\nconst fullscreenBtn = document.getElementById('fullscreenBtn');\nconst exitFullscreenBtn = document.getElementById('exitFullscreenBtn');\nconst playArea = document.getElementById('playArea');\nconst goalCount = document.getElementById('goalCount');\nconst stageLevelLabel = document.getElementById('stageLevelLabel');\nconst stageLevelValue = document.getElementById('stageLevelValue');\nconst stageCameraBadge = document.getElementById('stageCameraBadge');\nconst stageCameraLabel = document.getElementById('stageCameraLabel');\nconst stageCameraValue = document.getElementById('stageCameraValue');\nconst stageEatenBadge = document.getElementById('stageEatenBadge');\nconst stageEatenLabel = document.getElementById('stageEatenLabel');\nconst stageEatenValue = document.getElementById('stageEatenValue');\nconst diamondGuardBadge = document.getElementById('diamondGuardBadge');\nconst diamondGuardLabel = document.getElementById('diamondGuardLabel');\nconst diamondGuardValue = document.getElementById('diamondGuardValue');\nconst diamondIntruderBadge = document.getElementById('diamondIntruderBadge');\nconst diamondIntruderLabel = document.getElementById('diamondIntruderLabel');\nconst diamondIntruderValue = document.getElementById('diamondIntruderValue');\nconst surveillanceStartBtn = document.getElementById('surveillanceStartBtn');\nconst capturePopup = document.getElementById('capturePopup');\nconst capturePopupTitle = document.getElementById('capturePopupTitle');\nconst capturePopupCount = document.getElementById('capturePopupCount');\nconst levelEl = document.getElementById('levelEl');\nconst livesEl = document.getElementById('livesEl');\nconst progressEl = document.getElementById('progressEl');\nconst progressFill = document.getElementById('progressFill');\nconst statusLine = document.getElementById('statusLine');\nconst sentryTouch = document.getElementById('sentryTouch');\nconst brandMeta = document.getElementById('brandMeta');\nconst goalText = document.getElementById('goalText');\nconst protocolLegend = document.getElementById('protocolLegend');\nconst protocolSummary = document.getElementById('protocolSummary');\n\nconst VIEW_W = 800;\nconst VIEW_H = 560;\nconst TILE = 28;\nconst MAZE_W = 31;\nconst MAZE_H = 39;\nconst TUNNEL_ROW = 19;\nconst WORLD_W = MAZE_W * TILE;\nconst WORLD_H = MAZE_H * TILE;\nconst TAU = Math.PI * 2;\n\nconst MOBILE_RENDER_MODE = (\n  (window.matchMedia?.('(pointer: coarse)').matches===true || (navigator.maxTouchPoints||0)>0) &&\n  (Math.min(window.innerWidth,window.innerHeight)<=1024 || window.innerWidth<=1180)\n);\n\/\/ Op high-DPI smartphones kost DPR 2 bijna vier keer zoveel pixels als DPR 1.\n\/\/ 1.35 blijft visueel scherp genoeg op een CCTV-spel, maar maakt de hoofdanimatie veel stabieler.\nconst DPR = Math.min(window.devicePixelRatio || 1, MOBILE_RENDER_MODE ? 1.35 : 2);\ncanvas.width = Math.round(VIEW_W * DPR);\ncanvas.height = Math.round(VIEW_H * DPR);\nctx.setTransform(DPR,0,0,DPR,0,0);\n\nconst DIRS = {\n  up:{x:0,y:-1,name:'up'},\n  down:{x:0,y:1,name:'down'},\n  left:{x:-1,y:0,name:'left'},\n  right:{x:1,y:0,name:'right'},\n  none:{x:0,y:0,name:'none'}\n};\nconst CARDINALS = [DIRS.up,DIRS.left,DIRS.down,DIRS.right];\n\nconst LEVEL_CONFIG={\n  1:{eatGoal:5,maxCams:1,burglarCount:4,spawn:[2,35],diamond:[26,4],diamondRoom:[22,2,29,7],diamondFound:false,speed:1.00},\n  2:{eatGoal:10,maxCams:2,burglarCount:4,spawn:[27,35],diamond:[3,3],diamondRoom:[1,1,8,6],diamondFound:false,speed:1.02},\n  3:{eatGoal:12,maxCams:3,burglarCount:4,spawn:[2,2],diamond:[27,34],diamondRoom:[23,30,29,36],diamondFound:false,speed:1.06},\n  4:{eatGoal:14,maxCams:3,burglarCount:4,spawn:[27,2],diamond:[15,19],diamondRoom:[11,14,19,23],diamondFound:true,speed:1.10},\n  5:{eatGoal:15,maxCams:4,burglarCount:5,spawn:[15,35],diamond:[3,34],diamondRoom:[1,30,8,37],diamondFound:true,speed:1.18}\n};\nfunction levelConfig(level=state.level){ return LEVEL_CONFIG[level]||LEVEL_CONFIG[5]; }\n\nlet audioCtx = null;\n\nconst state = {\n  phase:'intro', paused:false, score:0, level:1, lives:3, name:'SECURITY',\n  map:[], levelRooms:[], diamondRoom:null, burglarSpawnSerial:0, player:null, cameras:[], burglars:[], dots:0, initialDots:0, collected:0,\n  exposed:0, exposedCombo:0, flashCooldown:0, flashFx:0, flashPulse:0, invuln:0,\n  banner:'', bannerTime:0, readyTimer:0,\n  sound:true, volume:5.0, lang:'nl', shake:0, chaseMode:true, modeTimer:10,\n  viewX:0, viewY:0, viewPanX:0, viewPanY:0, lastTime:performance.now(), levelSeed:0,\n  sentry:false, sentryPending:false, sentryTurnTimer:0, sentryHold:0, sentryDetected:false, sentryScanPulse:0, sentryResumeDir:null, surveillanceAngle:0, surveillanceReturn:false, surveillanceSweepDir:1, surveillanceWallSweep:false, surveillanceWallNormals:[], navTarget:null,\n  manualMode:false, movementFlashActive:false, stationaryTimer:0, flashHeld:false, flashTurnCooldown:0, flashResumeDir:null, flashResumeDesired:null, flashHoldTagged:[false,false,false,false], huntTarget:null, flashCount:0, flashGoal:5, eatenCount:0, eatGoal:5, spacePressStartedAt:0, lastQuickSpaceTapAt:0,\n  navWatchX:0, navWatchY:0, navWatchTimer:0, navRecoveryCooldown:0,\n  completeRevealTimer:0, scoreDecisionPending:false, pendingRank:null, diamond:null, diamondThreat:false, diamondGuardTime:60, diamondGuardActive:false, diamondIntruderTime:60, diamondIntruderActive:false, diamondPressureShown:0, winReason:null, goalReachedPending:false,\n  \/\/ 3D-monitorregie: dit verandert state.player NIET. Een achtergrondcamera kan tijdelijk\n  \/\/ in beeld springen wanneer hij een inbreker flasht en onderschept.\n  cctvEventCamId:null, cctvEventReturnAt:0, cctvEventReason:'', cctvEventTarget:null\n};\n\nfunction clamp(v,a,b){ return Math.max(a,Math.min(b,v)); }\nfunction dist(a,b){ return Math.hypot(a.x-b.x,a.y-b.y); }\nfunction reverseOf(a,b){ return !!a && !!b && a.x===-b.x && a.y===-b.y; }\nfunction formatScore(n){ return String(Math.max(0,Math.floor(n))).padStart(6,'0'); }\nfunction escapeHtml(value){ return String(value).replace(\/[&<>'\"]\/g,ch=>({'&':'&amp;','<':'&lt;','>':'&gt;',\"'\":'&#39;','\"':'&quot;'}[ch])); }\nfunction seeded(seed){ let a=seed|0; return () => { a=(a+0x6D2B79F5)|0; let t=a; t=Math.imul(t^(t>>>15),t|1); t^=t+Math.imul(t^(t>>>7),t|61); return ((t^(t>>>14))>>>0)\/4294967296; }; }\nfunction shuffle(array,rnd){ for(let i=array.length-1;i>0;i--){ const j=Math.floor(rnd()*(i+1)); [array[i],array[j]]=[array[j],array[i]]; } return array; }\n\n\nconst I18N={\n nl:{brand:'NIGHT WATCH \/\/ LIVE FEED<br>SECURITY MAZE CHASE',level:'Level',cams:'Cams',goal:'Doel',goalText:'<strong>LEVEL 1:<\/strong> 1 CAMERA \u2022 EET 5 INBREKERS',standby:'SECURITY SYSTEM \/\/ STANDBY',online:'SECURITY SYSTEM \/\/ ONLINE',intro:'<b>HOE SPEEL JE?<\/b> Kies een camera en klik waar hij naartoe moet. De camera doet de rest automatisch.',tips:[['Besturing','Linksklik = bestemming. Rechtermuisklik = volgende actieve camera.'],['Inbrekers','Flash en eet genoeg inbrekers om door te gaan.'],['Diamant','Bewaak de diamantkamer 60 seconden. Hoe langer je bewaakt, hoe meer inbrekers erop afkomen.'],['Geel bolletje','Pak het om een extra camera vrij te spelen.']],start:'\u25b6 START SPEL',restart:'\u21bb OPNIEUW SPELEN',soundOn:'\ud83d\udd0a GELUID AAN',soundOff:'\ud83d\udd07 GELUID UIT',holdFlash:'HOUD FLASH',sentry:'SURVEILLANCE',surveillance:'SURVEILLANCE',startSurveillance:'START SURVEILLANCE MODE \u2022 360\u00b0',eatenPopup:'INBREKER OPGEGETEN',fullscreen:'\u26f6 FULLSCREEN',exitFullscreen:'\u2715 EXIT FULLSCREEN',protocol:'Security Protocol',protocolRows:[['\ud83c\udfaf','Levels','L1: 1 camera\/5. L2: gele bolletjes + 2 camera\u2019s\/10. L3: tot 3 camera\u2019s\/12. L4: diamant meteen actief\/14. L5: 5 inbrekers + tot 4 camera\u2019s\/15.'],['\ud83d\udc8e','Diamant','Zodra het middelpunt van minstens \u00e9\u00e9n actieve camera de diamantkamer binnenkomt, verschijnt onmiddellijk de 60-seconden teller. Houd minstens \u00e9\u00e9n camera 60 seconden onafgebroken in de kamer om meteen naar het volgende level te gaan; in Level 5 win je daarmee het spel. Tijdens de bewaking wordt eerst \u00e9\u00e9n inbreker via de kortste route naar de diamantkamer gestuurd; elke 5 seconden wordt een extra actieve inbreker naar de diamantkamer gestuurd. Zij mogen daarvoor ook omkeren. Een inbreker die er 60 seconden onafgebroken blijft veroorzaakt Game Over. Het diamantpijltje knippert rood zolang er een inbreker in de kamer is.'],['\u25ce','Klikbesturing','Klik een camera aan en klik daarna zijn bestemming. De camera rijdt zelf de route.'],['\u25c9','Surveillance mode','Start automatisch zodra een camera stilstaat. Tijdens beweging blijft de flash automatisch actief.'],['\u2726','Flash herstel','Een geflashte inbreker blijft 30 seconden uitgeschakeld. Als geen camera hem binnen die tijd opeet, wordt hij vanzelf weer actief.'],['\ud83d\udeaa','Nieuwe inbrekers','Nieuwe of terugkerende inbrekers verschijnen altijd in een andere kamer dan de kamers waar actieve camera\u2019s staan.'],['\ud83d\udcf9','Camera wisselen','Klik op SWITCH CAMERA om naar de volgende actieve camera te springen. Rechtermuisklik op het speelveld doet hetzelfde. De verhouding, bijvoorbeeld 2\/3, toont welke actieve camera je geselecteerd hebt. Daarnaast toont GEGETEN bijvoorbeeld 6\/10 hoeveel inbrekers je in dit level al hebt opgegeten.'],['\ud83d\udcf9','Camera uitgeschakeld','Als een camera uitvalt terwijl er nog camera\u2019s over zijn, gaat het spel verder. Het beeld springt automatisch naar een overgebleven camera en in een nabije ruimte verschijnt een geel bolletje om opnieuw een camera vrij te spelen.'],['\ud83d\udcf9','Extra camera\u2019s','Pas in Level 5: pak een geel bolletje voor een tweede camera met een andere kleur en \u00e9\u00e9n been extra. Bij camerawissel gaat de andere automatisch in Surveillance mode.'],['\u2194','Beeld verschuiven','Gebruik de vier subtiele pijlen op het speelveld om het gebouw in alle richtingen te bekijken.'],['\u26f6','Mobiel fullscreen','Op smartphone en tablet opent Start\/Opnieuw spelen automatisch het spel schermvullend.'],['\ud83d\udd0a','Volume','Gebruik \u2212 \/ + om stiller of luider te zetten; de speakerknop dempt.']],waiting:'Wacht op operator\u2026',volDown:'Volume lager',volUp:'Volume hoger',mute:'Geluid uitschakelen',unmute:'Geluid inschakelen',ready:'KLAAR',held:'VAST',active:'ACTIEF',target:'DOELWIT',scan:'SCAN',sector:'SECTOR',clear:'STATUS VEILIG',alarm:'ALARM',flashed:'GEFLASHT',diamond:'DIAMANT',diamondFound:'DIAMANT GEVONDEN',diamondDanger:'DIAMANT IN GEVAAR',diamondStolen:'DIAMANT GESTOLEN',guardDiamond:'DIAMANTKAMER BEWAKEN',diamondSecured:'DIAMANT 60s BEWAAKT',intruderDiamondRoom:'INBREKER IN DIAMANTKAMER',diamondRoomLost:'DIAMANTKAMER 60s BEZET',missionComplete:'MISSIE VOLTOOID',goalAchieved:'DOEL BEHAALD',missionText:'Je hebt alle 5 levels voltooid.',gameOver:'SHIFT BE\u00cbINDIGD',lockdown:'LOCKDOWN'},\n fr:{brand:'NIGHT WATCH \/\/ FLUX EN DIRECT<br>COURSE DE S\u00c9CURIT\u00c9',level:'Niveau',cams:'Cam\u00e9ras',goal:'Objectif',goalText:'<strong>NIVEAU 1 :<\/strong> 1 CAM\u00c9RA \u2022 CAPTUREZ 5 CAMBRIOLEURS',standby:'SYST\u00c8ME DE S\u00c9CURIT\u00c9 \/\/ EN ATTENTE',online:'SYST\u00c8ME DE S\u00c9CURIT\u00c9 \/\/ EN LIGNE',intro:'<b>COMMENT JOUER ?<\/b> Choisissez une cam\u00e9ra puis cliquez sur sa destination. Elle fait le reste automatiquement.',tips:[['Commandes','Clic gauche = destination. Clic droit = cam\u00e9ra active suivante.'],['Cambrioleurs','Flashez et capturez assez de cambrioleurs pour avancer.'],['Diamant','Surveillez la salle du diamant 60 secondes. Plus vous la gardez longtemps, plus de cambrioleurs arrivent.'],['Boule jaune','Prenez-la pour d\u00e9bloquer une cam\u00e9ra suppl\u00e9mentaire.']],start:'\u25b6 D\u00c9MARRER',restart:'\u21bb REJOUER',soundOn:'\ud83d\udd0a SON ACTIV\u00c9',soundOff:'\ud83d\udd07 SON COUP\u00c9',holdFlash:'MAINTENIR FLASH',sentry:'SURVEILLANCE',surveillance:'SURVEILLANCE',startSurveillance:'D\u00c9MARRER SURVEILLANCE \u2022 360\u00b0',eatenPopup:'CAMBRIOLEUR CAPTUR\u00c9',fullscreen:'\u26f6 PLEIN \u00c9CRAN',exitFullscreen:'\u2715 QUITTER',protocol:'Protocole de s\u00e9curit\u00e9',protocolRows:[['\ud83c\udfaf','Niveaux','N1 : 1 cam\u00e9ra\/5. N2 : boules jaunes + 2 cam\u00e9ras\/10. N3 : jusqu\u2019\u00e0 3 cam\u00e9ras\/12. N4 : diamant actif imm\u00e9diatement\/14. N5 : 5 cambrioleurs + jusqu\u2019\u00e0 4 cam\u00e9ras\/15.'],['\ud83d\udc8e','Diamant','D\u00e8s que le centre d\u2019une cam\u00e9ra active entre dans la salle du diamant, le compteur de 60 secondes appara\u00eet imm\u00e9diatement. Gardez au moins une cam\u00e9ra active 60 secondes dans la salle pour passer au niveau suivant ; au niveau 5, cela gagne la partie. Pendant la surveillance, un premier cambrioleur prend le chemin le plus court vers la salle ; toutes les 5 secondes un autre cambrioleur actif est envoy\u00e9 vers la salle du diamant. Ils peuvent aussi faire demi-tour. Un cambrioleur qui y reste 60 secondes provoque la fin de partie. La fl\u00e8che du diamant clignote en rouge tant qu\u2019un cambrioleur est dans la salle.'],['\u2423','Flash maintenu','Maintenir = arr\u00eat + lumi\u00e8re ; \u2190\/\u2192 tourne. La lumi\u00e8re ne traverse pas les murs.'],['\u2191','Manuel','Dirigez avec les fl\u00e8ches.'],['\u25ce','Automatique','Cliquez ou touchez le b\u00e2timent pour d\u00e9finir une route.'],['S','Mode surveillance','D\u00e9marrez avec S ou 2 pressions rapides sur ESPACE. La cam\u00e9ra s\u2019arr\u00eate, tourne en continu \u00e0 360\u00b0 et garde le flash allum\u00e9. Apr\u00e8s d\u00e9tection, elle capture les cambrioleurs flash\u00e9s puis reprend la surveillance.'],['\ud83d\udcf9','Cam\u00e9ras suppl\u00e9mentaires','\u00c0 partir du niveau 5 : prenez une boule jaune pour obtenir une deuxi\u00e8me cam\u00e9ra d\u2019une autre couleur avec une patte de plus.'],['\u2194','D\u00e9placer la vue','Utilisez les quatre fl\u00e8ches sur l\u2019\u00e9cran pour d\u00e9placer la vue du b\u00e2timent dans toutes les directions.'],['\u26f6','Plein \u00e9cran mobile','Sur smartphone et tablette, D\u00e9marrer\/Rejouer ouvre automatiquement le jeu en plein \u00e9cran.'],['\ud83d\udd0a','Volume','Utilisez \u2212 \/ + pour r\u00e9gler le volume (jusqu\u2019\u00e0 500 %) ; le haut-parleur coupe le son.']],waiting:'En attente de l\u2019op\u00e9rateur\u2026',volDown:'Baisser le volume',volUp:'Augmenter le volume',mute:'Couper le son',unmute:'Activer le son',ready:'PR\u00caT',held:'MAINTENU',active:'ACTIF',target:'CIBLE',scan:'SCAN',sector:'SECTEUR',clear:'STATUT S\u00dbR',alarm:'ALARME',flashed:'FLASH\u00c9',diamond:'DIAMANT',diamondFound:'DIAMANT TROUV\u00c9',diamondDanger:'DIAMANT EN DANGER',diamondStolen:'DIAMANT VOL\u00c9',guardDiamond:'SURVEILLER LE DIAMANT',diamondSecured:'DIAMANT SURVEILL\u00c9 60s',intruderDiamondRoom:'CAMBRIOLEUR DANS LA SALLE',diamondRoomLost:'SALLE OCCUP\u00c9E 60s',missionComplete:'MISSION TERMIN\u00c9E',goalAchieved:'OBJECTIF ATTEINT',missionText:'Vous avez termin\u00e9 le niveau 5 en <b>capturant 10 cambrioleurs<\/b>.',gameOver:'SERVICE TERMIN\u00c9',lockdown:'VERROUILLAGE'},\n en:{brand:'NIGHT WATCH \/\/ LIVE FEED<br>SECURITY MAZE CHASE',level:'Level',cams:'Cams',goal:'Goal',goalText:'<strong>LEVEL 1:<\/strong> 1 CAMERA \u2022 EAT 5 BURGLARS',standby:'SECURITY SYSTEM \/\/ STANDBY',online:'SECURITY SYSTEM \/\/ ONLINE',intro:'<b>HOW TO PLAY?<\/b> Choose a camera and click where it should go. The camera does the rest automatically.',tips:[['Controls','Left click = destination. Right click = next active camera.'],['Burglars','Flash and eat enough burglars to advance.'],['Diamond','Guard the diamond room for 60 seconds. The longer you guard it, the more burglars move toward it.'],['Yellow orb','Pick it up to unlock an extra camera.']],start:'\u25b6 START GAME',restart:'\u21bb PLAY AGAIN',soundOn:'\ud83d\udd0a SOUND ON',soundOff:'\ud83d\udd07 SOUND OFF',holdFlash:'HOLD FLASH',sentry:'SURVEILLANCE',surveillance:'SURVEILLANCE',startSurveillance:'START SURVEILLANCE MODE \u2022 360\u00b0',eatenPopup:'BURGLAR EATEN',fullscreen:'\u26f6 FULLSCREEN',exitFullscreen:'\u2715 EXIT FULLSCREEN',protocol:'Security Protocol',protocolRows:[['\ud83c\udfaf','Levels','L1: 1 camera\/5. L2: yellow orbs + 2 cameras\/10. L3: up to 3 cameras\/12. L4: diamond active immediately\/14. L5: 5 burglars + up to 4 cameras\/15.'],['\ud83d\udc8e','Diamond','As soon as the centre of an active camera enters the diamond room, the 60-second timer appears immediately. Keep at least one camera in the room for 60 continuous seconds to advance to the next level; in Level 5 this wins the game. While you guard it, one burglar takes the shortest route to the room first and every 5 seconds another active burglar is sent toward the diamond room. They are allowed to turn around to do so. A burglar staying there for 60 continuous seconds causes Game Over. The diamond direction arrow flashes red while a burglar is inside.'],['\u2191','Manual','Drive with the click control.'],['\u25ce','Automatic','Click or tap in the building to set a route.'],['S','Surveillance mode','Start with S or a quick double-tap on SPACE. Camera stops, rotates smoothly through 360\u00b0 and keeps the flash lit. After detection it captures the flashed burglar(s), then resumes surveillance automatically.'],['\ud83d\udeaa','New burglars','New or returning burglars always appear in a different room from any room currently occupied by an active camera.'],['\ud83d\udcf9','Switch camera','Click SWITCH CAMERA to jump to the next active camera. Right-clicking the playfield does the same. The ratio, for example 2\/3, shows which active camera is selected. EATEN shows, for example, 6\/10 for the burglars already eaten in this level.'],['\ud83d\udcf9','Extra cameras','From Level 2 onward, yellow orbs unlock extra cameras. Higher levels allow more cameras.'],['\u2194','Pan view','Use the four on-screen arrows to move the building view in every direction.'],['\u26f6','Mobile fullscreen','On phones and tablets, Start\/Play Again automatically opens the game fullscreen.'],['\ud83d\udd0a','Volume','Use \u2212 \/ + to set volume (up to 500%); the speaker button mutes.']],waiting:'Waiting for operator\u2026',volDown:'Volume down',volUp:'Volume up',mute:'Mute sound',unmute:'Unmute sound',ready:'READY',held:'HELD',active:'ACTIVE',target:'TARGET',scan:'SCAN',sector:'SECTOR',clear:'STATUS CLEAR',alarm:'ALARM',flashed:'FLASHED',diamond:'DIAMOND',diamondFound:'DIAMOND FOUND',diamondDanger:'DIAMOND IN DANGER',diamondStolen:'DIAMOND STOLEN',guardDiamond:'GUARD DIAMOND ROOM',diamondSecured:'DIAMOND GUARDED 60s',intruderDiamondRoom:'BURGLAR IN DIAMOND ROOM',diamondRoomLost:'DIAMOND ROOM HELD 60s',missionComplete:'MISSION COMPLETE',goalAchieved:'GOAL ACHIEVED',missionText:'You completed all 5 levels.',gameOver:'SHIFT TERMINATED',lockdown:'LOCKDOWN'}\n};\nfunction L(){return I18N[state.lang]||I18N.nl;}\nfunction localizeBanner(text){\n  const d=L(); const map={\n    'READY!':d.ready,'FLASH HOLD':'FLASH HOLD','INTRUDER FLASHED':state.lang==='fr'?'CAMBRIOLEUR FLASH\u00c9':state.lang==='en'?'BURGLAR FLASHED':'INBREKER GEFLASHT',\n    'AUTO HUNT':state.lang==='fr'?'POURSUITE AUTO':state.lang==='en'?'AUTO HUNT':'AUTO JACHT','AWAITING INPUT':state.lang==='fr'?'EN ATTENTE':state.lang==='en'?'AWAITING INPUT':'WACHT OP INPUT',\n    'MANUAL':state.lang==='fr'?'MANUEL':state.lang==='en'?'MANUAL':'MANUEEL','ROUTE SET':state.lang==='fr'?'ITIN\u00c9RAIRE D\u00c9FINI':state.lang==='en'?'ROUTE SET':'ROUTE INGESTELD',\n    'NO ROUTE':state.lang==='fr'?'AUCUN CHEMIN':state.lang==='en'?'NO ROUTE':'GEEN ROUTE','DESTINATION':state.lang==='fr'?'DESTINATION':state.lang==='en'?'DESTINATION':'BESTEMMING',\n    'DIAMOND FOUND':d.diamondFound,'DIAMOND DANGER':d.diamondDanger,'DIAMOND STOLEN':d.diamondStolen,'PAUSED':state.lang==='fr'?'PAUSE':state.lang==='en'?'PAUSED':'PAUZE','RESUMED':state.lang==='fr'?'REPRIS':state.lang==='en'?'RESUMED':'HERVAT','ALARM MODE':state.lang==='fr'?'MODE ALARME':state.lang==='en'?'ALARM MODE':'ALARMMODUS',\n    'SECTOR CLEARED':state.lang==='fr'?'SECTEUR NETTOY\u00c9':state.lang==='en'?'SECTOR CLEARED':'SECTOR VRIJ','CAMERA OFFLINE':state.lang==='fr'?'CAM\u00c9RA HORS LIGNE':state.lang==='en'?'CAMERA OFFLINE':'CAMERA OFFLINE',\n    'BACKUP CAMERA':state.lang==='fr'?'CAM\u00c9RA DE SECOURS':state.lang==='en'?'BACKUP CAMERA':'RESERVECAMERA','SENTRY ONLINE':state.lang==='fr'?'SURVEILLANCE ACTIVE':state.lang==='en'?'SURVEILLANCE ACTIVE':'SURVEILLANCE ACTIEF','SENTRY OFF':state.lang==='fr'?'SURVEILLANCE ARR\u00caT\u00c9E':state.lang==='en'?'SURVEILLANCE OFF':'SURVEILLANCE UIT','SENTRY ARMING':state.lang==='fr'?'SURVEILLANCE EN PR\u00c9PARATION':state.lang==='en'?'SURVEILLANCE ARMING':'SURVEILLANCE WORDT ACTIEF','INTRUDER DETECTED':state.lang==='fr'?'CAMBRIOLEUR D\u00c9TECT\u00c9':state.lang==='en'?'INTRUDER DETECTED':'INBREKER GEDETECTEERD'\n  };\n  return map[text]||text;\n}\nlet capturePopupTimer=null;\n\nfunction showCompactPopup(title,countText='',duration=850){\n  if(!capturePopup) return;\n  capturePopupTitle.textContent=title;\n  capturePopupCount.textContent=countText;\n  capturePopup.classList.remove('show');\n  void capturePopup.offsetWidth;\n  capturePopup.classList.add('show');\n  if(capturePopupTimer) clearTimeout(capturePopupTimer);\n  capturePopupTimer=setTimeout(()=>capturePopup.classList.remove('show'),duration);\n}\n\n\nfunction updateLevelDisplay(){\n  const d=L();\n  if(stageLevelLabel) stageLevelLabel.textContent=d.level;\n  if(stageLevelValue) stageLevelValue.textContent=String(state.level);\n  const activeCameras=(state.cameras||[]).filter(cam=>cam&&cam.alive!==false);\n  const selectedPosition=Math.max(0,activeCameras.indexOf(state.player));\n  if(stageCameraLabel) stageCameraLabel.textContent='SWITCH CAMERA';\n  if(stageCameraValue) stageCameraValue.textContent=activeCameras.length?`${selectedPosition+1}\/${activeCameras.length}`:'0\/0';\n  if(stageCameraBadge){\n    const hint=state.lang==='fr'?'Passer \u00e0 la cam\u00e9ra active suivante':state.lang==='en'?'Switch to next active camera':'Ga naar de volgende actieve camera';\n    stageCameraBadge.setAttribute('aria-label',hint);\n    stageCameraBadge.title=hint;\n  }\n  if(stageEatenLabel) stageEatenLabel.textContent=state.lang==='fr'?'CAPTUR\u00c9S':state.lang==='en'?'EATEN':'GEGETEN';\n  if(stageEatenValue) stageEatenValue.textContent=`${state.eatenCount}\/${state.eatGoal}`;\n}\n\nfunction updateSurveillanceStartButton(){\n  if(!surveillanceStartBtn) return;\n  if(!surveillanceStartBtn) return;\n  surveillanceStartBtn.textContent=L().startSurveillance;\n  const active=!!(state.sentry||state.sentryPending);\n  surveillanceStartBtn.classList.toggle('active',active);\n  surveillanceStartBtn.setAttribute('aria-pressed',active?'true':'false');\n  surveillanceStartBtn.disabled=state.phase!=='playing' || state.paused || active;\n}\n\nfunction showCaptureProgressPopup(){\n  showCompactPopup(L().eatenPopup,`${state.eatenCount} \/ ${state.eatGoal}`,850);\n}\n\nfunction setLanguage(lang){\n  if(!I18N[lang]) return; state.lang=lang; document.documentElement.lang=lang==='en'?'en':lang;\n  const d=L();\n  document.querySelectorAll('.langBtn').forEach(b=>b.classList.toggle('active',b.dataset.lang===lang));\n  brandMeta.innerHTML=d.brand;\n  document.getElementById('hudLevelLabel').textContent=d.level;\n  document.getElementById('hudCamsLabel').textContent=d.cams;\n  document.getElementById('hudGoalLabel').textContent=d.goal;\n  updateLevelGoalText();\n  protocolSummary.textContent=d.protocol;\n  protocolLegend.innerHTML=d.protocolRows.map(r=>`<div class=\"legendRow\"><span class=\"key\">${r[0]}<\/span><span><b>${r[1]}<\/b> \u2014 ${r[2]}<\/span><\/div>`).join('');\n  if(flashTouch) flashTouch.textContent=d.holdFlash; if(sentryTouch) sentryTouch.textContent=d.sentry; if(soundBtn) soundBtn.textContent=state.sound?d.soundOn:d.soundOff;\n  if(fullscreenBtn) fullscreenBtn.textContent=d.fullscreen;\n  if(exitFullscreenBtn) exitFullscreenBtn.textContent=d.exitFullscreen;\n  if(diamondGuardLabel) diamondGuardLabel.textContent=d.guardDiamond;\n  if(diamondIntruderLabel) diamondIntruderLabel.textContent=d.intruderDiamondRoom;\n  updateLevelDisplay(); updateSurveillanceStartButton(); if(capturePopup?.classList.contains('show')){capturePopupTitle.textContent=d.eatenPopup;capturePopupCount.textContent=`${state.eatenCount} \/ ${state.eatGoal}`;}\n  volumeDownBtn.title=d.volDown; volumeDownBtn.setAttribute('aria-label',d.volDown); volumeUpBtn.title=d.volUp; volumeUpBtn.setAttribute('aria-label',d.volUp); volumeBtn.title=state.sound?d.mute:d.unmute; volumeBtn.setAttribute('aria-label',state.sound?d.mute:d.unmute);\n  if(state.phase==='intro'){overlayEyebrow.textContent=d.standby; overlayText.innerHTML=d.intro; startBtn.textContent=d.start; statusLine.textContent=d.waiting;}\n  else if(state.phase==='complete'){if(state.winReason==='diamondGuard'){overlayEyebrow.textContent=d.diamondSecured;overlayTitle.innerHTML=`<span class=\"white\">${d.diamond}<\/span> <b>${d.goalAchieved}<\/b>`;overlayText.innerHTML=state.lang==='fr'?`Vous avez surveill\u00e9 la salle du diamant pendant <b>60 secondes sans interruption<\/b>. La mission est termin\u00e9e.`:state.lang==='en'?`You guarded the diamond room for <b>60 continuous seconds<\/b>. The mission is complete.`:`Je hebt de diamantkamer <b>60 seconden onafgebroken bewaakt<\/b>. De missie is voltooid.`;}else{overlayEyebrow.textContent=`${d.missionComplete} \/\/ ${state.eatenCount} \/ ${state.eatGoal}`;overlayTitle.innerHTML=`<span class=\"white\">${d.goalAchieved.split(' ')[0]}<\/span> <b>${d.goalAchieved.split(' ').slice(1).join(' ')||d.goalAchieved}<\/b>`;overlayText.innerHTML=state.lang==='fr'?`Vous avez captur\u00e9 <b>${state.eatGoal} cambrioleurs au niveau 5<\/b>. La mission est termin\u00e9e.`:state.lang==='en'?`You ate <b>${state.eatGoal} burglars in Level 5<\/b>. The mission is complete.`:`Je hebt in Level 5 <b>${state.eatGoal} inbrekers opgegeten<\/b>. De missie is voltooid.`;}startBtn.textContent=d.restart;statusLine.textContent=d.missionComplete;}\n  else if(state.phase==='gameover'){startBtn.textContent=d.restart;if(state.diamond?.stolen){overlayEyebrow.textContent=d.diamondStolen;overlayTitle.innerHTML=`<span class=\"white\">${d.diamond}<\/span> <b>${d.lockdown}<\/b>`;overlayText.innerHTML=state.lang==='fr'?`Un cambrioleur a atteint le diamant. Vous aviez captur\u00e9 <b>${state.eatenCount}\/${state.eatGoal}<\/b> cambrioleurs.`:state.lang==='en'?`A burglar reached the diamond. You had eaten <b>${state.eatenCount}\/${state.eatGoal}<\/b> burglars.`:`Een inbreker bereikte de diamant. Je had <b>${state.eatenCount}\/${state.eatGoal}<\/b> inbrekers opgegeten.`;statusLine.textContent=d.diamondStolen;}else{overlayEyebrow.textContent=d.gameOver;overlayTitle.innerHTML=`<span class=\"white\">CCTV<\/span> <b>${d.lockdown}<\/b>`;overlayText.innerHTML=state.lang==='fr'?`Toutes les cam\u00e9ras sont hors ligne au niveau <b>${state.level}<\/b>.`:state.lang==='en'?`All cameras are offline in level <b>${state.level}<\/b>.`:`Alle camera's zijn offline in level <b>${state.level}<\/b>.`;statusLine.textContent=d.gameOver;}}\n  else if(state.phase==='playing'||state.phase==='ready'){statusLine.textContent=state.diamond?.found?(state.lang==='fr'?'Diamant sous surveillance \u2014 prot\u00e9gez-le.':state.lang==='en'?'Diamond under guard \u2014 protect it.':'Diamant onder bewaking \u2014 bescherm hem.'):(state.lang==='fr'?'Cherchez le diamant dans le b\u00e2timent.':state.lang==='en'?'Find the diamond in the building.':'Zoek de diamant in het gebouw.');}\n  introTips.innerHTML=d.tips.map(x=>`<div class=\"tip\"><b>${x[0]}<\/b><br>${x[1]}<\/div>`).join('');\n  updateVolumeUI(); updateHUD();\n}\n\nfunction ensureAudio(){\n  if(!state.sound) return;\n  const AC = window.AudioContext || window.webkitAudioContext;\n  if(!AC) return;\n  if(!audioCtx) audioCtx = new AC();\n  if(audioCtx.state==='suspended') audioCtx.resume();\n}\nfunction beep(freq=440,dur=.05,type='square',vol=.02,slide=0){\n  if(!state.sound) return;\n  ensureAudio();\n  if(!audioCtx) return;\n  const o=audioCtx.createOscillator(), g=audioCtx.createGain(), t=audioCtx.currentTime;\n  o.type=type; o.frequency.setValueAtTime(freq,t);\n  if(slide) o.frequency.exponentialRampToValueAtTime(Math.max(40,freq+slide),t+dur);\n  g.gain.setValueAtTime(vol*clamp(state.volume,0,5),t); g.gain.exponentialRampToValueAtTime(.0001,t+dur);\n  o.connect(g); g.connect(audioCtx.destination); o.start(t); o.stop(t+dur);\n}\nfunction sfx(type){\n  if(type==='dot') beep(620,.018,'square',.009,70);\n  if(type==='orb'){ beep(190,.11,'sawtooth',.03,520); setTimeout(()=>beep(720,.08,'square',.02,-140),70); }\n  if(type==='flash') beep(1050,.055,'sawtooth',.025,-650);\n  if(type==='stun') beep(260,.08,'square',.022,190);\n  if(type==='capture'){ beep(430,.07,'square',.028,320); setTimeout(()=>beep(780,.07,'square',.02,80),65); }\n  if(type==='hit'){ beep(160,.17,'sawtooth',.04,-90); setTimeout(()=>beep(85,.20,'square',.02,-20),100); }\n  if(type==='bonus'){ [820,1060,1320].forEach((f,i)=>setTimeout(()=>beep(f,.06,'triangle',.02),i*50)); }\n  if(type==='level'){ [440,560,680,900].forEach((f,i)=>setTimeout(()=>beep(f,.09,'square',.025,60),i*80)); }\n}\n\nfunction generateMaze(level){\n  const rnd=seeded(24681357 + level*7919);\n  const grid=Array.from({length:MAZE_H},()=>Array(MAZE_W).fill('#'));\n  const floor=(x,y)=>{ if(x>=0&&x<MAZE_W&&y>=0&&y<MAZE_H) grid[y][x]=' '; };\n  const declaredRooms=[];\n  const room=(x,y,w,h,track=true)=>{\n    for(let yy=y;yy<y+h;yy++) for(let xx=x;xx<x+w;xx++) floor(xx,yy);\n    if(track) declaredRooms.push({x,y,w,h});\n  };\n  const hallH=(x1,x2,y,width=1)=>{ for(let yy=y;yy<y+width;yy++) for(let x=Math.min(x1,x2);x<=Math.max(x1,x2);x++) floor(x,yy); };\n  const hallV=(y1,y2,x,width=1)=>{ for(let xx=x;xx<x+width;xx++) for(let y=Math.min(y1,y2);y<=Math.max(y1,y2);y++) floor(xx,y); };\n  let orbSeeds=[];\n\n  if(level===1){\n    \/\/ Compact kantoorverdieping: eenvoudig leesbaar voor de eerste ronde.\n    room(1,1,8,6); room(12,1,7,5); room(22,2,8,6);\n    room(1,11,7,7); room(12,11,8,6); room(22,11,8,7);\n    room(11,17,9,6); room(1,23,9,7); room(21,23,9,7);\n    room(2,33,8,5); room(12,32,7,6); room(22,33,8,5);\n    hallV(1,37,10); hallV(1,37,20); hallH(1,29,8); hallH(1,29,20); hallH(1,29,31);\n    hallH(7,12,4); hallH(18,22,4); hallH(7,12,14); hallH(19,22,14);\n    hallH(8,11,26); hallH(19,22,26); hallH(9,12,35); hallH(18,22,35);\n  }else if(level===2){\n    \/\/ Gebouw met hoofdgangen en veel aparte kamers.\n    room(1,1,8,6); room(12,1,7,5); room(22,1,8,6);\n    room(1,10,7,7); room(12,10,7,5); room(22,10,8,7);\n    room(11,16,9,7); room(1,21,8,7); room(22,21,8,7);\n    room(12,24,3,4); room(17,24,3,4); room(1,32,9,6); room(12,31,7,7); room(22,31,8,6);\n    hallV(1,37,10); hallV(1,37,20); hallH(1,29,8); hallH(1,29,19); hallH(1,29,29); hallV(6,15,15); hallV(23,30,15);\n    hallH(8,12,4);hallH(18,22,4);hallH(7,12,13);hallH(18,22,13);hallH(8,12,24);hallH(19,22,24);hallH(9,12,35);hallH(18,22,34);\n  }else if(level===3){\n    \/\/ Zigzag-verdieping met een centrale verticale verkeersas.\n    room(1,1,7,5); room(10,2,9,6); room(23,1,7,7);\n    room(2,10,9,6); room(18,10,11,6);\n    room(1,19,8,7); room(11,18,9,6); room(23,19,7,7);\n    room(2,29,10,8); room(14,29,7,6); room(23,30,7,7);\n    hallV(1,37,15,2); hallH(1,29,8); hallH(1,29,17); hallH(1,29,27); hallH(1,29,37);\n    hallV(5,17,8); hallV(18,29,22); hallV(27,37,12);\n    hallH(7,15,3);hallH(16,23,5);hallH(10,15,13);hallH(16,18,13);hallH(8,15,22);hallH(20,23,22);hallH(11,15,33);hallH(21,23,33);\n  }else if(level===4){\n    \/\/ Open magazijnverdieping met ringgang en grote zalen.\n    room(4,4,9,8); room(18,4,9,8); room(4,26,9,8); room(18,26,9,8);\n    room(11,14,9,10); room(2,15,6,8); room(23,15,6,8);\n    hallH(3,27,2,2); hallH(3,27,35,2); hallV(2,36,2,2); hallV(2,36,27,2);\n    hallH(2,29,13,2); hallH(2,29,24,2); hallV(2,36,15,2);\n    hallH(12,18,8);hallH(12,18,30);hallH(7,11,18);hallH(20,23,18);\n  }else{\n    \/\/ Level 5: beveiligde faciliteit met veel kleinere kamers en kruispunten.\n    room(1,1,6,5); room(9,1,6,6); room(17,1,5,5); room(24,1,6,6);\n    room(1,9,8,6); room(12,9,7,5); room(22,9,8,6);\n    room(1,20,7,6); room(10,19,10,7); room(23,20,7,6);\n    room(1,30,8,8); room(11,30,8,6); room(22,30,8,8);\n    hallV(1,37,7); hallV(1,37,21); hallV(1,37,15); hallH(1,29,7); hallH(1,29,17); hallH(1,29,28);\n    hallV(7,17,26); hallV(17,28,4); hallV(28,37,26);\n    hallH(6,9,3);hallH(14,17,4);hallH(21,24,4);hallH(8,12,12);hallH(18,22,12);hallH(7,10,23);hallH(20,23,23);hallH(8,11,34);hallH(18,22,34);\n  }\n\n  \/\/ Centrale inbrekersbasis blijft in elk ontwerp bereikbaar.\n  const cx=Math.floor(MAZE_W\/2),cy=TUNNEL_ROW;\n  room(cx-3,cy-2,7,5,false);\n  hallH(cx-5,cx+5,cy);\n\n  \/\/ Kleine, deterministische extra deuropeningen geven ieder level nog wat variatie.\n  const candidates=[];\n  for(let y=2;y<MAZE_H-2;y++) for(let x=2;x<MAZE_W-2;x++){\n    if(grid[y][x]!=='#') continue;\n    const horizontal=grid[y][x-1]!=='#'&&grid[y][x+1]!=='#';\n    const vertical=grid[y-1][x]!=='#'&&grid[y+1][x]!=='#';\n    if(horizontal||vertical)candidates.push([x,y]);\n  }\n  shuffle(candidates,rnd).slice(0,Math.min(3+level,candidates.length)).forEach(([x,y])=>floor(x,y));\n\n  for(let y=0;y<MAZE_H;y++) for(let x=0;x<MAZE_W;x++) if(grid[y][x]===' ') grid[y][x]='.';\n  for(let y=cy-2;y<=cy+2;y++) for(let x=cx-3;x<=cx+3;x++) grid[y][x]=' ';\n\n  function nearestFloor(tx,ty){\n    let best=null,bestD=Infinity;\n    for(let y=1;y<MAZE_H-1;y++) for(let x=1;x<MAZE_W-1;x++){\n      if(grid[y][x]==='#') continue;\n      const d=Math.abs(x-tx)+Math.abs(y-ty);\n      if(d<bestD){bestD=d;best={x,y};}\n    }\n    return best;\n  }\n\n  \/\/ Vanaf Level 2 krijgt ELKE grote kamer een geel camerabolletje.\n  \/\/ 'Groot' = minimaal 45 vloertegels (bv. 8\u00d76, 7\u00d77, 9\u00d75, ...).\n  \/\/ Kleine kamers, technische hokjes en de centrale inbrekersbasis tellen niet mee.\n  \/\/ Zodra het cameramaximum bereikt is, verwijdert removeUnusedCameraOrbs()\n  \/\/ de resterende bolletjes zoals afgesproken.\n  if(level>=2){\n    orbSeeds=declaredRooms\n      .filter(r=>r.w*r.h>=45)\n      .map(r=>[\n        Math.round(r.x+(r.w-1)\/2),\n        Math.round(r.y+(r.h-1)\/2)\n      ]);\n\n    for(const [sx,sy] of orbSeeds){\n      const p=nearestFloor(sx,sy);\n      if(p && grid[p.y][p.x]!=='#') grid[p.y][p.x]='o';\n    }\n  }\n\n  \/\/ Bewaar de echte kamergrenzen zodat nieuwe inbrekers nooit hoeven te\n  \/\/ verschijnen in een kamer waar op dat moment een camera staat.\n  state.levelRooms=declaredRooms.map((r,index)=>({...r,index}));\n  return grid;\n}\n\nfunction isPassable(col,row){\n  if(row===TUNNEL_ROW && (col<0 || col>=MAZE_W)) return true;\n  return row>=0 && row<MAZE_H && col>=0 && col<MAZE_W && state.map[row][col]!=='#';\n}\nfunction canStep(col,row,dir){ return isPassable(col+dir.x,row+dir.y); }\nfunction applyWrap(actor){\n  if(Math.round(actor.y)!==TUNNEL_ROW) return;\n  if(actor.x>=MAZE_W) actor.x=0;\n  if(actor.x<=-1) actor.x=MAZE_W-1;\n}\nfunction nearestPassable(tx,ty){\n  let best={x:1,y:1}, bestD=Infinity;\n  for(let y=0;y<MAZE_H;y++) for(let x=0;x<MAZE_W;x++){\n    if(!isPassable(x,y)) continue;\n    const d=Math.abs(x-tx)+Math.abs(y-ty);\n    if(d<bestD){bestD=d;best={x,y};}\n  }\n  return best;\n}\nfunction chooseInitialDir(p){\n  const prefs=[DIRS.right,DIRS.up,DIRS.left,DIRS.down];\n  return prefs.find(d=>canStep(p.x,p.y,d)) || DIRS.none;\n}\n\nfunction roomContainsPoint(room,x,y,pad=0){\n  return !!room &&\n    x>=room.x-pad && x<=room.x+room.w-1+pad &&\n    y>=room.y-pad && y<=room.y+room.h-1+pad;\n}\n\nfunction roomHasActiveCamera(room){\n  return (state.cameras||[]).some(cam=>cam&&cam.alive!==false&&roomContainsPoint(room,cam.x,cam.y,.08));\n}\n\nfunction safestTileInsideRoom(room){\n  if(!room) return null;\n  const cameras=(state.cameras||[]).filter(c=>c&&c.alive!==false);\n  const candidates=[];\n\n  for(let y=room.y;y<room.y+room.h;y++){\n    for(let x=room.x;x<room.x+room.w;x++){\n      if(!isPassable(x,y)) continue;\n      \/\/ Niet rechtstreeks op een camerabol of diamant laten verschijnen.\n      if(state.map[y]?.[x]==='o') continue;\n      if(state.diamond && Math.hypot(x-state.diamond.x,y-state.diamond.y)<1.25) continue;\n\n      let nearestCam=999;\n      for(const cam of cameras) nearestCam=Math.min(nearestCam,Math.hypot(x-cam.x,y-cam.y));\n      candidates.push({x,y,nearestCam});\n    }\n  }\n\n  if(!candidates.length) return null;\n  candidates.sort((a,b)=>b.nearestCam-a.nearestCam);\n  return candidates[0];\n}\n\nfunction chooseSafeBurglarSpawn(b=null,index=0){\n  const allRooms=(state.levelRooms||[]).filter(r=>r&&r.w>=4&&r.h>=4);\n  if(!allRooms.length){\n    const cx=Math.floor(MAZE_W\/2),cy=TUNNEL_ROW;\n    return nearestPassable(cx,cy);\n  }\n\n  \/\/ Harde regel: eerst ALLE kamers uitsluiten waarin een actieve camera staat.\n  let rooms=allRooms.filter(r=>!roomHasActiveCamera(r));\n\n  \/\/ Met de huidige levels blijven altijd meerdere kamers over. Dit is alleen\n  \/\/ een defensieve fallback voor toekomstige extreem kleine plattegronden.\n  if(!rooms.length){\n    rooms=[...allRooms].sort((a,b)=>{\n      const da=Math.min(...(state.cameras||[]).filter(c=>c.alive!==false).map(c=>Math.hypot((a.x+a.w\/2)-c.x,(a.y+a.h\/2)-c.y)),999);\n      const db=Math.min(...(state.cameras||[]).filter(c=>c.alive!==false).map(c=>Math.hypot((b.x+b.w\/2)-c.x,(b.y+b.h\/2)-c.y)),999);\n      return db-da;\n    }).slice(0,1);\n  }\n\n  \/\/ Als er keuze is, niet opnieuw dezelfde kamer gebruiken als bij de vorige spawn.\n  if(b && Number.isInteger(b.lastSpawnRoom) && rooms.length>1){\n    const other=rooms.filter(r=>r.index!==b.lastSpawnRoom);\n    if(other.length) rooms=other;\n  }\n\n  \/\/ Spreid opeenvolgende nieuwe inbrekers over verschillende veilige kamers.\n  const serial=state.burglarSpawnSerial||0;\n  state.burglarSpawnSerial=serial+1;\n  const pick=rooms[(serial+index)%rooms.length];\n  const tile=safestTileInsideRoom(pick) ||\n             nearestPassable(Math.round(pick.x+(pick.w-1)\/2),Math.round(pick.y+(pick.h-1)\/2));\n\n  return {...tile,roomIndex:pick.index};\n}\n\nfunction placeBurglarInSafeRoom(b){\n  const p=chooseSafeBurglarSpawn(b,b?.index||0);\n  if(!p) return false;\n  b.x=p.x;b.y=p.y;\n  b.home={x:p.x,y:p.y};\n  b.lastSpawnRoom=Number.isInteger(p.roomIndex)?p.roomIndex:null;\n  const d=chooseInitialDir(p);\n  b.dir=d===DIRS.none?DIRS.left:d;\n  b.desired=b.dir;\n  b.stun=0;\n  b.flashed=false;\n  b.flashRecover=0;\n  b.flashOwner=null;\n  b.pendingRespawn=false;\n  return true;\n}\n\nfunction queueBurglarRespawn(b,delay=.85){\n  if(!b) return;\n  b.respawn=Math.max(.05,delay);\n  b.pendingRespawn=true;\n  b.dir=DIRS.none;\n  b.desired=DIRS.none;\n  b.stun=0;\n  b.flashed=false;\n  b.flashRecover=0;\n  b.flashOwner=null;\n}\n\nfunction makeBurglar(index){\n  const colors=['#ff4d6d','#ff9f1c','#c77dff','#35e8a3','#20d8ff','#ffd166'];\n  const b={x:0,y:0,home:{x:0,y:0},dir:DIRS.none,desired:DIRS.none,color:colors[index%colors.length],index,stun:0,flashed:false,flashRecover:0,flashOwner:null,respawn:0,pendingRespawn:false,lastSpawnRoom:null};\n  placeBurglarInSafeRoom(b);\n  return b;\n}\n\nconst CAMERA_COLORS=['#8dff75','#20d8ff','#ff9f1c','#c77dff','#ff5f8f','#ffd166','#35e8a3','#6ea8ff','#ff7b54','#f4a7ff','#7df9ff','#f7ff6b'];\n\nfunction cameraColor(index){\n  if(index<CAMERA_COLORS.length) return CAMERA_COLORS[index];\n  return `hsl(${(index*137.508)%360} 88% 67%)`;\n}\n\nfunction makeCamera(x,y,index=0,facing=null){\n  const d=facing&&facing!==DIRS.none?facing:chooseInitialDir({x:Math.round(x),y:Math.round(y)});\n  const cam={\n    id:`cam-${index+1}`,\n    cameraIndex:index,\n    x,y,\n    dir:DIRS.none,\n    desired:DIRS.none,\n    facing:d===DIRS.none?DIRS.right:d,\n    visualAngle:facingAngle(d===DIRS.none?DIRS.right:d),\n    route:[],\n    color:cameraColor(index),\n    footCount:3+index,\n    autoSurveillance:false,\n    autoAngle:0,\n    autoSweepDir:1,\n    autoWallSweep:false,\n    autoWallNormals:[],\n    autoHuntTarget:null,\n    backgroundTransit:false,\n    alive:true\n  };\n  createCameraFeet(cam);\n  return cam;\n}\n\n\/\/ De 3D-monitor ligt rechts-onder bovenop het 2D-speelveld. Bij levelstart of het\n\/\/ ontstaan van een nieuwe camera vermijden we daarom startcellen die, door de clamping\n\/\/ aan de wereldrand, precies achter die monitor zouden vallen.\nfunction cameraFocusedScreenPoint(x,y){\n  const px=(x+.5)*TILE,py=(y+.5)*TILE;\n  const vx=clamp(px-VIEW_W\/2,0,Math.max(0,WORLD_W-VIEW_W));\n  const vy=clamp(py-VIEW_H\/2,0,Math.max(0,WORLD_H-VIEW_H));\n  return {x:px-vx,y:py-vy};\n}\nfunction cameraWouldStartBehindMonitor(x,y){\n  const p=cameraFocusedScreenPoint(x,y);\n  \/\/ Conservatiever dan de CSS-monitor zelf, zodat ook mobiel\/fullscreen veilig blijft.\n  return p.x>=VIEW_W*.49 && p.y>=VIEW_H*.55;\n}\nfunction safeCameraStartCell(tx,ty,occupiedSet=null){\n  const occupied=occupiedSet||new Set((state.cameras||[]).filter(c=>c&&c.alive!==false).map(c=>`${Math.round(c.x)},${Math.round(c.y)}`));\n  const base=nearestPassable(tx,ty);\n  if(base && !occupied.has(`${base.x},${base.y}`) && !cameraWouldStartBehindMonitor(base.x,base.y)) return base;\n  const candidates=[];\n  for(let y=0;y<MAZE_H;y++) for(let x=0;x<MAZE_W;x++){\n    if(!isPassable(x,y) || occupied.has(`${x},${y}`) || cameraWouldStartBehindMonitor(x,y)) continue;\n    candidates.push({x,y,d:Math.abs(x-tx)+Math.abs(y-ty)});\n  }\n  candidates.sort((a,b)=>a.d-b.d || a.y-b.y || a.x-b.x);\n  return candidates[0]||base||nearestPassable(tx,ty);\n}\n\nfunction cameraSpawnCellNear(x,y){\n  const occupied=new Set((state.cameras||[]).map(c=>`${Math.round(c.x)},${Math.round(c.y)}`));\n  const candidates=[];\n  for(let r=1;r<=6;r++){\n    for(let oy=-r;oy<=r;oy++) for(let ox=-r;ox<=r;ox++){\n      if(Math.abs(ox)+Math.abs(oy)!==r) continue;\n      const cx=x+ox, cy=y+oy;\n      if(cx<0||cx>=MAZE_W||cy<0||cy>=MAZE_H||!isPassable(cx,cy)) continue;\n      if(occupied.has(`${cx},${cy}`)) continue;\n      if(state.diamond && cx===state.diamond.x && cy===state.diamond.y) continue;\n      if(cameraWouldStartBehindMonitor(cx,cy)) continue;\n      candidates.push({x:cx,y:cy});\n    }\n    if(candidates.length) break;\n  }\n  return candidates[0]||safeCameraStartCell(x,y,occupied);\n}\n\nfunction removeUnusedCameraOrbs(){\n  if(orbCanCreateCamera()) return;\n  for(let y=0;y<MAZE_H;y++) for(let x=0;x<MAZE_W;x++){\n    if(state.map[y]?.[x]==='o'){\n      state.map[y][x]=' ';\n      state.dots=Math.max(0,state.dots-1);\n    }\n  }\n}\n\nfunction orbCanCreateCamera(){\n  return state.level>=2 && (state.cameras?.length||0)<levelConfig().maxCams;\n}\n\nfunction spawnCameraFromOrb(x,y){\n  if(state.level<2 || state.cameras.length>=levelConfig().maxCams) return null;\n  const index=state.cameras.length;\n  const spot=cameraSpawnCellNear(x,y);\n  const cam=makeCamera(spot.x,spot.y,index);\n  state.cameras.push(cam);\n  state.banner=state.lang==='fr'?`NOUVELLE CAM\u00c9RA \u2022 ${cam.footCount} PATTES`:state.lang==='en'?`NEW CAMERA \u2022 ${cam.footCount} LEGS`:`NIEUWE CAMERA \u2022 ${cam.footCount} BENEN`;\n  state.bannerTime=1.15;\n  statusLine.textContent=state.lang==='fr'\n    ?`Une nouvelle cam\u00e9ra ${index+1} est n\u00e9e. Cliquez dessus pour la contr\u00f4ler.`\n    :state.lang==='en'\n      ?`A new camera ${index+1} was born. Click it to take control.`\n      :`Nieuwe camera ${index+1} geboren. Klik erop om de bediening over te nemen.`;\n  sfx('bonus');\n  removeUnusedCameraOrbs();\n  updateHUD();\n  return cam;\n}\n\nfunction stopActiveCameraMotion(){\n  const p=state.player;\n  if(!p) return;\n  p.route=[];\n  p.dir=DIRS.none;\n  p.desired=DIRS.none;\n  state.manualMode=false;\n  state.huntTarget=null;\n  state.navTarget=null;\n  state.surveillanceReturn=false;\n}\n\n\nfunction backgroundAngleTouchesWall(cam,angle){\n  if(!cam?.autoWallSweep || !cam.autoWallNormals?.length) return false;\n  const dx=Math.cos(angle),dy=Math.sin(angle),maxBeamTiles=9.2;\n  for(const wall of cam.autoWallNormals){\n    const toward=dx*wall.x+dy*wall.y;\n    if(toward<=.0001) continue;\n    if(.5\/toward<=maxBeamTiles) return true;\n  }\n  return false;\n}\n\nfunction nearestSafeBackgroundAngle(cam,angle){\n  angle=normalizeAngle(angle);\n  if(!backgroundAngleTouchesWall(cam,angle)) return angle;\n  const step=Math.PI\/180;\n  for(let i=1;i<=180;i++){\n    const plus=normalizeAngle(angle+i*step),minus=normalizeAngle(angle-i*step);\n    const ps=!backgroundAngleTouchesWall(cam,plus),ms=!backgroundAngleTouchesWall(cam,minus);\n    if(ps&&ms) return cam.autoSweepDir>=0?plus:minus;\n    if(ps) return plus;\n    if(ms) return minus;\n  }\n  return angle;\n}\n\nfunction startBackgroundSurveillance(cam){\n  if(!cam || cam===state.player) return;\n  cam.backgroundTransit=false;\n  const cell=nearestPassable(Math.round(cam.x),Math.round(cam.y));\n  cam.x=cell.x;cam.y=cell.y;cam.dir=DIRS.none;cam.desired=DIRS.none;cam.route=[];\n  cam.autoHuntTarget=null;\n  cam.autoSurveillance=true;\n  cam.autoAngle=Number.isFinite(cam.visualAngle)?cam.visualAngle:facingAngle(cam.facing||DIRS.right);\n  cam.autoSweepDir=1;\n  cam.autoWallNormals=nearbySurveillanceWalls(cam);\n  cam.autoWallSweep=cam.autoWallNormals.length>0;\n  if(cam.autoWallSweep){\n    cam.autoAngle=nearestSafeBackgroundAngle(cam,cam.autoAngle);\n    const probe=.045;\n    const plusBlocked=backgroundAngleTouchesWall(cam,normalizeAngle(cam.autoAngle+probe));\n    const minusBlocked=backgroundAngleTouchesWall(cam,normalizeAngle(cam.autoAngle-probe));\n    if(plusBlocked&&!minusBlocked) cam.autoSweepDir=-1;\n    else if(minusBlocked&&!plusBlocked) cam.autoSweepDir=1;\n  }\n  retargetCameraFeet(cam,true);\n}\n\nfunction stopBackgroundSurveillance(cam,preserveMotion=false){\n  if(!cam) return;\n  cam.autoSurveillance=false;\n  cam.autoHuntTarget=null;\n  cam.autoWallSweep=false;\n  cam.autoWallNormals=[];\n  if(!preserveMotion){\n    cam.backgroundTransit=false;\n    cam.route=[];\n    cam.dir=DIRS.none;\n    cam.desired=DIRS.none;\n    cam.facing=nearestCardinalFromAngle(cam.autoAngle||facingAngle(cam.facing||DIRS.right));\n  }\n}\n\nfunction burglarVisibleForBackgroundCamera(cam,b){\n  if(!cam?.autoSurveillance || !b || b.respawn>0 || b.flashed) return false;\n  const fx=Math.cos(cam.autoAngle),fy=Math.sin(cam.autoAngle);\n  const vx=b.x-cam.x,vy=b.y-cam.y,r=Math.hypot(vx,vy);\n  if(r<.12||r>9.2) return false;\n  const dot=(vx\/r)*fx+(vy\/r)*fy;\n  return dot>=Math.cos(.20) && lineClear(cam.x,cam.y,b.x,b.y);\n}\n\n\nfunction burglarVisibleForBackgroundTransit(cam,b){\n  if(!cam?.backgroundTransit || !b || b.respawn>0 || b.flashed) return false;\n  const a=facingAngle(cam.facing||DIRS.right);\n  const fx=Math.cos(a),fy=Math.sin(a);\n  const vx=b.x-cam.x,vy=b.y-cam.y,r=Math.hypot(vx,vy);\n  if(r<.12||r>7.2) return false;\n  const dot=(vx\/r)*fx+(vy\/r)*fy;\n  return dot>=.78 && lineClear(cam.x,cam.y,b.x,b.y);\n}\n\nfunction updateBackgroundTransitFlash(cam){\n  if(!cam?.backgroundTransit) return false;\n  const visible=state.burglars.filter(b=>burglarVisibleForBackgroundTransit(cam,b));\n  if(!visible.length) return false;\n\n  let hits=0;\n  for(const b of visible){\n    if(markBurglarFlashed(b,cam)){\n      state.score+=125;\n      registerSuccessfulFlash();\n      hits++;\n      sfx('stun');\n    }\n  }\n  if(hits){\n    state.banner=`AUTO FLASH \u00d7${hits}`;\n    state.bannerTime=.55;\n    state.shake=.08;\n    cam.backgroundTransit=false;\n    if(startBackgroundHunt(cam)){\n      const target=state.burglars.find(b=>b.flashed&&b.respawn<=0&&b.flashOwner===cam.id)||null;\n      beginCctvEventCut(cam,target);\n    }\n    return true;\n  }\n  return false;\n}\n\nfunction beginCctvEventCut(cam,b=null){\n  if(!cam || cam===state.player || cam.alive===false) return;\n  state.cctvEventCamId=cam.id;\n  state.cctvEventReturnAt=0;\n  state.cctvEventReason='INTERCEPT';\n  state.cctvEventTarget=b?.index??cam.autoHuntTarget??null;\n}\nfunction finishCctvEventCut(cam,holdMs=620){\n  if(!cam || state.cctvEventCamId!==cam.id) return;\n  state.cctvEventReturnAt=performance.now()+Math.max(0,holdMs);\n}\nfunction clearCctvEventCut(){\n  state.cctvEventCamId=null;state.cctvEventReturnAt=0;state.cctvEventReason='';state.cctvEventTarget=null;\n}\nfunction cctvDisplayCamera(now=performance.now()){\n  if(!state.cctvEventCamId) return state.player;\n  const cam=(state.cameras||[]).find(c=>c&&c.id===state.cctvEventCamId&&c.alive!==false);\n  if(!cam){clearCctvEventCut();return state.player;}\n  if(state.cctvEventReturnAt>0 && now>=state.cctvEventReturnAt){clearCctvEventCut();return state.player;}\n  \/\/ Als een onderschepping onverwacht wordt afgebroken, niet op een achtergrondcamera blijven hangen.\n  if(state.cctvEventReturnAt<=0 && cam.autoHuntTarget===null && !cam.backgroundTransit){\n    state.cctvEventReturnAt=now+260;\n  }\n  return cam;\n}\n\nfunction setBackgroundHuntRoute(cam,b){\n  if(!cam||!b||!b.flashed||b.respawn>0) return false;\n  const start=nearestPassable(Math.round(cam.x),Math.round(cam.y));\n  const target=nearestPassable(Math.round(b.x),Math.round(b.y));\n  const path=findNavigationPath(start.x,start.y,target.x,target.y);\n  if(!path.length) return false;\n  cam.route=path;cam.dir=DIRS.none;cam.desired=DIRS.none;\n  cam.backgroundTransit=false;\n  cam.autoHuntTarget=b.index;cam.autoSurveillance=false;\n  return true;\n}\n\nfunction startBackgroundHunt(cam){\n  const owned=state.burglars\n    .filter(b=>b.flashed&&b.respawn<=0&&b.flashOwner===cam.id)\n    .sort((a,b)=>shortestMazeDistance(cam.x,cam.y,a.x,a.y)-shortestMazeDistance(cam.x,cam.y,b.x,b.y));\n  if(!owned.length){startBackgroundSurveillance(cam);return false;}\n  return setBackgroundHuntRoute(cam,owned[0]);\n}\n\nfunction updateBackgroundRoute(cam,dt){\n  if(!Array.isArray(cam.route)||!cam.route.length) return;\n  while(cam.route.length){\n    const node=cam.route[0];\n    if(Math.abs(cam.x-node.x)<.025&&Math.abs(cam.y-node.y)<.025){\n      cam.x=node.x;cam.y=node.y;cam.route.shift();\n    }else break;\n  }\n  if(!cam.route.length){cam.dir=DIRS.none;cam.desired=DIRS.none;return;}\n  const next=cam.route[0],dx=next.x-cam.x,dy=next.y-cam.y;\n  let d=DIRS.none;\n  if(Math.abs(dx)>.025)d=dx>0?DIRS.right:DIRS.left;\n  else if(Math.abs(dy)>.025)d=dy>0?DIRS.down:DIRS.up;\n  cam.desired=d;\n  if(cam.dir===DIRS.none||reverseOf(d,cam.dir)){cam.dir=d;cam.facing=d;}\n  moveActor(cam,4.55,dt,true);\n}\n\nfunction captureByBackgroundCamera(cam,b){\n  if(!cam||!b||!b.flashed||b.respawn>0) return false;\n  const gain=250;state.score+=gain;\n  b.flashed=false;b.flashRecover=0;b.flashOwner=null;b.stun=0;\n  queueBurglarRespawn(b,.85);\n  cam.route=[];cam.dir=DIRS.none;cam.desired=DIRS.none;cam.autoHuntTarget=null;\n  state.banner=`INTRUDER EATEN +${gain}`;state.bannerTime=.75;state.shake=.11;sfx('capture');\n  registerEatenBurglar();\n  \/\/ Laat de vangst nog heel even in beeld en keer daarna terug naar de camera die de speler zelf in focus heeft.\n  finishCctvEventCut(cam,650);\n  if(state.phase!=='playing') return true;\n  startBackgroundHunt(cam);\n  return true;\n}\n\nfunction updateBackgroundCamera(cam,dt){\n  if(!cam||cam.alive===false||cam===state.player||state.phase!=='playing') return;\n  if(cam.autoHuntTarget!==null){\n    const b=state.burglars[cam.autoHuntTarget];\n    if(!b||!b.flashed||b.respawn>0||b.flashOwner!==cam.id){cam.autoHuntTarget=null;startBackgroundHunt(cam);return;}\n    if(dist(cam,b)<.54){captureByBackgroundCamera(cam,b);return;}\n    if(!cam.route.length&&!setBackgroundHuntRoute(cam,b)){startBackgroundSurveillance(cam);return;}\n    updateBackgroundRoute(cam,dt);\n    if(dist(cam,b)<.54) captureByBackgroundCamera(cam,b);\n    return;\n  }\n\n  \/\/ Een camera die onderweg was toen de speler naar een andere camera\n  \/\/ schakelde, werkt zijn route volledig zelfstandig af.\n  if(cam.backgroundTransit){\n    if(cam.route.length){\n      updateBackgroundRoute(cam,dt);\n      if(updateBackgroundTransitFlash(cam)) return;\n    }\n    if(!cam.route.length){\n      cam.backgroundTransit=false;\n      cam.dir=DIRS.none;\n      cam.desired=DIRS.none;\n      if(!startBackgroundHunt(cam)) startBackgroundSurveillance(cam);\n    }\n    return;\n  }\n\n  if(!cam.autoSurveillance){startBackgroundSurveillance(cam);return;}\n  const turnStep=dt*1.18;\n  if(cam.autoWallSweep){\n    let candidate=normalizeAngle(cam.autoAngle+turnStep*cam.autoSweepDir);\n    if(backgroundAngleTouchesWall(cam,candidate)){\n      cam.autoSweepDir*=-1;\n      candidate=normalizeAngle(cam.autoAngle+turnStep*cam.autoSweepDir);\n      if(backgroundAngleTouchesWall(cam,candidate))candidate=nearestSafeBackgroundAngle(cam,cam.autoAngle);\n    }\n    cam.autoAngle=candidate;\n  }else cam.autoAngle=normalizeAngle(cam.autoAngle+turnStep);\n\n  const visible=state.burglars.filter(b=>burglarVisibleForBackgroundCamera(cam,b));\n  if(!visible.length)return;\n  let hits=0;\n  for(const b of visible){\n    if(markBurglarFlashed(b,cam)){\n      state.score+=175;registerSuccessfulFlash();hits++;sfx('stun');\n    }\n  }\n  if(hits){\n    state.banner=`AUTO FLASH \u00d7${hits}`;state.bannerTime=.65;state.shake=.10;sfx('flash');\n    if(startBackgroundHunt(cam)){\n      const target=state.burglars.find(b=>b.flashed&&b.respawn<=0&&b.flashOwner===cam.id)||null;\n      beginCctvEventCut(cam,target);\n    }\n  }\n}\n\nfunction selectCamera(cam){\n  if(!cam || cam===state.player || !state.cameras.includes(cam)) return false;\n\n  const previous=state.player;\n  const selectedWasTransit=!!(cam.backgroundTransit && Array.isArray(cam.route) && cam.route.length);\n\n  \/\/ Stop alleen de globale actieve surveillance-status. De fysieke route\n  \/\/ van de vorige camera wordt NIET gewist.\n  if(state.sentry||state.sentryPending) deactivateSentry();\n\n  if(previous && previous!==cam){\n    const hasOwnedFlashed=state.burglars.some(b=>b.flashed&&b.respawn<=0&&b.flashOwner===previous.id);\n\n    if(hasOwnedFlashed){\n      \/\/ Zelfde gedrag als voordien: eigen geflashte doelwitten eerst afwerken.\n      startBackgroundHunt(previous);\n    }else if(Array.isArray(previous.route) && previous.route.length){\n      \/\/ Belangrijk: camerawissel heeft geen invloed op de gekozen bestemming.\n      previous.backgroundTransit=true;\n      previous.autoSurveillance=false;\n      previous.autoHuntTarget=null;\n      previous.autoWallSweep=false;\n      previous.autoWallNormals=[];\n    }else{\n      startBackgroundSurveillance(previous);\n    }\n  }\n\n  \/\/ Als de nieuw geselecteerde camera zelf nog onderweg was, blijft zijn route\n  \/\/ intact en wordt die nu gewoon als actieve route verder gereden.\n  stopBackgroundSurveillance(cam,selectedWasTransit);\n  state.player=cam;\n  state.manualMode=false;\n  state.huntTarget=null;\n  state.flashHeld=false;\n  state.flashFx=selectedWasTransit?1:0;\n  state.flashCooldown=selectedWasTransit?0:Math.min(state.flashCooldown,.35);\n  state.surveillanceReturn=false;\n  state.viewPanX=0;\n  state.viewPanY=0;\n\n  if(selectedWasTransit){\n    cam.backgroundTransit=false;\n    state.movementFlashActive=true;\n    state.stationaryTimer=0;\n    const end=cam.route[cam.route.length-1];\n    state.navTarget=end?{x:end.x,y:end.y}:null;\n  }else{\n    state.movementFlashActive=false;\n    state.stationaryTimer=0;\n    state.navTarget=null;\n    cam.route=[];\n    cam.dir=DIRS.none;\n    cam.desired=DIRS.none;\n  }\n\n  resetNavigationWatch();\n  centerView(true);\n  state.banner=state.lang==='fr'?`CAM\u00c9RA ${cam.cameraIndex+1} ACTIVE`:state.lang==='en'?`CAMERA ${cam.cameraIndex+1} ACTIVE`:`CAMERA ${cam.cameraIndex+1} ACTIEF`;\n  state.bannerTime=.7;\n\n  statusLine.textContent=selectedWasTransit\n    ?(state.lang==='fr'\n      ?`Cam\u00e9ra ${cam.cameraIndex+1} s\u00e9lectionn\u00e9e \u2014 elle poursuit sa destination.`\n      :state.lang==='en'\n        ?`Camera ${cam.cameraIndex+1} selected \u2014 continuing its destination.`\n        :`Camera ${cam.cameraIndex+1} geselecteerd \u2014 bestaande route loopt verder.`)\n    :(state.lang==='fr'\n      ?`Cam\u00e9ra ${cam.cameraIndex+1} s\u00e9lectionn\u00e9e \u2014 surveillance automatique \u00e0 l\u2019arr\u00eat.`\n      :state.lang==='en'\n        ?`Camera ${cam.cameraIndex+1} selected \u2014 automatic surveillance while stationary.`\n        :`Camera ${cam.cameraIndex+1} geselecteerd \u2014 automatische Surveillance bij stilstand.`);\n\n  if(state.phase==='playing' && !selectedWasTransit) enterAutomaticSurveillance();\n  updateHUD();\n  return true;\n}\n\n\nfunction cycleToNextActiveCamera(){\n  const active=(state.cameras||[]).filter(cam=>cam&&cam.alive!==false);\n  if(!active.length) return false;\n\n  if(active.length===1){\n    state.player=active[0];\n    state.viewPanX=0;\n    state.viewPanY=0;\n    centerView(true);\n    state.banner=state.lang==='fr'?'CAM\u00c9RA 1':state.lang==='en'?'CAMERA 1':'CAMERA 1';\n    state.bannerTime=.35;\n    return true;\n  }\n\n  let currentIndex=active.indexOf(state.player);\n  if(currentIndex<0) currentIndex=0;\n  const next=active[(currentIndex+1)%active.length];\n\n  if(next===state.player){\n    state.viewPanX=0;\n    state.viewPanY=0;\n    centerView(true);\n    return true;\n  }\n\n  selectCamera(next);\n  state.viewPanX=0;\n  state.viewPanY=0;\n  centerView(true);\n\n  showCompactPopup(\n    state.lang==='fr'?`CAM\u00c9RA ${next.cameraIndex+1}`:state.lang==='en'?`CAMERA ${next.cameraIndex+1}`:`CAMERA ${next.cameraIndex+1}`,\n    state.lang==='fr'?'ACTIVE':state.lang==='en'?'ACTIVE':'ACTIEF',\n    650\n  );\n  return true;\n}\n\nfunction cameraAtWorldPoint(wx,wy,hitRadius=.72){\n  let best=null,bestD=hitRadius;\n  for(const cam of state.cameras||[]){\n    if(cam.alive===false) continue;\n    const d=Math.hypot(cam.x-wx,cam.y-wy);\n    if(d<=bestD){best=cam;bestD=d;}\n  }\n  return best;\n}\n\nfunction createCameraFeet(player){\n  const count=Math.max(3,Math.floor(player.footCount||3));\n  player.footCount=count;\n  player.feet=Array.from({length:count},(_,index)=>({\n    side:index%2===0?-1:1,\n    phase:index,\n    anchor:null,from:null,to:null,t:1\n  }));\n  player.footStep=0;\n  player.lastFootTile=`${Math.round(player.x)},${Math.round(player.y)}`;\n  retargetCameraFeet(player,true);\n}\n\nfunction localFrameForPlayer(player){\n  const f=player.facing&&player.facing!==DIRS.none?player.facing:DIRS.right;\n  return {f,perp:{x:-f.y,y:f.x}};\n}\n\nfunction wallAnchorCandidates(player,side){\n  const {f,perp}=localFrameForPlayer(player);\n  const px=Math.round(player.x), py=Math.round(player.y);\n  const out=[];\n  for(let oy=-3;oy<=3;oy++) for(let ox=-3;ox<=3;ox++){\n    const x=px+ox,y=py+oy;\n    if(y<0||y>=MAZE_H||x<0||x>=MAZE_W||state.map[y][x]!=='#') continue;\n    const vx=x-player.x,vy=y-player.y;\n    const sideScore=vx*perp.x+vy*perp.y;\n    const forward=vx*f.x+vy*f.y;\n    if(side<0 && sideScore>-.15) continue;\n    if(side>0 && sideScore<.15) continue;\n    const d=Math.hypot(vx,vy);\n    if(d>.15&&d<3.6) out.push({x,y,score:d+Math.abs(forward)*.16-Math.abs(sideScore)*.08});\n  }\n  out.sort((a,b)=>a.score-b.score);\n  return out;\n}\n\nfunction fallbackFootAnchor(player,side,index){\n  const {f,perp}=localFrameForPlayer(player);\n  const reach=1.02+Math.min(.55,index*.09);\n  return {\n    x:player.x+perp.x*side*reach-f.x*.15,\n    y:player.y+perp.y*side*reach-f.y*.15,\n    synthetic:true\n  };\n}\n\nfunction retargetCameraFeet(player,immediate=false){\n  if(!player.feet) return;\n  const used=new Set();\n  player.feet.forEach((foot,index)=>{\n    const preferredSide=(index===player.feet.length-1 && player.feet.length%2===1)?-((player.footStep%2)?1:-1):foot.side;\n    let candidates=wallAnchorCandidates(player,preferredSide);\n    if(!candidates.length) candidates=wallAnchorCandidates(player,-preferredSide);\n    let next=candidates.find(c=>!used.has(`${c.x},${c.y}`));\n    if(next) used.add(`${next.x},${next.y}`);\n    else next=fallbackFootAnchor(player,preferredSide,index);\n    const current=foot.anchor||next;\n    foot.from={x:current.x,y:current.y,synthetic:current.synthetic};\n    foot.to={x:next.x,y:next.y,synthetic:next.synthetic};\n    foot.anchor=immediate?{...next}:{...current};\n    foot.t=immediate?1:0;\n  });\n}\n\nfunction updateCameraFeet(player,dt){\n  if(!player?.feet) return;\n  const tileKey=`${Math.round(player.x)},${Math.round(player.y)}`;\n  if(tileKey!==player.lastFootTile && Math.abs(player.x-Math.round(player.x))<.12 && Math.abs(player.y-Math.round(player.y))<.12){\n    player.lastFootTile=tileKey; player.footStep++; retargetCameraFeet(player,false);\n  }\n  player.feet.forEach((foot,index)=>{\n    if(foot.t>=1) return;\n    \/\/ Elk voetje beweegt net na elkaar; extra camera's behouden dezelfde klik-gang met meer poten.\n    const stagger=index*.075;\n    foot.t=clamp(foot.t+dt*4.4-stagger*dt,0,1);\n    const t=foot.t<.5?2*foot.t*foot.t:1-Math.pow(-2*foot.t+2,2)\/2;\n    foot.anchor={x:foot.from.x+(foot.to.x-foot.from.x)*t,y:foot.from.y+(foot.to.y-foot.from.y)*t,synthetic:foot.to.synthetic};\n  });\n}\n\nfunction loadLevel(first=false){\n  state.levelSeed=24681357+state.level*7919;\n  state.map=generateMaze(state.level);\n  const cfg=levelConfig();\n  const spawn=safeCameraStartCell(cfg.spawn[0],cfg.spawn[1],new Set());\n  state.map[spawn.y][spawn.x]=' ';\n  const firstDir=chooseInitialDir(spawn);\n  state.player=makeCamera(spawn.x,spawn.y,0,firstDir);\n  state.cameras=[state.player];\n  state.burglarSpawnSerial=0;\n  state.burglars=Array.from({length:cfg.burglarCount},(_,i)=>makeBurglar(i));\n  const diamondCell=nearestPassable(cfg.diamond[0],cfg.diamond[1]);\n  const resolvedDiamondRoom=(state.levelRooms||[]).find(r=>roomContainsPoint(r,diamondCell.x,diamondCell.y,.01)) || null;\n  state.diamondRoom=resolvedDiamondRoom ? {...resolvedDiamondRoom} : null;\n  state.diamond={x:diamondCell.x,y:diamondCell.y,found:!!cfg.diamondFound,stolen:false,pulse:0,roomIndex:resolvedDiamondRoom?.index??null};\n  state.map[diamondCell.y][diamondCell.x]=' ';\n  state.diamondThreat=!!cfg.diamondFound;\n  state.dots=0;\n  for(const row of state.map) for(const cell of row) if(cell==='.' || cell==='o') state.dots++;\n  state.initialDots=state.dots; state.collected=0; state.exposed=0; state.exposedCombo=0;\n  state.flashCount=0; state.eatenCount=0; state.eatGoal=cfg.eatGoal; state.diamondGuardTime=60; state.diamondGuardActive=false; state.diamondIntruderTime=60; state.diamondIntruderActive=false; state.diamondPressureShown=0; state.winReason=null;\n  state.flashCooldown=first?0:.8; state.flashFx=0; state.invuln=1.5;\n  state.modeTimer=18; state.chaseMode=true; state.sentry=false; state.sentryPending=false; state.sentryTurnTimer=0; state.sentryHold=0; state.sentryDetected=false; state.sentryScanPulse=0; state.sentryResumeDir=null; state.surveillanceReturn=false; state.surveillanceSweepDir=1; state.surveillanceWallSweep=false; state.surveillanceWallNormals=[]; state.manualMode=false; state.movementFlashActive=false; state.stationaryTimer=0; state.flashHeld=false; state.flashTurnCooldown=0; state.flashResumeDir=null; state.flashResumeDesired=null; state.flashHoldTagged=Array(cfg.burglarCount).fill(false); state.huntTarget=null; state.goalReachedPending=false; state.cctvEventCamId=null; state.cctvEventReturnAt=0; state.cctvEventReason=''; state.cctvEventTarget=null;\n  if(sentryTouch) sentryTouch.classList.remove('active');\n  state.banner=first?'READY!':`LEVEL ${state.level}`; state.bannerTime=1.4;\n  if(capturePopup){capturePopup.classList.remove('show');} if(capturePopupTimer){clearTimeout(capturePopupTimer);capturePopupTimer=null;} if(diamondGuardBadge) diamondGuardBadge.classList.remove('show'); if(diamondIntruderBadge) diamondIntruderBadge.classList.remove('show');\n  state.viewPanX=0;state.viewPanY=0;centerView(true); resetNavigationWatch(); updateHUD(); updateLevelDisplay(); updateSurveillanceStartButton();\n}\n\nfunction startGame(){\n  ensureAudio();\n  state.name='SECURITY';\n  state.score=0; state.level=1; state.lives=1; state.flashCount=0; state.eatenCount=0; state.eatGoal=5; state.paused=false; state.phase='ready'; state.readyTimer=1.15;\n  state.scoreDecisionPending=false; state.pendingRank=null; state.completeRevealTimer=0; state.goalReachedPending=false;\n  state.diamondGuardTime=60; state.diamondGuardActive=false;\n  state.diamondIntruderTime=60; state.diamondIntruderActive=false; state.diamondPressureShown=0; state.winReason=null;\n  startBtn.style.display='';\n  loadLevel(true);\n  overlay.classList.remove('celebrate'); overlay.classList.add('hidden');\n  startBtn.blur(); root.focus();\n  statusLine.textContent=state.lang==='fr'?'Niveau 1 \u2014 une seule cam\u00e9ra. Capturez 5 cambrioleurs.':state.lang==='en'?'Level 1 \u2014 one camera only. Eat 5 burglars.':'Level 1 \u2014 slechts \u00e9\u00e9n camera. Eet 5 inbrekers.';\n  state.lastTime=performance.now();\n}\n\nfunction playerCentered(p=state.player){\n  return !!p && Math.abs(p.x-Math.round(p.x))<.001 && Math.abs(p.y-Math.round(p.y))<.001;\n}\n\nfunction directionIndex(dir){\n  const order=[DIRS.right,DIRS.down,DIRS.left,DIRS.up];\n  const i=order.findIndex(d=>d===dir || (d.x===dir?.x && d.y===dir?.y));\n  return i<0?0:i;\n}\n\nfunction nearestCardinalFromAngle(angle){\n  const a=Math.atan2(Math.sin(angle),Math.cos(angle));\n  const candidates=[{d:DIRS.right,a:0},{d:DIRS.down,a:Math.PI\/2},{d:DIRS.left,a:Math.PI},{d:DIRS.up,a:-Math.PI\/2}];\n  candidates.sort((x,y)=>Math.abs(Math.atan2(Math.sin(a-x.a),Math.cos(a-x.a)))-Math.abs(Math.atan2(Math.sin(a-y.a),Math.cos(a-y.a))));\n  return candidates[0].d;\n}\n\nfunction normalizeAngle(angle){\n  angle%=TAU;\n  return angle<0?angle+TAU:angle;\n}\n\nfunction nearbySurveillanceWalls(player=state.player){\n  if(!player) return [];\n  const x=Math.round(player.x), y=Math.round(player.y);\n  \/\/ Alleen direct aangrenzende muren beperken de sweep. Staat de camera vrij\n  \/\/ in een kamer of gangkruising, dan zijn er geen wall normals en krijgt\n  \/\/ Surveillance mode dus altijd een volledige, vloeiende 360\u00b0-rotatie.\n  return CARDINALS.filter(d=>!isPassable(x+d.x,y+d.y)).map(d=>({x:d.x,y:d.y}));\n}\n\nfunction canStartSurveillanceHere(player=state.player){\n  if(!player) return false;\n  const x=Math.round(player.x), y=Math.round(player.y);\n  return isPassable(x,y);\n}\n\nfunction surveillanceAngleTouchesWall(angle){\n  if(!state.surveillanceWallSweep || !state.surveillanceWallNormals?.length) return false;\n  const dx=Math.cos(angle), dy=Math.sin(angle);\n  \/\/ De lamp reikt in Surveillance mode 9.2 tegels. Wanneer de centrale lichtstraal\n  \/\/ binnen dat bereik de grens van een direct aangrenzende muur zou raken, keren we om.\n  \/\/ Zo ontstaat langs \u00e9\u00e9n muur een heen-en-weer sweep, in een hoek een kleinere sweep.\n  const maxBeamTiles=9.2;\n  for(const wall of state.surveillanceWallNormals){\n    const toward=dx*wall.x+dy*wall.y;\n    if(toward<=0.0001) continue;\n    const wallHitDistance=.5\/toward;\n    if(wallHitDistance<=maxBeamTiles) return true;\n  }\n  return false;\n}\n\nfunction nearestSafeSurveillanceAngle(angle){\n  angle=normalizeAngle(angle);\n  if(!surveillanceAngleTouchesWall(angle)) return angle;\n  const step=Math.PI\/180;\n  for(let i=1;i<=180;i++){\n    const plus=normalizeAngle(angle+i*step);\n    const minus=normalizeAngle(angle-i*step);\n    const plusSafe=!surveillanceAngleTouchesWall(plus);\n    const minusSafe=!surveillanceAngleTouchesWall(minus);\n    if(plusSafe && minusSafe) return state.surveillanceSweepDir>=0?plus:minus;\n    if(plusSafe) return plus;\n    if(minusSafe) return minus;\n  }\n  return angle;\n}\n\nfunction activateSentryNow(){\n  if(!state.player || state.phase!=='playing') return;\n  const p=state.player;\n  \/\/ Surveillance mode start EXACT waar de camera zich bevindt.\n  \/\/ Geen centrering, geen eerstvolgend tegelblok en geen muurvereiste.\n  \/\/ Zo werkt hij ook midden in een gang, midden tijdens een bocht of tussen twee tegelcentra.\n  const cx=Math.round(p.x), cy=Math.round(p.y);\n  if(!isPassable(cx,cy)) return;\n  state.sentryResumeDir=(p.dir&&p.dir!==DIRS.none)?p.dir:(p.facing||DIRS.right);\n  p.dir=DIRS.none; p.desired=DIRS.none; p.route=[];\n  state.manualMode=false; state.navTarget=null; state.huntTarget=null;\n  state.sentry=true; state.sentryPending=false; state.sentryDetected=false; state.sentryScanPulse=0;\n  state.surveillanceAngle=Number.isFinite(p.visualAngle)?p.visualAngle:facingAngle(p.facing||DIRS.right);\n  state.surveillanceSweepDir=1;\n  state.surveillanceWallNormals=nearbySurveillanceWalls(p);\n  state.surveillanceWallSweep=state.surveillanceWallNormals.length>0;\n  if(state.surveillanceWallSweep){\n    state.surveillanceAngle=nearestSafeSurveillanceAngle(state.surveillanceAngle);\n    const probe=.045;\n    const plusBlocked=surveillanceAngleTouchesWall(normalizeAngle(state.surveillanceAngle+probe));\n    const minusBlocked=surveillanceAngleTouchesWall(normalizeAngle(state.surveillanceAngle-probe));\n    if(plusBlocked&&!minusBlocked) state.surveillanceSweepDir=-1;\n    else if(minusBlocked&&!plusBlocked) state.surveillanceSweepDir=1;\n  }\n  state.flashCooldown=0; state.flashFx=1; state.flashPulse=0;\n  state.banner='SURVEILLANCE ONLINE'; state.bannerTime=.9;\n  state.movementFlashActive=false; state.stationaryTimer=0;\n  statusLine.textContent=state.surveillanceWallSweep\n    ?(state.lang==='fr'?'Mode surveillance actif \u2014 balayage continu entre les murs, flash allum\u00e9.':state.lang==='en'?'Surveillance mode active \u2014 continuous sweep between the walls with the flash lit.':'Surveillance mode actief \u2014 continue scan heen en weer tussen de muren met de flash aan.')\n    :(state.lang==='fr'?'Mode surveillance actif \u2014 rotation continue \u00e0 360\u00b0 et flash allum\u00e9.':state.lang==='en'?'Surveillance mode active \u2014 continuous 360\u00b0 rotation with the flash lit.':'Surveillance mode actief \u2014 continue 360\u00b0 rotatie met de flash aan.');\n  if(sentryTouch) sentryTouch.classList.add('active');\n  updateSurveillanceStartButton();\n}\n\nfunction deactivateSentry(moveDir=null){\n  if(!state.player) return;\n  const wasActive=state.sentry||state.sentryPending;\n  state.sentry=false; state.sentryPending=false; state.sentryDetected=false; state.sentryHold=0;\n  state.surveillanceWallSweep=false; state.surveillanceWallNormals=[];\n  if(wasActive && !state.flashHeld){state.flashFx=.18;state.flashCooldown=Math.max(state.flashCooldown,.45);}\n  if(sentryTouch) sentryTouch.classList.remove('active');\n  const d=moveDir || nearestCardinalFromAngle(state.surveillanceAngle) || state.sentryResumeDir || state.player.facing || DIRS.right;\n  state.player.facing=d;\n  if(moveDir){state.player.desired=d;state.player.dir=d;}\n  else{state.player.desired=DIRS.none;state.player.dir=DIRS.none;}\n  statusLine.textContent=state.lang==='fr'?`Op\u00e9rateur ${state.name} \u2014 cam\u00e9ra mobile.`:state.lang==='en'?`Operator ${state.name} \u2014 mobile camera.`:`Operator ${state.name} actief \u2014 mobiele camera.`;\n  updateSurveillanceStartButton();\n}\n\nfunction toggleSentry(){\n  if(!state.player || state.phase!=='playing' || state.paused) return;\n  state.surveillanceReturn=false;\n  \/\/ Surveillance start onmiddellijk vanuit ELKE huidige camerapositie.\n  \/\/ Vrije positie = vloeiende 360\u00b0 scan; muur\/hoek = begrensde sweep.\n  if(state.sentry || state.sentryPending){\n    deactivateSentry();\n    state.banner='SURVEILLANCE OFF'; state.bannerTime=.7;\n    return;\n  }\n  activateSentryNow();\n}\n\nfunction corridorRangeTiles(player=state.player,facing=player?.facing||DIRS.right){\n  if(!player) return 0;\n  const sx=Math.round(player.x), sy=Math.round(player.y);\n  let x=sx,y=sy,range=0;\n  const max=Math.max(MAZE_W,MAZE_H)+2;\n  for(let i=0;i<max;i++){\n    let nx=x+facing.x, ny=y+facing.y;\n    \/\/ Voor de zijtunnel scannen we tot aan de rand; we tekenen\/detecteren niet oneindig rond de wrap.\n    if(nx<0||nx>=MAZE_W||ny<0||ny>=MAZE_H) break;\n    if(!isPassable(nx,ny)) break;\n    range++; x=nx; y=ny;\n  }\n  return range+.45;\n}\n\nfunction burglarInSentryCorridor(b){\n  if(!state.sentry || !state.player || b.respawn>0 || b.flashed) return false;\n  const p=state.player, a=state.surveillanceAngle;\n  const fx=Math.cos(a), fy=Math.sin(a);\n  const vx=b.x-p.x, vy=b.y-p.y, r=Math.hypot(vx,vy);\n  if(r<.12 || r>9.2) return false;\n  const dot=(vx\/r)*fx+(vy\/r)*fy;\n  if(dot<Math.cos(.20)) return false;\n  return lineClear(p.x,p.y,b.x,b.y);\n}\n\nfunction updateSentry(dt){\n  if(!state.sentry || !state.player) return;\n  const p=state.player;\n  p.dir=DIRS.none; p.desired=DIRS.none;\n  state.sentryScanPulse+=dt;\n  const turnStep=dt*1.18;\n  if(state.surveillanceWallSweep){\n    let candidate=normalizeAngle(state.surveillanceAngle+turnStep*state.surveillanceSweepDir);\n    if(surveillanceAngleTouchesWall(candidate)){\n      state.surveillanceSweepDir*=-1;\n      candidate=normalizeAngle(state.surveillanceAngle+turnStep*state.surveillanceSweepDir);\n      if(surveillanceAngleTouchesWall(candidate)) candidate=nearestSafeSurveillanceAngle(state.surveillanceAngle);\n    }\n    state.surveillanceAngle=candidate;\n  }else{\n    state.surveillanceAngle=normalizeAngle(state.surveillanceAngle+turnStep);\n  }\n  state.flashCooldown=0; state.flashFx=1; state.flashPulse+=dt;\n\n  const visible=state.burglars.filter(burglarInSentryCorridor);\n  state.sentryDetected=visible.length>0;\n  if(!visible.length) return;\n\n  let hits=0;\n  for(const b of visible){\n    if(state.phase!=='playing') break;\n    if(markBurglarFlashed(b)){\n      state.score+=175;\n      registerSuccessfulFlash();\n      hits++;\n      sfx('stun');\n    }\n  }\n  if(!hits || state.phase!=='playing') return;\n\n  state.banner=`AUTO FLASH \u00d7${hits}`; state.bannerTime=.8; state.shake=.15; sfx('flash');\n  state.surveillanceReturn=true;\n  state.sentry=false; state.sentryPending=false; state.sentryDetected=false;\n  state.surveillanceWallSweep=false; state.surveillanceWallNormals=[];\n  state.flashFx=.35;\n  if(sentryTouch) sentryTouch.classList.remove('active');\n  if(!startFlashedHunt()) finishFlashedHuntIfNeeded();\n}\n\n\nfunction findNavigationPath(sx,sy,tx,ty){\n  sx=Math.round(sx); sy=Math.round(sy); tx=Math.round(tx); ty=Math.round(ty);\n  if(!isPassable(sx,sy) || !isPassable(tx,ty)) return [];\n  const size=MAZE_W*MAZE_H, prev=new Int32Array(size); prev.fill(-1);\n  const qx=new Int16Array(size), qy=new Int16Array(size);\n  const key=(x,y)=>y*MAZE_W+x;\n  let head=0,tail=0;\n  qx[tail]=sx; qy[tail]=sy; tail++; prev[key(sx,sy)]=key(sx,sy);\n  while(head<tail){\n    const x=qx[head],y=qy[head++];\n    if(x===tx && y===ty) break;\n    for(const d of CARDINALS){\n      const nx=x+d.x,ny=y+d.y;\n      if(nx<0||nx>=MAZE_W||ny<0||ny>=MAZE_H||!isPassable(nx,ny)) continue;\n      const nk=key(nx,ny); if(prev[nk]!==-1) continue;\n      prev[nk]=key(x,y); qx[tail]=nx; qy[tail]=ny; tail++;\n    }\n  }\n  const end=key(tx,ty); if(prev[end]===-1) return [];\n  const path=[]; let k=end;\n  while(true){\n    const x=k%MAZE_W,y=Math.floor(k\/MAZE_W); path.push({x,y});\n    if(k===prev[k]) break;\n    k=prev[k];\n  }\n  path.reverse(); return path;\n}\n\n\nfunction buildNavigationPathFromCurrentPosition(p,target){\n  if(!p || !target) return [];\n\n  const rx=Math.round(p.x), ry=Math.round(p.y);\n  const fx=Math.abs(p.x-rx), fy=Math.abs(p.y-ry);\n  const candidates=[];\n\n  const addCandidate=(x,y)=>{\n    if(x<0||x>=MAZE_W||y<0||y>=MAZE_H||!isPassable(x,y)) return;\n    if(!candidates.some(c=>c.x===x&&c.y===y)) candidates.push({x,y});\n  };\n\n  \/\/ Normale beweging ligt altijd op \u00e9\u00e9n rasteras. Wanneer een mobiele tap komt\n  \/\/ terwijl de camera tussen twee tegelcentra staat, gebruiken we de twee\n  \/\/ eindpunten van dat huidige gangsegment als veilige route-startpunten.\n  if(fx>.035 && fy<.20){\n    addCandidate(Math.floor(p.x),ry);\n    addCandidate(Math.ceil(p.x),ry);\n  }else if(fy>.035 && fx<.20){\n    addCandidate(rx,Math.floor(p.y));\n    addCandidate(rx,Math.ceil(p.y));\n  }else{\n    addCandidate(rx,ry);\n  }\n\n  \/\/ Defensieve fallback voor een positie die door een oud overgangsframe licht\n  \/\/ buiten de ideale rasteras is geraakt.\n  if(!candidates.length){\n    const safe=nearestPassable(rx,ry);\n    if(safe){\n      p.x=safe.x;\n      p.y=safe.y;\n      p.dir=DIRS.none;\n      p.desired=DIRS.none;\n      addCandidate(safe.x,safe.y);\n    }\n  }\n\n  let best=null;\n  for(const start of candidates){\n    const path=findNavigationPath(start.x,start.y,target.x,target.y);\n    if(!path.length) continue;\n\n    \/\/ Het eerste deel van de route moet vanuit de actuele positie recht over\n    \/\/ het bestaande gangsegment bereikbaar zijn, nooit diagonaal.\n    const aligned=(Math.abs(start.x-p.x)<.001 || Math.abs(start.y-p.y)<.001);\n    if(!aligned && (fx>.035 || fy>.035)) continue;\n\n    const approach=Math.hypot(start.x-p.x,start.y-p.y);\n    const cost=approach+Math.max(0,path.length-1);\n    if(!best || cost<best.cost) best={path,cost,start};\n  }\n\n  if(!best){\n    const safe=nearestPassable(rx,ry);\n    if(!safe) return [];\n    \/\/ Alleen bij een werkelijk corrupte tussenpositie centreren we \u00e9\u00e9nmalig.\n    p.x=safe.x;\n    p.y=safe.y;\n    p.dir=DIRS.none;\n    p.desired=DIRS.none;\n    return findNavigationPath(safe.x,safe.y,target.x,target.y);\n  }\n\n  return best.path;\n}\n\nfunction setNavigationTarget(col,row){\n  if(!state.player || !['ready','playing'].includes(state.phase) || state.paused) return;\n  state.surveillanceReturn=false;\n  const target=nearestPassable(clamp(col,0,MAZE_W-1),clamp(row,0,MAZE_H-1));\n  const p=state.player;\n  const path=buildNavigationPathFromCurrentPosition(p,target);\n  if(!path.length){ state.banner='NO ROUTE'; state.bannerTime=.8; return; }\n  if(state.sentry || state.sentryPending) deactivateSentry();\n  state.movementFlashActive=true; state.stationaryTimer=0; state.flashCooldown=0; state.flashFx=1;\n  p.route=path;\n  state.manualMode=false;\n  state.huntTarget=null;\n  state.navTarget={x:target.x,y:target.y};\n  resetNavigationWatch();\n  state.banner='ROUTE SET'; state.bannerTime=.45;\n  statusLine.textContent=state.lang==='fr'?`Nouvelle destination : ${target.x},${target.y} \u2014 navigation automatique.`:state.lang==='en'?`New destination: ${target.x},${target.y} \u2014 automatic navigation.`:`Nieuwe bestemming: ${target.x},${target.y} \u2014 camera navigeert automatisch.`;\n}\n\nfunction resetNavigationWatch(){\n  const p=state.player;\n  state.navWatchX=p?.x||0;\n  state.navWatchY=p?.y||0;\n  state.navWatchTimer=0;\n  state.navRecoveryCooldown=0;\n}\n\nfunction rebuildActiveNavigationRoute(){\n  const p=state.player;\n  if(!p) return false;\n  let target=null;\n  if(state.huntTarget!==null && state.huntTarget!==undefined){\n    const b=state.burglars.find(x=>x.index===state.huntTarget && x.flashed && x.respawn<=0);\n    if(b) target=nearestPassable(Math.round(b.x),Math.round(b.y));\n  }\n  if(!target && state.navTarget) target=nearestPassable(Math.round(state.navTarget.x),Math.round(state.navTarget.y));\n  if(!target) return false;\n  const path=buildNavigationPathFromCurrentPosition(p,target);\n  if(!path.length) return false;\n  p.route=path;\n  p.dir=DIRS.none;\n  p.desired=DIRS.none;\n  state.navTarget={x:target.x,y:target.y};\n  return true;\n}\n\nfunction navigationWatchdog(dt){\n  const p=state.player;\n  if(!p || state.manualMode || state.flashHeld || state.sentry || state.sentryPending || !Array.isArray(p.route) || !p.route.length){\n    resetNavigationWatch();\n    return;\n  }\n  state.navRecoveryCooldown=Math.max(0,state.navRecoveryCooldown-dt);\n  const moved=Math.hypot(p.x-state.navWatchX,p.y-state.navWatchY);\n  if(moved>.035){\n    state.navWatchX=p.x; state.navWatchY=p.y; state.navWatchTimer=0;\n    return;\n  }\n  state.navWatchTimer+=dt;\n  if(state.navWatchTimer<.45 || state.navRecoveryCooldown>0) return;\n\n  \/\/ Route actief maar geen vooruitgang: zet een bijna bereikte tegel exact op het\n  \/\/ raster en bereken de resterende route opnieuw. Zo kan een overgang nooit blijven hangen.\n  const rx=Math.round(p.x), ry=Math.round(p.y);\n  if(Math.abs(p.x-rx)<.18 && Math.abs(p.y-ry)<.18 && isPassable(rx,ry)){\n    p.x=rx; p.y=ry;\n  }\n  if(rebuildActiveNavigationRoute()){\n    state.banner='ROUTE RECOVERED'; state.bannerTime=.35;\n  }\n  state.navWatchX=p.x; state.navWatchY=p.y; state.navWatchTimer=0; state.navRecoveryCooldown=.35;\n}\n\nfunction updatePlayerNavigation(dt){\n  const p=state.player;\n  if(!p) return;\n  if(!Array.isArray(p.route) || !p.route.length){\n    p.dir=DIRS.none; p.desired=DIRS.none; return;\n  }\n\n  \/\/ Verwijder elk routepunt dat werkelijk bereikt is.\n  while(p.route.length){\n    const node=p.route[0];\n    if(Math.abs(p.x-node.x)<.045 && Math.abs(p.y-node.y)<.045){\n      p.x=node.x; p.y=node.y; p.route.shift();\n    }else break;\n  }\n  if(!p.route.length){\n    p.dir=DIRS.none; p.desired=DIRS.none;\n    state.banner='DESTINATION'; state.bannerTime=.6;\n    statusLine.textContent=state.lang==='fr'?'Destination atteinte \u2014 cliquez ailleurs pour une nouvelle route.':state.lang==='en'?'Destination reached \u2014 click elsewhere for a new route.':'Bestemming bereikt \u2014 klik ergens anders voor een nieuwe route.';\n    return;\n  }\n\n  const next=p.route[0], dx=next.x-p.x, dy=next.y-p.y;\n  let d=DIRS.none;\n  if(Math.abs(dx)>.045) d=dx>0?DIRS.right:DIRS.left;\n  else if(Math.abs(dy)>.045) d=dy>0?DIRS.down:DIRS.up;\n  p.desired=d;\n  if(p.dir===DIRS.none || reverseOf(d,p.dir)){ p.dir=d; p.facing=d; }\n  moveActor(p,4.75,dt,true);\n}\n\nfunction cancelSentryForDirectControl(){\n  state.surveillanceReturn=false;\n  if(!state.sentry && !state.sentryPending) return;\n  deactivateSentry();\n}\n\nfunction beginManualMove(dir){\n  if(!state.player || !['ready','playing'].includes(state.phase) || state.paused || state.flashHeld) return;\n  cancelSentryForDirectControl();\n  const p=state.player;\n  state.movementFlashActive=true; state.stationaryTimer=0; state.flashCooldown=0; state.flashFx=1;\n  state.manualMode=true;\n  state.huntTarget=null;\n  state.navTarget=null;\n  p.route=[];\n  p.desired=dir;\n\n  \/\/ Als de camera eerder midden tussen twee tegels werd stilgezet (bv. door FLASH),\n  \/\/ mag een loodrechte pijl hem niet van het gangraster af sturen. Eerst netjes\n  \/\/ naar de dichtstbijzijnde tegelas, daarna wordt de gewenste bocht uitgevoerd.\n  const rx=Math.round(p.x), ry=Math.round(p.y);\n  const ox=p.x-rx, oy=p.y-ry;\n  const offX=Math.abs(ox)>.002, offY=Math.abs(oy)>.002;\n  if(offX && !offY){\n    if(dir.x!==0) p.dir=dir;\n    else p.dir=ox>0?DIRS.left:DIRS.right;\n  }else if(offY && !offX){\n    if(dir.y!==0) p.dir=dir;\n    else p.dir=oy>0?DIRS.up:DIRS.down;\n  }else if(offX || offY){\n    \/\/ Veilig herstel voor een uitzonderlijke niet-uitgelijnde positie.\n    if(Math.abs(ox)>=Math.abs(oy)) p.dir=ox>0?DIRS.left:DIRS.right;\n    else p.dir=oy>0?DIRS.up:DIRS.down;\n  }else{\n    p.x=rx; p.y=ry;\n    if(canStep(rx,ry,dir)) p.dir=dir;\n    else if(!p.dir || p.dir===DIRS.none || !canStep(rx,ry,p.dir)) p.dir=DIRS.none;\n  }\n  p.facing=dir;\n  resetNavigationWatch();\n  state.banner='MANUAL'; state.bannerTime=.35;\n  statusLine.textContent=state.lang==='fr'?'Commande manuelle \u2014 cliquez dans le b\u00e2timent pour reprendre la navigation automatique.':state.lang==='en'?'Manual control \u2014 click in the building to resume automatic navigation.':'Manuele besturing \u2014 klik in het gebouw om automatische navigatie opnieuw te starten.';\n}\n\nfunction angleDelta(from,to){ return Math.atan2(Math.sin(to-from),Math.cos(to-from)); }\nfunction smoothAngle(from,to,maxStep){\n  const d=angleDelta(from,to);\n  if(Math.abs(d)<=maxStep) return normalizeAngle(to);\n  return normalizeAngle(from+Math.sign(d)*maxStep);\n}\nfunction cameraTargetAngle(cam){\n  if(cam===state.player && state.sentry) return state.surveillanceAngle;\n  if(cam!==state.player && cam.autoSurveillance) return cam.autoAngle;\n  return facingAngle(cam.facing||DIRS.right);\n}\nfunction updateCameraVisualAngles(dt){\n  const step=dt*5.4; \/\/ ongeveer 310\u00b0\/s: snel maar zichtbaar vloeiend.\n  for(const cam of state.cameras||[]){\n    if(!Number.isFinite(cam.visualAngle)) cam.visualAngle=cameraTargetAngle(cam);\n    cam.visualAngle=smoothAngle(cam.visualAngle,cameraTargetAngle(cam),step);\n  }\n}\nfunction cameraForwardVector(cam=state.player){\n  const a=Number.isFinite(cam?.visualAngle)?cam.visualAngle:facingAngle(cam?.facing||DIRS.right);\n  return {x:Math.cos(a),y:Math.sin(a),angle:a};\n}\n\nfunction rotateFacing(step){\n  if(!state.player) return;\n  const order=[DIRS.right,DIRS.down,DIRS.left,DIRS.up];\n  const i=directionIndex(state.player.facing||DIRS.right);\n  state.player.facing=order[(i+step+4)%4];\n}\n\nfunction markBurglarFlashed(b,owner=state.player){\n  if(!b || b.respawn>0) return false;\n  const first=!b.flashed;\n  b.flashed=true;\n  if(first){\n    b.flashOwner=owner?.id||null;\n    b.flashRecover=30;\n  }\n  b.stun=0;\n  b.dir=DIRS.none;\n  b.desired=DIRS.none;\n  return first;\n}\n\nfunction registerSuccessfulFlash(){\n  if(state.phase!=='playing') return;\n  state.flashCount++;\n}\n\n\nfunction levelGoalText(){\n  const l=state.level;\n  if(state.lang==='fr'){\n    return [\n      '',\n      '<strong>NIVEAU 1 :<\/strong> 1 CAM\u00c9RA \u2022 CAPTUREZ 5 CAMBRIOLEURS',\n      '<strong>NIVEAU 2 :<\/strong> BOULES JAUNES \u2022 2 CAM\u00c9RAS \u2022 CAPTUREZ 10',\n      '<strong>NIVEAU 3 :<\/strong> D\u00c9BLOQUEZ UNE 3e CAM\u00c9RA \u2022 CAPTUREZ 12',\n      '<strong>NIVEAU 4 :<\/strong> DIAMANT D\u00c9J\u00c0 REP\u00c9R\u00c9 \u2022 CAPTUREZ 14',\n      '<strong>NIVEAU 5 :<\/strong> 5 CAMBRIOLEURS ACTIFS \u2022 JUSQU\u2019\u00c0 4 CAM\u00c9RAS \u2022 CAPTUREZ 15'\n    ][l]||'';\n  }\n  if(state.lang==='en'){\n    return [\n      '',\n      '<strong>LEVEL 1:<\/strong> 1 CAMERA \u2022 EAT 5 BURGLARS',\n      '<strong>LEVEL 2:<\/strong> YELLOW ORBS \u2022 2 CAMERAS \u2022 EAT 10',\n      '<strong>LEVEL 3:<\/strong> UNLOCK A 3rd CAMERA \u2022 EAT 12',\n      '<strong>LEVEL 4:<\/strong> DIAMOND ALREADY EXPOSED \u2022 EAT 14',\n      '<strong>LEVEL 5:<\/strong> 5 ACTIVE BURGLARS \u2022 UP TO 4 CAMERAS \u2022 EAT 15'\n    ][l]||'';\n  }\n  return [\n    '',\n    '<strong>LEVEL 1:<\/strong> 1 CAMERA \u2022 EET 5 INBREKERS',\n    '<strong>LEVEL 2:<\/strong> GELE BOLLETJES \u2022 2 CAMERA\u2019S \u2022 EET 10',\n    '<strong>LEVEL 3:<\/strong> SPEEL EEN 3e CAMERA VRIJ \u2022 EET 12',\n    '<strong>LEVEL 4:<\/strong> DIAMANT METEEN ONTDEKT \u2022 EET 14',\n    '<strong>LEVEL 5:<\/strong> 5 ACTIEVE INBREKERS \u2022 MAX. 4 CAMERA\u2019S \u2022 EET 15'\n  ][l]||'';\n}\nfunction updateLevelGoalText(){\n  if(goalText) goalText.innerHTML=levelGoalText();\n}\n\nfunction completeCurrentLevel(reason='captures'){\n  if(state.phase!=='playing') return;\n  const completed=state.level;\n\n  \/\/ In Level 5 is elk voltooid hoofddoel de eindoverwinning.\n  if(completed>=5){\n    missionComplete(reason==='diamondGuard'?'diamondGuard':'captures');\n    return;\n  }\n\n  state.phase='levelclear';\n  state.winReason=reason;\n  state.flashHeld=false;\n  state.flashFx=0;\n  state.sentry=false;\n  state.sentryPending=false;\n  state.diamondGuardActive=false;\n  state.diamondGuardTime=60;\n  state.diamondPressureShown=0;\n  state.diamondIntruderActive=false;\n  state.diamondIntruderTime=60;\n  state.huntTarget=null;\n  state.navTarget=null;\n  if(diamondGuardBadge) diamondGuardBadge.classList.remove('show');\n  if(diamondIntruderBadge) diamondIntruderBadge.classList.remove('show');\n\n  if(state.player){\n    state.player.route=[];\n    state.player.dir=DIRS.none;\n    state.player.desired=DIRS.none;\n  }\n\n  const next=completed+1;\n  const diamondGoal=reason==='diamondGuard';\n\n  state.banner=diamondGoal\n    ?(state.lang==='fr'?`DIAMANT S\u00c9CURIS\u00c9 \u2014 NIVEAU ${next}`:state.lang==='en'?`DIAMOND GOAL ACHIEVED \u2014 LEVEL ${next}`:`DIAMOND GOAL ACHIEVED \u2014 LEVEL ${next}`)\n    :(state.lang==='fr'?`NIVEAU ${completed} TERMIN\u00c9 \u2014 NIVEAU ${next}`:state.lang==='en'?`LEVEL ${completed} COMPLETE \u2014 LEVEL ${next}`:`LEVEL ${completed} VOLTOOID \u2014 LEVEL ${next}`);\n  state.bannerTime=1.8;\n  sfx('level');\n  updateHUD();\n\n  statusLine.textContent=diamondGoal\n    ?(state.lang==='fr'?`Salle du diamant surveill\u00e9e 60 secondes. Pr\u00e9paration du niveau ${next}\u2026`:state.lang==='en'?`Diamond room guarded for 60 seconds. Preparing Level ${next}\u2026`:`Diamantkamer 60 seconden bewaakt. Level ${next} wordt klaargezet\u2026`)\n    :(state.lang==='fr'?`${state.eatGoal} cambrioleurs captur\u00e9s. Pr\u00e9paration du niveau ${next}\u2026`:state.lang==='en'?`${state.eatGoal} burglars eaten. Preparing Level ${next}\u2026`:`${state.eatGoal} inbrekers opgegeten. Level ${next} wordt klaargezet\u2026`);\n\n  setTimeout(()=>{\n    if(state.phase!=='levelclear' || state.level!==completed) return;\n    state.level=next;\n    state.phase='ready';\n    state.readyTimer=1.15;\n    loadLevel(false);\n    updateLevelGoalText();\n\n    const extra=state.lang==='fr'\n      ?['','', 'Cherchez une boule jaune pour obtenir une 2e cam\u00e9ra.','Vous pouvez maintenant d\u00e9bloquer une 3e cam\u00e9ra.','Le diamant est d\u00e9j\u00e0 visible : d\u00e9fendez-le imm\u00e9diatement.','Un 5e cambrioleur est actif et les ennemis sont plus rapides.'][next]\n      :state.lang==='en'\n        ?['','', 'Find a yellow orb to get a 2nd camera.','You can now unlock a 3rd camera.','The diamond starts exposed: defend it immediately.','A 5th burglar is active and enemies are faster.'][next]\n        :['','', 'Zoek een geel bolletje voor een 2e camera.','Je kunt nu een 3e camera vrijspelen.','De diamant is meteen zichtbaar: verdedig hem direct.','Een 5e inbreker is actief en de vijanden zijn sneller.'][next];\n\n    statusLine.textContent=`${state.lang==='fr'?'Niveau':state.lang==='en'?'Level':'Level'} ${next} \u2014 ${extra}`;\n  },1700);\n}\n\nfunction registerEatenBurglar(){\n  if(state.phase!=='playing') return;\n  state.eatenCount=Math.min(state.eatGoal,state.eatenCount+1);\n  updateHUD(); showCaptureProgressPopup();\n  if(state.eatenCount>=state.eatGoal) completeCurrentLevel('captures');\n}\nfunction flashedBurglars(){\n  return state.burglars.filter(b=>b.flashed && b.respawn<=0 && (!b.flashOwner || b.flashOwner===state.player?.id));\n}\n\nfunction setHuntRouteTo(b){\n  if(!state.player || !b || !b.flashed || b.respawn>0) return false;\n  const p=state.player;\n  const start=nearestPassable(Math.round(p.x),Math.round(p.y));\n  const target=nearestPassable(Math.round(b.x),Math.round(b.y));\n  const path=findNavigationPath(start.x,start.y,target.x,target.y);\n  if(!path.length) return false;\n  p.route=path;\n  p.dir=DIRS.none;\n  p.desired=DIRS.none;\n  state.manualMode=false;\n  state.huntTarget=b.index;\n  state.navTarget={x:target.x,y:target.y};\n  resetNavigationWatch();\n  statusLine.textContent=state.lang==='fr'?'Cambrioleur flash\u00e9 localis\u00e9 \u2014 la cam\u00e9ra va automatiquement le capturer.':state.lang==='en'?'Flashed burglar located \u2014 the camera will automatically capture him.':'Geflashte inbreker gelokaliseerd \u2014 camera gaat hem automatisch pakken.';\n  return true;\n}\n\nfunction startFlashedHunt(){\n  if(!state.player) return false;\n  const targets=flashedBurglars();\n  if(!targets.length){\n    state.huntTarget=null;\n    state.navTarget=null;\n    state.player.route=[];\n    state.player.dir=DIRS.none;\n    state.player.desired=DIRS.none;\n    return false;\n  }\n  targets.sort((a,b)=>shortestMazeDistance(state.player.x,state.player.y,a.x,a.y)-shortestMazeDistance(state.player.x,state.player.y,b.x,b.y));\n  return setHuntRouteTo(targets[0]);\n}\n\nfunction finishFlashedHuntIfNeeded(){\n  if(startFlashedHunt()) return;\n  state.surveillanceReturn=false;\n  if(state.phase==='playing'){\n    state.banner='SURVEILLANCE AUTO'; state.bannerTime=.55;\n    enterAutomaticSurveillance();\n  }\n}\n\n\nfunction updateMovementFlashTargets(){\n  if(!state.movementFlashActive || !state.player || state.sentry || state.flashHeld) return;\n  const p=state.player, f=cameraForwardVector(p), maxRange=7.2;\n  let hits=0;\n  for(const b of state.burglars){\n    if(b.respawn>0 || b.flashed) continue;\n    const vx=b.x-p.x,vy=b.y-p.y,r=Math.hypot(vx,vy);\n    if(r<.01 || r>maxRange) continue;\n    const dot=(vx\/r)*f.x+(vy\/r)*f.y;\n    if(dot<=.78 || !lineClear(p.x,p.y,b.x,b.y)) continue;\n    if(markBurglarFlashed(b)){\n      state.score+=125;\n      registerSuccessfulFlash();\n      hits++;\n      sfx('stun');\n    }\n  }\n  if(hits){\n    state.banner=`FLASH \u00d7${hits}`;\n    state.bannerTime=.55;\n    state.shake=.08;\n    \/\/ De oude spelregel blijft behouden: een geflashte inbreker wordt opgegeten.\n    \/\/ De camera schakelt daarom na detectie automatisch over op de dichtstbijzijnde jacht.\n    if(state.phase==='playing' && state.huntTarget==null){\n      state.manualMode=false;\n      if(startFlashedHunt()) state.banner='AUTO HUNT';\n    }\n  }\n}\n\nfunction enterAutomaticSurveillance(){\n  if(!state.player || state.phase!=='playing' || state.paused || state.flashHeld || state.sentry) return;\n  state.movementFlashActive=false;\n  state.stationaryTimer=0;\n  activateSentryNow();\n}\n\nfunction beginFlashHold(){\n  if(!state.player || state.phase!=='playing' || state.paused || state.flashHeld) return;\n  cancelSentryForDirectControl();\n  const p=state.player;\n  state.flashHeld=true;\n  state.flashTurnCooldown=0;\n  state.flashResumeDir=p.dir;\n  state.flashResumeDesired=p.desired;\n  state.flashHoldTagged=Array(cfg.burglarCount).fill(false);\n  p.dir=DIRS.none; p.desired=DIRS.none;\n  state.flashCooldown=0;\n  state.flashFx=1; state.flashPulse=0;\n  sfx('flash');\n  updateHeldFlashTargets();\n  state.banner='FLASH HOLD'; state.bannerTime=.55;\n  statusLine.textContent=state.lang==='fr'?'FLASH actif \u2014 cam\u00e9ra immobile. Tournez avec \u2190 et \u2192 ; rel\u00e2chez ESPACE pour continuer.':state.lang==='en'?'FLASH active \u2014 camera stopped. Rotate with \u2190 and \u2192; release SPACE to continue.':'FLASH actief \u2014 camera staat stil. Draai met \u2190 en \u2192; laat SPATIE los om verder te gaan.';\n}\n\nfunction updateHeldFlashTargets(){\n  if(!state.flashHeld || !state.player) return;\n  const p=state.player, f=cameraForwardVector(p), maxRange=5.4;\n  for(const b of state.burglars){\n    if(b.respawn>0) continue;\n    const vx=b.x-p.x, vy=b.y-p.y, r=Math.hypot(vx,vy);\n    if(r<.01 || r>maxRange) continue;\n    const dot=(vx\/r)*f.x+(vy\/r)*f.y;\n    if(dot<=.74 || !lineClear(p.x,p.y,b.x,b.y)) continue;\n    const firstFlash=markBurglarFlashed(b);\n    if(!state.flashHoldTagged[b.index]){\n      state.flashHoldTagged[b.index]=true;\n      if(firstFlash){ state.score+=125; registerSuccessfulFlash(); }\n      sfx('stun'); state.shake=.08;\n      if(state.phase==='playing'){ state.banner='INTRUDER FLASHED'; state.bannerTime=.45; }\n    }\n  }\n}\n\nfunction endFlashHold(){\n  if(!state.flashHeld || !state.player) return;\n  const p=state.player;\n  state.flashHeld=false;\n  state.flashFx=.24;\n  state.flashCooldown=5.5;\n  state.flashTurnCooldown=0;\n\n  \/\/ Na het loslaten vertrekt de camera nooit opnieuw op de oude route.\n  \/\/ Alleen een geflashte inbreker mag automatisch een nieuwe route starten.\n  p.dir=DIRS.none;\n  p.desired=DIRS.none;\n  p.route=[];\n  state.manualMode=false;\n  state.navTarget=null;\n  state.huntTarget=null;\n  state.flashResumeDir=null;\n  state.flashResumeDesired=null;\n  resetNavigationWatch();\n\n  if(flashedBurglars().length){\n    state.banner='AUTO HUNT'; state.bannerTime=.65;\n    if(!startFlashedHunt()) finishFlashedHuntIfNeeded();\n  }else{\n    enterAutomaticSurveillance();\n  }\n}\nfunction turnDuringFlash(step){\n  if(!state.flashHeld || state.flashTurnCooldown>0) return;\n  rotateFacing(step);\n  state.flashTurnCooldown=.14;\n  updateHeldFlashTargets();\n}\n\nfunction setDesired(dir){\n  if(!state.player || !['ready','playing'].includes(state.phase) || state.paused) return;\n  if(state.sentry || state.sentryPending){\n    deactivateSentry(dir);\n    return;\n  }\n  state.player.desired=dir;\n  if(reverseOf(dir,state.player.dir)){\n    state.player.dir=dir; state.player.facing=dir;\n  }\n}\n\nfunction moveActor(actor,speed,dt,isPlayer=false){\n  if(!actor || actor.dir===DIRS.none && actor.desired===DIRS.none) return;\n  if(actor.desired && reverseOf(actor.desired,actor.dir)){\n    actor.dir=actor.desired;\n    if(isPlayer) actor.facing=actor.dir;\n  }\n  let remaining=speed*dt, loops=0;\n  while(remaining>0.00001 && loops++<6){\n    applyWrap(actor);\n    const rx=Math.round(actor.x), ry=Math.round(actor.y);\n    const centered=Math.abs(actor.x-rx)<0.0001 && Math.abs(actor.y-ry)<0.0001;\n    if(centered){\n      actor.x=rx; actor.y=ry;\n      if(actor.desired && actor.desired!==DIRS.none && canStep(rx,ry,actor.desired)) actor.dir=actor.desired;\n      if(!actor.dir || actor.dir===DIRS.none || !canStep(rx,ry,actor.dir)){ actor.dir=DIRS.none; break; }\n      if(isPlayer) actor.facing=actor.dir;\n    }\n    if(actor.dir===DIRS.none) break;\n    let nx=actor.x, ny=actor.y;\n    if(actor.dir.x>0) nx=Math.floor(actor.x+1e-7)+1;\n    else if(actor.dir.x<0) nx=Math.ceil(actor.x-1e-7)-1;\n    else if(actor.dir.y>0) ny=Math.floor(actor.y+1e-7)+1;\n    else ny=Math.ceil(actor.y-1e-7)-1;\n    let toCenter=Math.abs(nx-actor.x)+Math.abs(ny-actor.y);\n    if(toCenter<0.000001) toCenter=1;\n    const step=Math.min(remaining,toCenter);\n    actor.x+=actor.dir.x*step; actor.y+=actor.dir.y*step; remaining-=step;\n    if(step>=toCenter-0.000001){ actor.x=nx; actor.y=ny; applyWrap(actor); }\n  }\n}\n\nfunction availableDirs(b){\n  const x=Math.round(b.x), y=Math.round(b.y);\n  const all=CARDINALS.filter(d=>canStep(x,y,d));\n  const noReverse=all.filter(d=>!reverseOf(d,b.dir));\n  return noReverse.length?noReverse:all;\n}\nfunction diamondPressureEligibleBurglars(){\n  return state.burglars\n    .filter(b=>b && b.respawn<=0 && !b.flashed)\n    .sort((a,b)=>a.index-b.index);\n}\n\nfunction diamondPressureCount(){\n  if(!state.diamond || state.diamond.stolen || !state.diamondGuardActive) return 0;\n  const eligible=diamondPressureEligibleBurglars();\n  if(!eligible.length) return 0;\n\n  \/\/ Zodra een camera de diamantkamer bewaakt, start de drukopbouw.\n  \/\/ Iedere volle 5 seconden wordt \u00e9\u00e9n extra actieve inbreker naar de\n  \/\/ diamantkamer gestuurd: 5s = 1, 10s = 2, 15s = 3, ...\n  const elapsed=clamp(60-state.diamondGuardTime,0,60);\n  return Math.min(eligible.length,Math.floor(elapsed\/5));\n}\n\nfunction diamondPressureGroup(){\n  const eligible=diamondPressureEligibleBurglars();\n  const count=diamondPressureCount();\n  if(count<=0) return [];\n\n  \/\/ Vaste volgorde binnen dit level. Hierdoor wisselen doelwitten niet willekeurig\n  \/\/ van rol tijdens hun route naar de diamantkamer.\n  return [...eligible]\n    .sort((a,b)=>{\n      const aa=(a.index+state.level)%Math.max(1,state.burglars.length);\n      const bb=(b.index+state.level)%Math.max(1,state.burglars.length);\n      return aa-bb;\n    })\n    .slice(0,count);\n}\n\nfunction burglarDrawnToDiamondRoom(b){\n  return diamondPressureGroup().includes(b);\n}\n\nfunction diamondRoomTarget(){\n  if(!state.diamond) return null;\n  return nearestPassable(state.diamond.x,state.diamond.y);\n}\n\nfunction forceBurglarTowardDiamond(b){\n  if(!b || !burglarDrawnToDiamondRoom(b)) return false;\n  const target=diamondRoomTarget();\n  if(!target) return false;\n\n  \/\/ Een inbreker die de diamantkamer bereikt, blijft daar bewust hangen.\n  \/\/ Daardoor kan de 60-seconden dreiging echt oplopen totdat een camera hem uitschakelt.\n  if(burglarInsideDiamondRoom(b)){\n    b.desired=DIRS.none;\n    b.dir=DIRS.none;\n    return true;\n  }\n\n  const sx=Math.round(b.x), sy=Math.round(b.y);\n\n  \/\/ Alleen een nieuwe richting kiezen wanneer hij op een tegelcentrum staat.\n  if(Math.abs(b.x-sx)>.001 || Math.abs(b.y-sy)>.001) return true;\n\n  const path=findNavigationPath(sx,sy,target.x,target.y);\n  if(path.length<2){\n    b.desired=DIRS.none;\n    b.dir=DIRS.none;\n    return true;\n  }\n\n  const next=path[1];\n  const dx=next.x-sx, dy=next.y-sy;\n\n  \/\/ BFS mag WEL een U-turn kiezen. Dit was het probleem in de vorige versie:\n  \/\/ availableDirs() filterde de achterwaartse richting meestal uit.\n  if(dx>0) b.desired=DIRS.right;\n  else if(dx<0) b.desired=DIRS.left;\n  else if(dy>0) b.desired=DIRS.down;\n  else if(dy<0) b.desired=DIRS.up;\n  else b.desired=DIRS.none;\n\n  b.dir=b.desired;\n  return true;\n}\n\nfunction burglarTarget(b){\n  const p=state.player, f=p.facing||DIRS.right, perp={x:-f.y,y:f.x};\n\n  if(!state.chaseMode){\n    const offsets=[{x:-3,y:-3},{x:3,y:-3},{x:-3,y:3},{x:3,y:3},{x:0,y:-5},{x:0,y:5}];\n    const o=offsets[b.index%offsets.length];\n    return nearestPassable(Math.round(p.x+o.x),Math.round(p.y+o.y));\n  }\n  if(b.index===0) return nearestPassable(Math.round(p.x),Math.round(p.y));\n  if(b.index===1) return nearestPassable(Math.round(p.x+f.x*4),Math.round(p.y+f.y*4));\n  if(b.index===2) return nearestPassable(Math.round(p.x+f.x*2+perp.x*3),Math.round(p.y+f.y*2+perp.y*3));\n  return nearestPassable(Math.round(p.x+f.x*6-perp.x*2),Math.round(p.y+f.y*6-perp.y*2));\n}\n\nfunction shortestMazeDistance(sx,sy,tx,ty,maxDepth=1800){\n  sx=Math.round(sx);sy=Math.round(sy);tx=Math.round(tx);ty=Math.round(ty);\n  if(sx===tx&&sy===ty) return 0;\n  const key=(x,y)=>y*MAZE_W+x;\n  const qx=new Int16Array(MAZE_W*MAZE_H), qy=new Int16Array(MAZE_W*MAZE_H), qd=new Int16Array(MAZE_W*MAZE_H);\n  const seen=new Uint8Array(MAZE_W*MAZE_H);\n  let head=0,tail=0;\n  if(sy<0||sy>=MAZE_H) return 9999;\n  sx=(sx+MAZE_W)%MAZE_W;\n  qx[tail]=sx;qy[tail]=sy;qd[tail]=0;tail++;seen[key(sx,sy)]=1;\n  while(head<tail && head<maxDepth){\n    const x=qx[head],y=qy[head],d=qd[head++];\n    for(const dir of CARDINALS){\n      let nx=x+dir.x,ny=y+dir.y;\n      if(y===TUNNEL_ROW){ if(nx<0) nx=MAZE_W-1; else if(nx>=MAZE_W) nx=0; }\n      if(nx<0||nx>=MAZE_W||ny<0||ny>=MAZE_H||!isPassable(nx,ny)) continue;\n      if(nx===tx&&ny===ty) return d+1;\n      const k=key(nx,ny); if(seen[k]) continue; seen[k]=1; qx[tail]=nx;qy[tail]=ny;qd[tail]=d+1;tail++;\n    }\n  }\n  return 9999;\n}\n\nfunction chooseBurglarDir(b){\n  const opts=availableDirs(b); if(!opts.length){ b.desired=DIRS.none; return; }\n  if(state.exposed>0){\n    opts.sort((a,c)=>{\n      const da=shortestMazeDistance(b.x+a.x,b.y+a.y,state.player.x,state.player.y);\n      const dc=shortestMazeDistance(b.x+c.x,b.y+c.y,state.player.x,state.player.y);\n      return dc-da;\n    });\n    b.desired=opts[Math.random()<.08?Math.floor(Math.random()*opts.length):0]; return;\n  }\n  const t=burglarTarget(b);\n  opts.sort((a,c)=>{\n    const da=shortestMazeDistance(b.x+a.x,b.y+a.y,t.x,t.y)+Math.random()*.03;\n    const dc=shortestMazeDistance(b.x+c.x,b.y+c.y,t.x,t.y)+Math.random()*.03;\n    return da-dc;\n  });\n  b.desired=opts[0];\n}\nfunction updateBurglars(dt){\n  for(const b of state.burglars){\n    if(b.respawn>0){\n      b.respawn-=dt;\n      if(b.respawn<=0 && b.pendingRespawn){\n        b.respawn=0;\n        placeBurglarInSafeRoom(b);\n      }\n      continue;\n    }\n    if(b.flashed){\n      b.dir=DIRS.none;\n      b.desired=DIRS.none;\n      b.flashRecover=Math.max(0,(Number.isFinite(b.flashRecover)?b.flashRecover:30)-dt);\n\n      if(b.flashRecover<=0){\n        b.flashed=false;\n        b.flashRecover=0;\n        b.flashOwner=null;\n\n        const rx=Math.round(b.x), ry=Math.round(b.y);\n        const dx=rx-b.x, dy=ry-b.y;\n\n        \/\/ Herstart vanaf exact dezelfde plaats. Als hij tussen twee tegelcentra\n        \/\/ bevroor, loopt hij eerst naar het dichtstbijzijnde centrum.\n        if(Math.abs(dx)>.02 && Math.abs(b.y-ry)<.18){\n          b.dir=dx>0?DIRS.right:DIRS.left;\n          b.desired=b.dir;\n        }else if(Math.abs(dy)>.02 && Math.abs(b.x-rx)<.18){\n          b.dir=dy>0?DIRS.down:DIRS.up;\n          b.desired=b.dir;\n        }else{\n          b.x=rx;\n          b.y=ry;\n          chooseBurglarDir(b);\n          b.dir=b.desired;\n        }\n\n        \/\/ Een lopende automatische jacht op dit doel vervalt zodra hij herleeft.\n        if(state.huntTarget===b.index){\n          state.huntTarget=null;\n          state.navTarget=null;\n          if(state.player) state.player.route=[];\n        }\n        for(const cam of state.cameras||[]){\n          if(cam?.autoHuntTarget===b.index){\n            cam.autoHuntTarget=null;\n            cam.route=[];\n            if(cam!==state.player) startBackgroundSurveillance(cam);\n          }\n        }\n\n        showCompactPopup(\n          state.lang==='fr'?'CAMBRIOLEUR R\u00c9ACTIV\u00c9':state.lang==='en'?'BURGLAR REVIVED':'INBREKER WEER ACTIEF',\n          '30s',\n          650\n        );\n      }\n      continue;\n    }\n    if(b.stun>0){ b.stun-=dt; continue; }\n    const rx=Math.round(b.x), ry=Math.round(b.y);\n    if(Math.abs(b.x-rx)<.001 && Math.abs(b.y-ry)<.001){\n      b.x=rx; b.y=ry;\n      if(!forceBurglarTowardDiamond(b)) chooseBurglarDir(b);\n    }\n\n    const base=(3.48+Math.min(1.65,(state.level-1)*.14))*levelConfig().speed;\n    const pressureSpeed=burglarDrawnToDiamondRoom(b)?base*1.04:base;\n    moveActor(b,state.exposed>0?pressureSpeed*.72:pressureSpeed,dt,false);\n  }\n}\n\n\n\nfunction currentDiamondRoom(){\n  if(state.diamondRoom) return state.diamondRoom;\n  if(state.diamond){\n    const found=(state.levelRooms||[]).find(r=>roomContainsPoint(r,state.diamond.x,state.diamond.y,.01));\n    if(found){\n      state.diamondRoom={...found};\n      return state.diamondRoom;\n    }\n  }\n\n  \/\/ Alleen als defensieve fallback voor een toekomstige plattegrond zonder\n  \/\/ geregistreerde kamer: zet de oude configrechthoek om naar een kamerobject.\n  const fallback=levelConfig().diamondRoom;\n  if(Array.isArray(fallback) && fallback.length===4){\n    const [x1,y1,x2,y2]=fallback;\n    return {x:x1,y:y1,w:x2-x1+1,h:y2-y1+1,index:-1};\n  }\n  return null;\n}\n\nfunction pointInsideDiamondRoom(x,y,pad=.38){\n  const room=currentDiamondRoom();\n  if(!room) return false;\n  \/\/ De kamer is in tegelco\u00f6rdinaten opgeslagen. De extra ~0.4 tegel marge\n  \/\/ vangt de fysieke doorgang\/deuropening op zonder aangrenzende gangen als\n  \/\/ onderdeel van de kamer te behandelen.\n  return roomContainsPoint(room,x,y,pad);\n}\n\nfunction cameraInsideDiamondRoom(cam){\n  return !!cam && cam.alive!==false && pointInsideDiamondRoom(cam.x,cam.y,.38);\n}\n\nfunction burglarInsideDiamondRoom(b){\n  return !!b && b.respawn<=0 && pointInsideDiamondRoom(b.x,b.y,.26);\n}\n\nfunction updateDiamondIntruderThreat(dt){\n  if(state.phase!=='playing' || !state.diamond || state.diamond.stolen) return;\n\n  const occupied=state.burglars.some(burglarInsideDiamondRoom);\n  state.diamondThreat=occupied;\n\n  if(!occupied){\n    if(state.diamondIntruderActive){\n      state.diamondIntruderActive=false;\n      state.diamondIntruderTime=60;\n      if(diamondIntruderBadge) diamondIntruderBadge.classList.remove('show');\n    }\n    return;\n  }\n\n  if(!state.diamondIntruderActive){\n    state.diamondIntruderActive=true;\n    state.diamondIntruderTime=60;\n    state.banner='DIAMOND DANGER';\n    state.bannerTime=.75;\n  }\n\n  state.diamondIntruderTime=Math.max(0,state.diamondIntruderTime-dt);\n  if(diamondIntruderBadge){\n    diamondIntruderBadge.classList.add('show');\n    diamondIntruderLabel.textContent=L().intruderDiamondRoom;\n    diamondIntruderValue.textContent=`${Math.ceil(state.diamondIntruderTime)}s`;\n  }\n\n  if(state.diamondIntruderTime<=0){\n    diamondRoomCompromised();\n  }\n}\n\nfunction diamondRoomCompromised(){\n  if(state.phase!=='playing') return;\n  state.diamond.stolen=true;\n  state.phase='gameover';\n  state.flashHeld=false;\n  state.flashFx=0;\n  state.diamondGuardActive=false;\n  state.diamondGuardTime=60;\n  state.diamondIntruderActive=false;\n  state.diamondIntruderTime=60;\n  if(diamondGuardBadge) diamondGuardBadge.classList.remove('show');\n  if(diamondIntruderBadge) diamondIntruderBadge.classList.remove('show');\n\n  if(state.player){\n    state.player.dir=DIRS.none;\n    state.player.desired=DIRS.none;\n    state.player.route=[];\n  }\n\n  state.banner='DIAMOND STOLEN';\n  state.bannerTime=1.2;\n  sfx('hit');\n  renderScores();\n  updateHUD();\n  overlay.classList.remove('celebrate');\n\n  const d=L();\n  overlayEyebrow.textContent=d.diamondRoomLost;\n  overlayTitle.innerHTML=`<span class=\"white\">${d.diamond}<\/span> <b>${d.lockdown}<\/b>`;\n  overlayText.innerHTML=state.lang==='fr'\n    ?`Un cambrioleur est rest\u00e9 <b>60 secondes sans interruption<\/b> dans la salle du diamant. Vous aviez captur\u00e9 <b>${state.eatenCount}\/${state.eatGoal}<\/b> cambrioleurs.`\n    :state.lang==='en'\n      ?`A burglar remained in the diamond room for <b>60 continuous seconds<\/b>. You had eaten <b>${state.eatenCount}\/${state.eatGoal}<\/b> burglars.`\n      :`Een inbreker bleef <b>60 seconden onafgebroken<\/b> in de diamantkamer. Je had <b>${state.eatenCount}\/${state.eatGoal}<\/b> inbrekers opgegeten.`;\n\n  introTips.style.display='none';\n  startBtn.textContent=d.restart;\n  overlay.classList.remove('hidden');\n  statusLine.textContent=d.diamondRoomLost;\n  \n}\n\nfunction updateDiamondGuard(dt){\n  if(state.phase!=='playing' || !state.diamond || state.diamond.stolen) return;\n\n  const guarded=(state.cameras||[]).some(cameraInsideDiamondRoom);\n  if(!guarded){\n    if(state.diamondGuardActive){\n      state.diamondGuardActive=false;\n      state.diamondGuardTime=60;\n      state.diamondPressureShown=0;\n      if(diamondGuardBadge) diamondGuardBadge.classList.remove('show');\n\n      \/\/ Alle inbrekers vallen onmiddellijk terug op hun normale routekeuze.\n      for(const b of state.burglars){\n        if(b && b.respawn<=0 && !b.flashed){\n          const rx=Math.round(b.x),ry=Math.round(b.y);\n          if(Math.abs(b.x-rx)<.001 && Math.abs(b.y-ry)<.001) chooseBurglarDir(b);\n        }\n      }\n    }\n    return;\n  }\n\n  if(!state.diamondGuardActive){\n    state.diamondGuardActive=true;\n    state.diamondGuardTime=60;\n    state.diamondPressureShown=0;\n    if(diamondGuardBadge){\n      diamondGuardBadge.classList.add('show');\n      diamondGuardLabel.textContent=L().guardDiamond;\n      diamondGuardValue.textContent='60s';\n    }\n  }\n\n  state.diamondGuardTime=Math.max(0,state.diamondGuardTime-dt);\n\n  const pressureNow=diamondPressureCount();\n  if(pressureNow>state.diamondPressureShown){\n    state.diamondPressureShown=pressureNow;\n    showCompactPopup(\n      state.lang==='fr'?'PRESSION SUR LE DIAMANT':state.lang==='en'?'DIAMOND PRESSURE':'DRUK OP DIAMANT',\n      `${pressureNow}\/${diamondPressureEligibleBurglars().length}`,\n      700\n    );\n  }\n  if(diamondGuardBadge){\n    diamondGuardBadge.classList.add('show');\n    diamondGuardLabel.textContent=L().guardDiamond;\n    diamondGuardValue.textContent=`${Math.ceil(state.diamondGuardTime)}s`;\n  }\n\n  if(state.diamondGuardTime<=0){\n    diamondGuardTimeVictory();\n  }\n}\n\nfunction diamondGuardTimeVictory(){\n  if(state.phase!=='playing') return;\n  state.score+=5000;\n  state.winReason='diamondGuard';\n  state.diamondGuardActive=false;\n  state.diamondGuardTime=60;\n  state.diamondIntruderActive=false;\n  state.diamondIntruderTime=60;\n  if(diamondGuardBadge) diamondGuardBadge.classList.remove('show');\n  if(diamondIntruderBadge) diamondIntruderBadge.classList.remove('show');\n  showCompactPopup(\n    state.lang==='fr'?'OBJECTIF DIAMANT ATTEINT':state.lang==='en'?'DIAMOND GOAL ACHIEVED':'DIAMOND GOAL ACHIEVED',\n    '60 \/ 60',\n    1100\n  );\n  completeCurrentLevel('diamondGuard');\n}\n\nfunction updateDiamond(dt){\n  const d=state.diamond, p=state.player; if(!d||!p||d.stolen) return;\n  d.pulse=(d.pulse||0)+dt;\n  if(!d.found && dist(p,d)<2.25){\n    d.found=true; state.banner='DIAMOND FOUND'; state.bannerTime=1.3; state.score+=500; sfx('bonus');\n    statusLine.textContent=state.lang==='fr'?'Diamant trouv\u00e9 \u2014 surveillez sa salle pendant 60 secondes et emp\u00eachez les cambrioleurs d\u2019y rester.':state.lang==='en'?'Diamond found \u2014 guard its room for 60 seconds and keep burglars from holding it.':'Diamant gevonden \u2014 bewaak de kamer 60 seconden en laat geen inbreker er 60 seconden blijven.';\n  }\n}\nfunction diamondStolen(){\n  diamondRoomCompromised();\n}\n\nfunction collectTile(){\n  const p=state.player, x=Math.round(p.x), y=Math.round(p.y);\n  if(Math.abs(p.x-x)>.13 || Math.abs(p.y-y)>.13) return;\n  const cell=state.map[y]?.[x];\n  if(cell==='.'){\n    state.map[y][x]=' '; state.score+=10; state.dots--; state.collected++; sfx('dot');\n  }else if(cell==='o'){\n    state.map[y][x]=' '; state.dots--; state.collected++;\n    if(orbCanCreateCamera()){\n      state.score+=50;\n      sfx('orb');\n      spawnCameraFromOrb(x,y);\n    }\n  }\n}\n\n\nfunction lineClear(x1,y1,x2,y2){\n  const d=Math.hypot(x2-x1,y2-y1), steps=Math.max(1,Math.ceil(d\/.16));\n  for(let i=1;i<steps;i++){\n    const t=i\/steps, x=x1+(x2-x1)*t, y=y1+(y2-y1)*t;\n    if(!isPassable(Math.round(x),Math.round(y))) return false;\n  }\n  return true;\n}\nfunction burglarVisibleInFacingCorridor(b,maxRange){\n  const p=state.player, f=p.facing||DIRS.right;\n  const vx=b.x-p.x, vy=b.y-p.y;\n  const forward=vx*f.x+vy*f.y;\n  const side=Math.abs(vx*(-f.y)+vy*f.x);\n  return forward>.15 && forward<=maxRange+.35 && side<.48 && lineClear(p.x,p.y,b.x,b.y);\n}\nfunction useFlash(auto=false){\n  if(state.phase!=='playing' || state.paused || state.flashCooldown>0) return false;\n  const p=state.player, fullCorridor=state.sentry || auto;\n  const maxRange=fullCorridor?corridorRangeTiles(p,p.facing||DIRS.right):5.4;\n  state.flashCooldown=fullCorridor?3.6:5.5; state.flashFx=.92; state.flashPulse=0; sfx('flash');\n  let hits=0;\n  for(const b of state.burglars){\n    if(b.respawn>0) continue;\n    let visible=false;\n    if(fullCorridor){\n      visible=burglarVisibleInFacingCorridor(b,maxRange);\n    }else{\n      const f=cameraForwardVector(p), vx=b.x-p.x, vy=b.y-p.y, r=Math.hypot(vx,vy);\n      if(r>=.01 && r<=maxRange){\n        const dot=(vx\/r)*f.x+(vy\/r)*f.y;\n        visible=dot>.74 && lineClear(p.x,p.y,b.x,b.y);\n      }\n    }\n    if(visible){\n      const firstFlash=markBurglarFlashed(b); if(firstFlash){ state.score+=auto?175:125; registerSuccessfulFlash(); } hits++; sfx('stun');\n    }\n  }\n  if(hits){\n    state.banner=auto?`AUTO FLASH \u00d7${hits}`:`FLASH HIT \u00d7${hits}`; state.bannerTime=.8; state.shake=.15;\n    if(auto && !state.flashHeld){\n      state.sentry=false; state.sentryPending=false; state.sentryDetected=false; state.sentryHold=0;\n      if(sentryTouch) sentryTouch.classList.remove('active');\n      startFlashedHunt();\n    }\n  }\n  return true;\n}\n\n\n\nfunction recoveryRoomSeedsForLevel(level=state.level){\n  const sets={\n    1:[{x:4,y:4},{x:25,y:4},{x:5,y:34},{x:25,y:34}],\n    2:[{x:4,y:3},{x:25,y:3},{x:15,y:12},{x:4,y:24},{x:25,y:24},{x:5,y:35},{x:25,y:34}],\n    3:[{x:5,y:5},{x:14,y:5},{x:25,y:6},{x:6,y:20},{x:16,y:22},{x:25,y:31}],\n    4:[{x:4,y:5},{x:13,y:9},{x:24,y:5},{x:6,y:28},{x:18,y:30},{x:26,y:33}],\n    5:[{x:5,y:4},{x:15,y:6},{x:25,y:5},{x:5,y:20},{x:25,y:21},{x:14,y:34}]\n  };\n  return sets[level]||sets[2];\n}\n\nfunction spawnRecoveryCameraOrbNear(cam){\n  if(!cam || state.level<2) return false;\n  const cfg=levelConfig();\n  if((state.cameras?.length||0)>=cfg.maxCams) return false;\n\n  \/\/ Als er al een geel bolletje ligt dat een camera kan maken, is geen extra nodig.\n  for(let y=0;y<MAZE_H;y++) for(let x=0;x<MAZE_W;x++){\n    if(state.map[y]?.[x]==='o') return false;\n  }\n\n  const seeds=recoveryRoomSeedsForLevel();\n  const candidates=[];\n  for(const seed of seeds){\n    const p=nearestPassable(seed.x,seed.y);\n    if(!p) continue;\n    if(state.map[p.y]?.[p.x]==='#') continue;\n    const d=Math.hypot(p.x-cam.x,p.y-cam.y);\n    candidates.push({x:p.x,y:p.y,d});\n  }\n  if(!candidates.length) return false;\n\n  \/\/ Kies een nabijgelegen kamer, maar niet exact op de gestorven camera.\n  candidates.sort((a,b)=>a.d-b.d);\n  const chosen=candidates.find(c=>c.d>2.5) || candidates[0];\n  state.map[chosen.y][chosen.x]='o';\n  state.dots++;\n  state.initialDots++;\n  return true;\n}\n\nfunction chooseHandoffCamera(fromCamera){\n  const survivors=(state.cameras||[]).filter(c=>c&&c.alive!==false&&c!==fromCamera);\n  if(!survivors.length) return null;\n  if(!fromCamera) return survivors[0];\n  survivors.sort((a,b)=>dist(a,fromCamera)-dist(b,fromCamera));\n  return survivors[0];\n}\n\nfunction destroyCamera(cam){\n  if(!cam || cam.alive===false || !(state.cameras||[]).includes(cam)) return false;\n\n  const wasActive=cam===state.player;\n  const deadId=cam.id;\n  cam.alive=false;\n  cam.route=[];\n  cam.dir=DIRS.none;\n  cam.desired=DIRS.none;\n  cam.autoSurveillance=false;\n  cam.autoHuntTarget=null;\n\n  \/\/ Geflashte inbrekers die bij deze camera hoorden blijven geflasht,\n  \/\/ maar worden vrijgegeven zodat een overgebleven camera ze kan afwerken.\n  for(const b of state.burglars){\n    if(b.flashOwner===deadId) b.flashOwner=null;\n  }\n\n  state.cameras=state.cameras.filter(c=>c!==cam);\n  state.shake=.42;\n  sfx('hit');\n  updateHUD();\n\n  if(!state.cameras.length){\n    state.player=null;\n    state.diamondIntruderActive=false; state.diamondIntruderTime=60; if(diamondIntruderBadge) diamondIntruderBadge.classList.remove('show');\n    state.banner='ALL CAMERAS OFFLINE';\n    state.bannerTime=1.1;\n    gameOver();\n    return true;\n  }\n\n  const recoveryOrbSpawned=spawnRecoveryCameraOrbNear(cam);\n  state.banner=state.lang==='fr'?'CAM\u00c9RA HORS LIGNE':state.lang==='en'?'CAMERA OFFLINE':'CAMERA OFFLINE';\n  state.bannerTime=.9;\n\n  if(wasActive){\n    state.sentry=false;\n    state.sentryPending=false;\n    state.sentryDetected=false;\n    state.surveillanceReturn=false;\n    state.movementFlashActive=false;\n    state.stationaryTimer=0;\n    state.huntTarget=null;\n    state.navTarget=null;\n    state.manualMode=false;\n    state.flashHeld=false;\n    state.flashFx=0;\n\n    const next=chooseHandoffCamera(cam) || state.cameras[0];\n    stopBackgroundSurveillance(next);\n    state.player=next;\n    next.route=[];\n    next.dir=DIRS.none;\n    next.desired=DIRS.none;\n\n    \/\/ Een korte bescherming voorkomt dat de nieuwe camerafocus op exact hetzelfde\n    \/\/ frame opnieuw wordt vernietigd.\n    state.invuln=1.15;\n    state.viewPanX=0;\n    state.viewPanY=0;\n    centerView(true);\n    resetNavigationWatch();\n\n    statusLine.textContent=state.lang==='fr'\n      ?`Cam\u00e9ra hors ligne \u2014 contr\u00f4le transf\u00e9r\u00e9 automatiquement \u00e0 la cam\u00e9ra ${next.cameraIndex+1}.`\n      :state.lang==='en'\n        ?`Camera offline \u2014 control automatically transferred to camera ${next.cameraIndex+1}.`\n        :`Camera offline \u2014 beeld en bediening automatisch overgenomen door camera ${next.cameraIndex+1}.`;\n\n    showCompactPopup(\n      state.lang==='fr'?'CAM\u00c9RA HORS LIGNE':state.lang==='en'?'CAMERA OFFLINE':'CAMERA OFFLINE',\n      recoveryOrbSpawned\n        ?(state.lang==='fr'?'BOULE JAUNE DISPONIBLE':state.lang==='en'?'YELLOW ORB AVAILABLE':'GEEL BOLLETJE BESCHIKBAAR')\n        :'',\n      1000\n    );\n\n    \/\/ De nieuwe actieve camera staat stil: dus meteen terug naar de vereenvoudigde regel.\n    setTimeout(()=>{\n      if(state.phase==='playing' && state.player===next && next.alive!==false){\n        enterAutomaticSurveillance();\n      }\n    },80);\n  }else{\n    statusLine.textContent=state.lang==='fr'\n      ?`Une cam\u00e9ra autonome est hors ligne. ${state.cameras.length} cam\u00e9ra(s) restent actives.`\n      :state.lang==='en'\n        ?`An autonomous camera is offline. ${state.cameras.length} camera(s) remain active.`\n        :`Een autonome camera is offline. Nog ${state.cameras.length} camera('s) actief.`;\n\n    showCompactPopup(\n      state.lang==='fr'?'CAM\u00c9RA HORS LIGNE':state.lang==='en'?'CAMERA OFFLINE':'CAMERA OFFLINE',\n      recoveryOrbSpawned\n        ?(state.lang==='fr'?'BOULE JAUNE DISPONIBLE':state.lang==='en'?'YELLOW ORB AVAILABLE':'GEEL BOLLETJE BESCHIKBAAR')\n        :'',\n      1000\n    );\n  }\n\n  removeUnusedCameraOrbs();\n  return true;\n}\n\nfunction cameraCollisionWithBurglar(cam,b){\n  return !!cam && cam.alive!==false && !!b && b.respawn<=0 && dist(cam,b)<.54;\n}\n\nfunction resetPositions(){\n  const cfg=levelConfig(), spawn=nearestPassable(cfg.spawn[0],cfg.spawn[1]), d=chooseInitialDir(spawn);\n  const active=state.player || state.cameras[0];\n  if(active){\n    active.x=spawn.x; active.y=spawn.y; active.dir=DIRS.none; active.desired=DIRS.none; active.facing=d===DIRS.none?DIRS.right:d; active.visualAngle=facingAngle(active.facing); active.route=[];\n    createCameraFeet(active);\n    state.player=active;\n  }\n  state.burglarSpawnSerial=0; state.burglars=Array.from({length:cfg.burglarCount},(_,i)=>makeBurglar(i)); state.invuln=1.6; state.exposed=0; state.exposedCombo=0; state.sentry=false; state.sentryPending=false; state.sentryDetected=false; state.surveillanceReturn=false; state.surveillanceSweepDir=1; state.surveillanceWallSweep=false; state.surveillanceWallNormals=[]; state.manualMode=false; state.movementFlashActive=false; state.stationaryTimer=0; state.flashHeld=false; state.flashTurnCooldown=0; state.flashResumeDir=null; state.flashResumeDesired=null; state.flashHoldTagged=Array(state.burglars.length).fill(false); state.huntTarget=null; if(sentryTouch)sentryTouch.classList.remove('active'); centerView(true); resetNavigationWatch();\n}\nfunction collisions(){\n  if(state.invuln>0 || state.phase!=='playing') return;\n\n  \/\/ Eerst de actieve camera: die behoudt de bestaande eet-\/capturelogica.\n  if(state.player && state.player.alive!==false){\n    for(const b of state.burglars){\n      if(!cameraCollisionWithBurglar(state.player,b)) continue;\n      if(b.flashed){\n        const gain=250;\n        state.score+=gain;\n        b.flashed=false;\n        b.flashRecover=0;\n        b.flashOwner=null;\n        b.stun=0;\n        queueBurglarRespawn(b,.85);\n        state.banner=`INTRUDER EATEN +${gain}`; state.bannerTime=.85; state.shake=.14; sfx('capture');\n        registerEatenBurglar();\n        if(state.phase!=='playing') return;\n        state.huntTarget=null;\n        state.navTarget=null;\n        state.player.route=[];\n        state.player.dir=DIRS.none;\n        state.player.desired=DIRS.none;\n        finishFlashedHuntIfNeeded();\n      }else if(state.exposed>0){\n        const gain=200*Math.pow(2,Math.min(3,state.exposedCombo++)); state.score+=gain;\n        queueBurglarRespawn(b,.7);\n        state.banner=`INTRUDER TAGGED +${gain}`; state.bannerTime=.8; state.shake=.14; sfx('capture');\n      }else if(b.stun<=0){\n        destroyCamera(state.player);\n      }\n      return;\n    }\n  }\n\n  \/\/ Daarna de niet-actieve camera's. Ook zij kunnen door een levende inbreker\n  \/\/ uitgeschakeld worden terwijl ze autonoom bewaken.\n  const backgroundCameras=[...(state.cameras||[])].filter(c=>c!==state.player&&c.alive!==false);\n  for(const cam of backgroundCameras){\n    for(const b of state.burglars){\n      if(!cameraCollisionWithBurglar(cam,b)) continue;\n\n      \/\/ Een geflashte inbreker die deze camera bereikt wordt gewoon opgegeten.\n      if(b.flashed){\n        const gain=250;\n        state.score+=gain;\n        b.flashed=false;\n        b.flashRecover=0;\n        b.flashOwner=null;\n        b.stun=0;\n        queueBurglarRespawn(b,.85);\n        state.banner=`INTRUDER EATEN +${gain}`;state.bannerTime=.75;state.shake=.11;sfx('capture');\n        registerEatenBurglar();\n        if(state.phase!=='playing') return;\n        startBackgroundHunt(cam);\n      }else if(b.stun<=0){\n        destroyCamera(cam);\n      }\n      return;\n    }\n  }\n}\n\n\/\/ Legacy naam behouden voor eventuele oude codepaden: de actieve camera sterft nu\n\/\/ werkelijk in plaats van een algemene 'life' te verliezen.\nfunction loseLife(){\n  if(state.player) destroyCamera(state.player);\n}\nfunction showMissionCompleteOverlay(){\n  if(state.phase!=='finishing') return;\n  state.phase='complete';\n  renderScores();updateHUD();overlay.classList.add('celebrate'); const d=L();\n  if(state.winReason==='diamondGuard'){\n    overlayEyebrow.textContent=d.diamondSecured;\n    overlayTitle.innerHTML=`<span class=\"white\">${d.diamond}<\/span> <b>${d.goalAchieved}<\/b>`;\n    overlayText.innerHTML=state.lang==='fr'\n      ?`Vous avez surveill\u00e9 la salle du diamant pendant <b>60 secondes sans interruption<\/b>. La mission est termin\u00e9e.`\n      :state.lang==='en'\n        ?`You guarded the diamond room for <b>60 continuous seconds<\/b>. The mission is complete.`\n        :`Je hebt de diamantkamer <b>60 seconden onafgebroken bewaakt<\/b>. De missie is voltooid.`;\n  }else{\n    overlayEyebrow.textContent=`${d.missionComplete} \/\/ ${state.eatenCount} \/ ${state.eatGoal}`;\n    overlayTitle.innerHTML=`<span class=\"white\">${d.goalAchieved.split(' ')[0]}<\/span> <b>${d.goalAchieved.split(' ').slice(1).join(' ')||d.goalAchieved}<\/b>`;\n    overlayText.innerHTML=state.lang==='fr'?`Vous avez captur\u00e9 <b>${state.eatGoal} cambrioleurs au niveau 5<\/b>. La mission est termin\u00e9e.`:state.lang==='en'?`You ate <b>${state.eatGoal} burglars in Level 5<\/b>. The mission is complete.`:`Je hebt in Level 5 <b>${state.eatGoal} inbrekers opgegeten<\/b>. De missie is voltooid.`;\n  }\n  introTips.style.display='none';startBtn.style.display='';startBtn.textContent=d.restart;overlay.classList.remove('hidden');statusLine.textContent=d.missionComplete;\n}\n\nfunction missionComplete(reason='captures'){\n  if(state.phase!=='playing') return;\n  state.winReason=reason||state.winReason||'captures';\n  state.goalReachedPending=false;\n  state.diamondGuardActive=false;\n  if(diamondGuardBadge) diamondGuardBadge.classList.remove('show');\n  state.phase='finishing'; state.flashHeld=false; state.flashFx=0; state.sentry=false; state.sentryPending=false; state.sentryDetected=false; state.surveillanceReturn=false; state.huntTarget=null; state.navTarget=null;\n  if(sentryTouch) sentryTouch.classList.remove('active');\n  if(state.player){state.player.route=[];state.player.dir=DIRS.none;state.player.desired=DIRS.none;}\n  overlay.classList.add('hidden'); overlay.classList.remove('celebrate');\n  state.completeRevealTimer=1.05;\n  renderScores();updateHUD();updateSurveillanceStartButton();\n  statusLine.textContent=state.lang==='fr'?`${state.eatenCount} \/ ${state.eatGoal} \u2014 objectif atteint.`:state.lang==='en'?`${state.eatenCount} \/ ${state.eatGoal} \u2014 goal achieved.`:`${state.eatenCount} \/ ${state.eatGoal} \u2014 doel behaald.`;\n}\nfunction gameOver(){\n  state.phase='gameover'; state.sentry=false; state.sentryPending=false; state.surveillanceReturn=false; if(sentryTouch)sentryTouch.classList.remove('active'); renderScores();updateHUD();overlay.classList.remove('celebrate');const d=L();overlayEyebrow.textContent=d.gameOver;overlayTitle.innerHTML=`<span class=\"white\">CCTV<\/span> <b>${d.lockdown}<\/b>`;\n  overlayText.innerHTML=state.lang==='fr'?`Toutes les cam\u00e9ras sont hors ligne au niveau <b>${state.level}<\/b>.`:state.lang==='en'?`All cameras are offline in level <b>${state.level}<\/b>.`:`Alle camera's zijn offline in level <b>${state.level}<\/b>.`;\n  introTips.style.display='none';startBtn.style.display='';startBtn.textContent=d.restart;overlay.classList.remove('hidden');statusLine.textContent=d.gameOver;\n}\n\nfunction centerView(immediate=false,dt=.016){\n  if(!state.player) return;\n  const px=(state.player.x+.5)*TILE, py=(state.player.y+.5)*TILE;\n  const tx=clamp(px-VIEW_W\/2+state.viewPanX,0,Math.max(0,WORLD_W-VIEW_W));\n  const ty=clamp(py-VIEW_H\/2+state.viewPanY,0,Math.max(0,WORLD_H-VIEW_H));\n  if(immediate){ state.viewX=tx; state.viewY=ty; return; }\n  const k=1-Math.pow(.0008,dt);\n  state.viewX+=(tx-state.viewX)*k; state.viewY+=(ty-state.viewY)*k;\n}\n\nfunction panView(dir){\n  if(!state.player || !['ready','playing'].includes(state.phase)) return;\n  \/\/ Elke bewuste schermbediening stopt Surveillance mode van de actieve camera.\n  cancelSentryForDirectControl();\n  const stepX=VIEW_W*.24,stepY=VIEW_H*.24;\n  if(dir==='left')state.viewPanX-=stepX;\n  if(dir==='right')state.viewPanX+=stepX;\n  if(dir==='up')state.viewPanY-=stepY;\n  if(dir==='down')state.viewPanY+=stepY;\n  state.viewPanX=clamp(state.viewPanX,-WORLD_W,WORLD_W);\n  state.viewPanY=clamp(state.viewPanY,-WORLD_H,WORLD_H);\n  centerView(true);\n}\n\nfunction updateHUD(){\n  levelEl.textContent=String(state.level).padStart(2,'0');\n  livesEl.textContent=`\ud83d\udcf9 \u00d7${state.cameras?.length||0}`;\n  const progress=clamp((state.eatenCount\/state.eatGoal)*100,0,100);\n  progressEl.textContent=`${state.eatenCount} \/ ${state.eatGoal}`;\n  progressFill.style.width=`${progress}%`;\n  if(goalCount) goalCount.textContent=`${state.eatenCount} \/ ${state.eatGoal}`;\n  updateLevelGoalText();\n  updateLevelDisplay();\n  updateSurveillanceStartButton();\n}\n\nfunction update(dt){\n  if(state.phase==='finishing'){\n    state.completeRevealTimer=Math.max(0,state.completeRevealTimer-dt);\n    centerView(false,dt);\n    if(state.completeRevealTimer<=0) showMissionCompleteOverlay();\n    return;\n  }\n  if(state.phase==='ready'){\n    state.readyTimer-=dt; state.bannerTime=Math.max(state.bannerTime,Math.min(1,state.readyTimer));\n    centerView(false,dt);\n    if(state.readyTimer<=0){\n      state.phase='playing'; state.banner=''; state.bannerTime=0;\n      enterAutomaticSurveillance();\n      updateDiamondGuard(0);\n    }\n    return;\n  }\n  if(state.phase!=='playing' || state.paused){ centerView(false,dt); return; }\n\n  state.invuln=Math.max(0,state.invuln-dt); state.exposed=Math.max(0,state.exposed-dt);\n  state.flashTurnCooldown=Math.max(0,state.flashTurnCooldown-dt);\n\n  \/\/ Surveillance en beweging gebruiken allebei een continu brandende flash.\n  if(state.flashHeld || state.sentry || state.movementFlashActive){\n    state.flashCooldown=0; state.flashFx=1; state.flashPulse+=dt;\n    if(state.flashHeld) updateHeldFlashTargets();\n  }else{\n    state.flashCooldown=Math.max(0,state.flashCooldown-dt);\n    state.flashFx=Math.max(0,state.flashFx-dt);\n    if(state.flashFx>0) state.flashPulse+=dt;\n  }\n\n  state.shake=Math.max(0,state.shake-dt); state.bannerTime=Math.max(0,state.bannerTime-dt);\n  state.modeTimer-=dt;\n  if(state.modeTimer<=0){ state.chaseMode=!state.chaseMode; state.modeTimer=state.chaseMode?18:2.5; }\n\n  const beforeX=state.player.x,beforeY=state.player.y;\n\n  if(state.flashHeld){\n    state.player.dir=DIRS.none; state.player.desired=DIRS.none;\n  }else if(state.sentry){\n    updateSentry(dt);\n  }else if(state.manualMode){\n    moveActor(state.player,4.75,dt,true);\n  }else{\n    updatePlayerNavigation(dt);\n  }\n\n  const moved=Math.hypot(state.player.x-beforeX,state.player.y-beforeY)>.00015;\n\n  if(!state.flashHeld && !state.sentry){\n    if(moved){\n      state.stationaryTimer=0;\n      state.movementFlashActive=true;\n      state.flashCooldown=0;\n      state.flashFx=1;\n      state.flashPulse+=dt;\n      updateMovementFlashTargets();\n    }else{\n      state.movementFlashActive=false;\n      state.stationaryTimer+=dt;\n\n      const routeActive=Array.isArray(state.player.route)&&state.player.route.length>0;\n      const hunting=state.huntTarget!==null&&state.huntTarget!==undefined;\n      const manualBlocked=state.manualMode && state.player.dir===DIRS.none;\n      const genuinelyStopped=(!routeActive&&!hunting&&state.player.dir===DIRS.none) || manualBlocked;\n\n      \/\/ Een heel korte drempel voorkomt flikkeren op \u00e9\u00e9n overgangsframe.\n      if(genuinelyStopped && state.stationaryTimer>=.08){\n        state.manualMode=false;\n        state.navTarget=null;\n        state.player.route=[];\n        enterAutomaticSurveillance();\n      }\n    }\n  }else if(state.sentry){\n    state.movementFlashActive=false;\n    state.stationaryTimer=0;\n  }\n\n  navigationWatchdog(dt);\n  for(const cam of state.cameras||[]) if(cam!==state.player&&cam.alive!==false) updateBackgroundCamera(cam,dt);\n  updateCameraVisualAngles(dt);\n  for(const cam of state.cameras||[]) if(cam.alive!==false) updateCameraFeet(cam,dt);\n\n  \/\/ Eerst de diamantkamerstatus bijwerken; daarna reageren de inbrekers meteen\n  \/\/ op de actuele bewakingsdruk van dit frame.\n  updateDiamond(dt);\n  updateDiamondGuard(dt);\n  if(state.phase!=='playing') return;\n\n  updateBurglars(dt);\n  updateDiamondIntruderThreat(dt);\n  if(state.phase!=='playing') return;\n  if(!state.sentry) collectTile();\n  collisions();\n  centerView(false,dt);\n  updateHUD();\n}\n\nfunction rr(x,y,w,h,r,fill=true,stroke=false){\n  r=Math.min(r,w\/2,h\/2); ctx.beginPath(); ctx.moveTo(x+r,y); ctx.arcTo(x+w,y,x+w,y+h,r); ctx.arcTo(x+w,y+h,x,y+h,r); ctx.arcTo(x,y+h,x,y,r); ctx.arcTo(x,y,x+w,y,r); ctx.closePath(); if(fill)ctx.fill(); if(stroke)ctx.stroke();\n}\nfunction worldToPx(x,y){ return {x:(x+.5)*TILE,y:(y+.5)*TILE}; }\nfunction drawMaze(){\n  const minX=Math.max(0,Math.floor(state.viewX\/TILE)-1), maxX=Math.min(MAZE_W-1,Math.ceil((state.viewX+VIEW_W)\/TILE)+1);\n  const minY=Math.max(0,Math.floor(state.viewY\/TILE)-1), maxY=Math.min(MAZE_H-1,Math.ceil((state.viewY+VIEW_H)\/TILE)+1);\n  for(let y=minY;y<=maxY;y++) for(let x=minX;x<=maxX;x++) if(state.map[y][x]==='#'){\n    const px=x*TILE, py=y*TILE;\n    ctx.fillStyle='#06131f'; ctx.fillRect(px+1,py+1,TILE-2,TILE-2);\n    ctx.strokeStyle='rgba(54,129,255,.78)'; ctx.lineWidth=1.6; ctx.strokeRect(px+3,py+3,TILE-6,TILE-6);\n    ctx.strokeStyle='rgba(32,216,255,.12)'; ctx.lineWidth=.8; ctx.strokeRect(px+7,py+7,TILE-14,TILE-14);\n  }\n}\nfunction drawDots(){\n  const now=performance.now()\/1000;\n  const minX=Math.max(0,Math.floor(state.viewX\/TILE)-1), maxX=Math.min(MAZE_W-1,Math.ceil((state.viewX+VIEW_W)\/TILE)+1);\n  const minY=Math.max(0,Math.floor(state.viewY\/TILE)-1), maxY=Math.min(MAZE_H-1,Math.ceil((state.viewY+VIEW_H)\/TILE)+1);\n  for(let y=minY;y<=maxY;y++) for(let x=minX;x<=maxX;x++){\n    const c=state.map[y][x], px=(x+.5)*TILE, py=(y+.5)*TILE;\n    if(c==='.'){\n      ctx.beginPath(); ctx.fillStyle='rgba(232,251,255,.88)'; ctx.arc(px,py,2.25,0,TAU); ctx.fill();\n    }else if(c==='o'){\n      const cameraOrb=orbCanCreateCamera();\n      const pulse=(Math.sin(now*5+x+y)+1)\/2;\n      const r=6.3+pulse*1.35;\n\n      if(cameraOrb){\n        \/\/ Dit bolletje kan nog een extra camera opleveren:\n        \/\/ gele kern + koele, pulserende halo\/ringen zodat het meteen herkenbaar is.\n        ctx.save();\n        ctx.globalCompositeOperation='screen';\n\n        ctx.beginPath();\n        ctx.strokeStyle=`rgba(32,216,255,${.28+pulse*.34})`;\n        ctx.lineWidth=1.5+pulse*.9;\n        ctx.shadowColor='#20d8ff';\n        ctx.shadowBlur=18+pulse*10;\n        ctx.arc(px,py,11+pulse*5,0,TAU);\n        ctx.stroke();\n\n        ctx.beginPath();\n        ctx.strokeStyle=`rgba(141,255,117,${.20+(1-pulse)*.30})`;\n        ctx.lineWidth=1.2;\n        ctx.shadowColor='#8dff75';\n        ctx.shadowBlur=12;\n        ctx.arc(px,py,16-pulse*3,0,TAU);\n        ctx.stroke();\n\n        ctx.beginPath();\n        ctx.fillStyle='#fff4a8';\n        ctx.shadowColor='#8dff75';\n        ctx.shadowBlur=20;\n        ctx.arc(px,py,r,0,TAU);\n        ctx.fill();\n\n        ctx.beginPath();\n        ctx.fillStyle='rgba(255,255,255,.92)';\n        ctx.shadowBlur=0;\n        ctx.arc(px-2,py-2,1.8,0,TAU);\n        ctx.fill();\n        ctx.restore();\n      }else{\n        \/\/ Gele bolletjes bestaan alleen wanneer ze nog een camera kunnen opleveren.\n        continue;\n      }\n    }\n  }\n}\nfunction facingAngle(f){ if(f===DIRS.down)return Math.PI\/2;if(f===DIRS.left)return Math.PI;if(f===DIRS.up)return-Math.PI\/2;return 0; }\nfunction drawCameraFeet(p){\n  if(!p.feet) return;\n  const body=worldToPx(p.x,p.y);\n  const {f}=localFrameForPlayer(p);\n  const n=p.feet.length;\n  p.feet.forEach((foot,index)=>{\n    if(!foot.anchor) return;\n    const target=worldToPx(foot.anchor.x,foot.anchor.y);\n    const spread=n<=1?0:(index\/(n-1)-.5)*15;\n    const attach={x:body.x-f.y*spread-f.x*4,y:body.y+f.x*spread-f.y*4};\n    const dx=target.x-attach.x,dy=target.y-attach.y;\n    const len=Math.hypot(dx,dy)||1;\n    const nx=-dy\/len,ny=dx\/len;\n    const lift=(foot.t<1?Math.sin(foot.t*Math.PI)*8:0)*(index%2? -1:1);\n    const joint={x:(attach.x+target.x)\/2+nx*(8+lift),y:(attach.y+target.y)\/2+ny*(8+lift)};\n\n    const firstCam=p.cameraIndex===0;\n    ctx.save();\n    ctx.lineCap='round';ctx.lineJoin='round';\n    ctx.shadowColor=firstCam?'rgba(141,255,117,.45)':p.color;ctx.shadowBlur=6;\n    ctx.strokeStyle=firstCam?'rgba(188,255,176,.92)':p.color;ctx.lineWidth=3.2;\n    ctx.beginPath();ctx.moveTo(attach.x,attach.y);ctx.lineTo(joint.x,joint.y);ctx.lineTo(target.x,target.y);ctx.stroke();\n    ctx.shadowBlur=0;ctx.strokeStyle=firstCam?'#173e1e':'rgba(2,12,18,.86)';ctx.lineWidth=1.1;ctx.stroke();\n\n    const landing=foot.t<1?1-Math.sin(foot.t*Math.PI)*.35:1;\n    ctx.translate(target.x,target.y);ctx.rotate(Math.atan2(dy,dx));ctx.scale(landing,landing);\n    ctx.fillStyle=firstCam?(foot.anchor.synthetic?'#84c77b':'#d9ffd2'):p.color;\n    ctx.strokeStyle=firstCam?'#5bea50':'#efffff';ctx.lineWidth=1.3;\n    rr(-4.8,-3.2,9.6,6.4,2.4,true,true);\n    ctx.beginPath();ctx.fillStyle='#061209';ctx.arc(0,0,1.5,0,TAU);ctx.fill();\n    ctx.restore();\n  });\n}\n\n\nfunction drawNavigationTarget(){\n  if(!state.navTarget) return;\n  const p=worldToPx(state.navTarget.x,state.navTarget.y);\n  const pulse=1+Math.sin(performance.now()\/180)*.12;\n  ctx.save(); ctx.translate(p.x,p.y); ctx.scale(pulse,pulse);\n  ctx.strokeStyle='rgba(32,216,255,.92)'; ctx.lineWidth=1.6;\n  ctx.beginPath(); ctx.arc(0,0,8,0,TAU); ctx.stroke();\n  ctx.beginPath(); ctx.moveTo(-11,0);ctx.lineTo(-5,0);ctx.moveTo(11,0);ctx.lineTo(5,0);ctx.moveTo(0,-11);ctx.lineTo(0,-5);ctx.moveTo(0,11);ctx.lineTo(0,5);ctx.stroke();\n  ctx.restore();\n}\n\nfunction drawCamera(p){\n  drawCameraFeet(p);\n  const active=p===state.player;\n  const firstCam=p.cameraIndex===0;\n  const pos=worldToPx(p.x,p.y);\n  ctx.save(); ctx.translate(pos.x,pos.y);\n  const displayAngle=Number.isFinite(p.visualAngle)?p.visualAngle:(active&&state.sentry?state.surveillanceAngle:(!active&&p.autoSurveillance?p.autoAngle:facingAngle(p.facing)));\n  ctx.rotate(displayAngle);\n\n  if(active){\n    ctx.save();\n    ctx.rotate(-displayAngle);\n    ctx.strokeStyle='rgba(255,255,255,.72)';\n    ctx.lineWidth=1.25;\n    ctx.setLineDash([3,4]);\n    ctx.beginPath();ctx.arc(0,0,19,0,TAU);ctx.stroke();\n    ctx.setLineDash([]);\n    ctx.restore();\n  }\n\n  if(active && state.invuln>0 && Math.floor(performance.now()\/85)%2===0) ctx.globalAlpha=.32;\n  const flashing=(active&&state.flashFx>0)||(!active&&(p.autoSurveillance||p.backgroundTransit));\n  const flashBoost=flashing?22:12;\n  const glow=firstCam?'#8dff75':p.color;\n  ctx.shadowColor=flashing?'#fff6bf':glow; ctx.shadowBlur=flashBoost;\n  ctx.fillStyle=firstCam?'#d8ffd0':p.color; rr(-10,-8,17,16,4,true,false);\n  ctx.fillStyle=firstCam?'#75ee65':p.color; ctx.beginPath(); ctx.moveTo(7,-6);ctx.lineTo(15,-10);ctx.lineTo(15,10);ctx.lineTo(7,6);ctx.closePath();ctx.fill();\n  ctx.fillStyle='#041a08';ctx.beginPath();ctx.arc(-2,0,4.3,0,TAU);ctx.fill();\n  ctx.fillStyle=flashing?'#fffbdc':'#fff';ctx.shadowColor='#fff7b0';ctx.shadowBlur=flashing?18:0;ctx.beginPath();ctx.arc(-3.3,-1.4,flashing?2.2:1.3,0,TAU);ctx.fill();\n  ctx.restore();\n}\nfunction drawBurglar(b){\n  const pos=worldToPx(b.x,b.y), frightened=state.exposed>0, stunned=b.stun>0 || b.flashed;\n  let body=b.color;\n  if(frightened){ const flashing=state.exposed<2 && Math.floor(performance.now()\/140)%2===0; body=flashing?'#f2fbff':'#184fd5'; }\n  ctx.save();ctx.translate(pos.x,pos.y);if(stunned){ctx.rotate(Math.sin(performance.now()\/38)*.08);ctx.globalAlpha=.78;}\n  ctx.shadowColor=body;ctx.shadowBlur=frightened?16:8;ctx.fillStyle=body;rr(-9,-4,18,17,6,true,false);ctx.beginPath();ctx.arc(0,-6,7.5,Math.PI,TAU);ctx.fill();ctx.shadowBlur=0;\n  ctx.fillStyle='#101114';ctx.fillRect(-8,-8,16,5);ctx.fillStyle='#f7ffff';ctx.fillRect(-5,-7,3,2);ctx.fillRect(2,-7,3,2);ctx.fillStyle='#001018';ctx.fillRect(-4,-6,1,1);ctx.fillRect(3,-6,1,1);\n  ctx.strokeStyle='#111';ctx.lineWidth=2;ctx.beginPath();ctx.moveTo(-6,12);ctx.lineTo(-9,16);ctx.moveTo(6,12);ctx.lineTo(9,16);ctx.stroke();\n  if(stunned){ctx.fillStyle='#ffd166';ctx.font='bold 10px monospace';ctx.textAlign='center';ctx.fillText('\u2726',-8,-15);ctx.fillText('\u2726',8,-18);if(b.flashed){ctx.font='bold 8px monospace';ctx.fillText(L().flashed,0,25);}}ctx.restore();\n}\n\nfunction drawDiamond(){\n  const d=state.diamond;if(!d||d.stolen)return; const p=worldToPx(d.x,d.y); const pulse=1+Math.sin((d.pulse||0)*4)*.07;\n  ctx.save();ctx.translate(p.x,p.y);ctx.scale(pulse,pulse);ctx.shadowColor=state.diamondThreat?'#ff4d6d':'#7de8ff';ctx.shadowBlur=24;\n  ctx.fillStyle=state.diamondThreat?'#ff7a91':'#7de8ff';ctx.beginPath();ctx.moveTo(0,-17);ctx.lineTo(15,-5);ctx.lineTo(9,14);ctx.lineTo(0,20);ctx.lineTo(-9,14);ctx.lineTo(-15,-5);ctx.closePath();ctx.fill();\n  ctx.shadowBlur=0;ctx.strokeStyle='#eaffff';ctx.lineWidth=1.5;ctx.beginPath();ctx.moveTo(-15,-5);ctx.lineTo(15,-5);ctx.moveTo(-15,-5);ctx.lineTo(0,20);ctx.moveTo(15,-5);ctx.lineTo(0,20);ctx.moveTo(0,-17);ctx.lineTo(-5,-5);ctx.lineTo(0,20);ctx.moveTo(0,-17);ctx.lineTo(5,-5);ctx.lineTo(0,20);ctx.stroke();\n  ctx.fillStyle='#eaffff';ctx.font='bold 9px monospace';ctx.textAlign='center';ctx.fillText(L().diamond,0,32);ctx.restore();\n}\n\nfunction drawDiamondDirectionArrow(){\n  const d=state.diamond;\n  if(!d || d.stolen || !['ready','playing'].includes(state.phase)) return;\n\n  const threat=state.diamondIntruderActive;\n  const now=performance.now();\n  const threatBlink=threat ? (Math.floor(now\/180)%2===0) : false;\n\n  for(const cam of state.cameras||[]){\n    if(!cam || cam.alive===false) continue;\n\n    const dx=d.x-cam.x, dy=d.y-cam.y;\n    const distance=Math.hypot(dx,dy);\n    if(distance<.35) continue;\n\n    const angle=Math.atan2(dy,dx);\n    const cameraPx=worldToPx(cam.x,cam.y);\n    const sx=cameraPx.x-state.viewX, sy=cameraPx.y-state.viewY;\n\n    \/\/ Alleen pijlen tekenen voor camera's die zich in of vlak bij het zicht bevinden.\n    if(sx<-45||sx>VIEW_W+45||sy<-45||sy>VIEW_H+45) continue;\n\n    const active=cam===state.player;\n    const radius=active?31:27;\n    const ax=sx+Math.cos(angle)*radius, ay=sy+Math.sin(angle)*radius;\n\n    const normalPulse=.48+.16*(.5+.5*Math.sin(now\/260 + (cam.cameraIndex||0)));\n    const alpha=threat ? (threatBlink?.98:.38) : normalPulse;\n    const fill=threat ? '#ff304f' : '#dffcff';\n    const glow=threat ? '#ff163d' : '#7de8ff';\n\n    ctx.save();\n    ctx.translate(ax,ay);\n    ctx.rotate(angle);\n    ctx.globalAlpha=alpha;\n    ctx.shadowColor=glow;\n    ctx.shadowBlur=threat?16:9;\n    ctx.fillStyle=fill;\n    ctx.beginPath();\n    ctx.moveTo(active?8:7,0);\n    ctx.lineTo(-4,-5);\n    ctx.lineTo(-1,0);\n    ctx.lineTo(-4,5);\n    ctx.closePath();\n    ctx.fill();\n\n    ctx.shadowBlur=0;\n    ctx.globalAlpha=threat ? alpha*.72 : .34;\n    ctx.strokeStyle=glow;\n    ctx.lineWidth=threat?1.6:1;\n    ctx.beginPath();\n    ctx.arc(0,0,active?11:9,-.55,.55);\n    ctx.stroke();\n    ctx.restore();\n  }\n}\n\nfunction raycastFlashTiles(x,y,angle,maxTiles){\n  const step=.045, dx=Math.cos(angle), dy=Math.sin(angle);\n  for(let d=.10;d<=maxTiles;d+=step){\n    const tx=Math.round(x+dx*d), ty=Math.round(y+dy*d);\n    if(!isPassable(tx,ty)) return Math.max(.10,d-step*1.4);\n  }\n  return maxTiles;\n}\n\n\nfunction drawBackgroundSurveillanceCone(cam){\n  if(!cam || cam===state.player || (!cam.autoSurveillance && !cam.backgroundTransit)) return;\n  const transit=!!cam.backgroundTransit;\n  const angle=transit?facingAngle(cam.facing||DIRS.right):cam.autoAngle;\n  const p=worldToPx(cam.x,cam.y),maxTiles=transit?7.2:9.2;\n  const spread=transit?.43:.18,halfAngle=Math.atan(spread),rayCount=transit?19:15,rays=[];\n  for(let i=0;i<rayCount;i++){\n    const rel=-halfAngle+(halfAngle*2*i)\/(rayCount-1);\n    rays.push({rel,reach:raycastFlashTiles(cam.x,cam.y,angle+rel,maxTiles)});\n  }\n  ctx.save();ctx.translate(p.x,p.y);ctx.rotate(angle);ctx.globalCompositeOperation='screen';\n  const len=TILE*maxTiles,g=ctx.createLinearGradient(0,0,len,0);\n  g.addColorStop(0,'rgba(255,255,235,.62)');\n  g.addColorStop(.22,'rgba(255,241,150,.28)');\n  g.addColorStop(1,'rgba(255,240,150,0)');\n  ctx.fillStyle=g;ctx.beginPath();ctx.moveTo(8,0);\n  for(const ray of rays){const d=ray.reach*TILE;ctx.lineTo(Math.cos(ray.rel)*d,Math.sin(ray.rel)*d);}\n  ctx.closePath();ctx.fill();\n  ctx.restore();\n}\n\nfunction drawBackgroundSurveillanceCones(){\n  for(const cam of state.cameras||[]) if(cam!==state.player&&cam.alive!==false) drawBackgroundSurveillanceCone(cam);\n}\n\nfunction drawCone(){\n  if(!state.player) return;\n  const p=worldToPx(state.player.x,state.player.y), angle=state.sentry?state.surveillanceAngle:cameraForwardVector(state.player).angle;\n  const active=state.sentry||state.movementFlashActive||state.flashHeld||state.flashFx>0;\n  const sentry=state.sentry;\n  const pulse=active?.90+.10*Math.sin(state.flashPulse*32):1;\n  const maxTiles=sentry?9.2:(active?7.2:5.5), len=TILE*maxTiles, spread=active?(sentry?.20:.43):(sentry?.085:.30);\n  const halfAngle=Math.atan(spread), rayCount=active?23:15, rays=[];\n  for(let i=0;i<rayCount;i++){\n    const rel=-halfAngle+(halfAngle*2*i)\/(rayCount-1);\n    rays.push({rel,reach:raycastFlashTiles(state.player.x,state.player.y,angle+rel,maxTiles)});\n  }\n  const centerReach=raycastFlashTiles(state.player.x,state.player.y,angle,maxTiles);\n  ctx.save();ctx.translate(p.x,p.y);ctx.rotate(angle);\n  if(active) ctx.globalCompositeOperation='screen';\n\n  const g=ctx.createLinearGradient(0,0,len,0);\n  g.addColorStop(0,active?`rgba(255,255,238,${.96*pulse})`:(sentry?(state.sentryDetected?'rgba(255,110,90,.24)':'rgba(141,255,117,.16)'):'rgba(255,246,185,.045)'));\n  g.addColorStop(.18,active?`rgba(255,246,174,${.70*pulse})`:(sentry?(state.sentryDetected?'rgba(255,80,70,.12)':'rgba(32,216,255,.07)'):'rgba(255,246,185,.028)'));\n  g.addColorStop(1,'rgba(255,246,185,0)');\n  ctx.fillStyle=g;ctx.beginPath();ctx.moveTo(8,0);\n  for(const ray of rays){const d=ray.reach*TILE;ctx.lineTo(Math.cos(ray.rel)*d,Math.sin(ray.rel)*d);}\n  ctx.closePath();ctx.fill();\n\n  if(active){\n    \/\/ Felle witte kern van de lamp, eveneens begrensd door de eerstvolgende muur.\n    const coreLen=Math.min(len*.86,centerReach*TILE);\n    const core=ctx.createLinearGradient(0,0,Math.max(10,coreLen),0);core.addColorStop(0,`rgba(255,255,255,${.98*pulse})`);core.addColorStop(.35,`rgba(255,252,210,${.48*pulse})`);core.addColorStop(1,'rgba(255,255,255,0)');\n    ctx.fillStyle=core;ctx.shadowColor='#fff7b0';ctx.shadowBlur=24;ctx.beginPath();ctx.moveTo(9,-4);ctx.lineTo(coreLen,-Math.min(coreLen*.075,9));ctx.lineTo(coreLen,Math.min(coreLen*.075,9));ctx.lineTo(9,4);ctx.closePath();ctx.fill();\n    ctx.shadowBlur=0;\n    \/\/ Halo rond de lens gedurende de volledige activatie.\n    const halo=ctx.createRadialGradient(10,0,0,10,0,TILE*2.2);halo.addColorStop(0,`rgba(255,255,245,${.88*pulse})`);halo.addColorStop(.25,`rgba(255,239,150,${.32*pulse})`);halo.addColorStop(1,'rgba(255,230,120,0)');ctx.fillStyle=halo;ctx.beginPath();ctx.arc(10,0,TILE*2.2,0,TAU);ctx.fill();\n  }\n  ctx.restore();\n}\n\nfunction drawFlashScreenBloom(){\n  if(state.flashFx<=0 || !state.player) return;\n  const p={x:(state.player.x+.5)*TILE-state.viewX,y:(state.player.y+.5)*TILE-state.viewY};\n  const strength=clamp(state.flashFx\/.82,0,1);\n  const glow=ctx.createRadialGradient(p.x,p.y,10,p.x,p.y,260);\n  glow.addColorStop(0,`rgba(255,250,210,${.16*strength})`);glow.addColorStop(1,'rgba(255,250,210,0)');\n  ctx.save();ctx.globalCompositeOperation='screen';ctx.fillStyle=glow;ctx.fillRect(0,0,VIEW_W,VIEW_H);ctx.restore();\n}\nfunction drawScreenHUD(){\n  ctx.save();\n  ctx.fillStyle='rgba(0,10,14,.40)';ctx.fillRect(12,12,176,38);ctx.strokeStyle='rgba(32,216,255,.35)';ctx.strokeRect(12.5,12.5,176,38);\n  ctx.fillStyle='#ff4d6d';ctx.beginPath();ctx.arc(27,31,4,0,TAU);ctx.fill();\n  ctx.fillStyle='#dff8ff';ctx.font='bold 12px monospace';ctx.textBaseline='middle';ctx.fillText('REC  CAM-07',39,31);\n  ctx.textAlign='right';ctx.fillStyle='rgba(223,248,255,.75)';ctx.font='10px monospace';ctx.fillText(`${L().sector} ${String(state.level).padStart(2,'0')}`,VIEW_W-16,27);\n  ctx.fillText(state.exposed>0?`${L().alarm} ${state.exposed.toFixed(1)}s`:(state.diamondThreat?L().diamondDanger:(state.sentry?(state.sentryDetected?`${L().surveillance} \u2022 ${L().target}`:`${L().surveillance} \u2022 ${L().scan}`):L().clear)),VIEW_W-16,43);\n  ctx.restore();\n}\nfunction drawBanner(){\n  if(state.bannerTime<=0 || !state.banner) return;\n  const a=clamp(state.bannerTime*2,0,1);\n  const msg=localizeBanner(state.banner);\n  const maxW=Math.min(300,VIEW_W-28);\n\n  ctx.save();\n  ctx.globalAlpha=a;\n  ctx.font='bold 11px monospace';\n\n  const measured=Math.min(maxW,Math.max(110,ctx.measureText(msg).width+28));\n  const h=30;\n  const x=(VIEW_W-measured)\/2;\n  const y=VIEW_H-h-12;\n\n  ctx.fillStyle='rgba(0,8,12,.80)';\n  rr(x,y,measured,h,7,true,false);\n  ctx.strokeStyle='rgba(32,216,255,.38)';\n  ctx.lineWidth=1;\n  rr(x,y,measured,h,7,false,true);\n\n  ctx.fillStyle='#eefcff';\n  ctx.textAlign='center';\n  ctx.textBaseline='middle';\n  ctx.fillText(msg,VIEW_W\/2,y+h\/2);\n  ctx.restore();\n}\n\n\n\/\/ ---------------------------------------------------------------------------\n\/\/ v5.1 REALTIME WEBGL CCTV FEED\n\/\/ ---------------------------------------------------------------------------\n\/\/ E\u00e9n bron van waarheid: deze renderer bouwt zijn 3D-wereld rechtstreeks uit\n\/\/ state.map, state.levelRooms en state.diamondRoom. Er bestaat dus geen tweede\n\/\/ handgemaakt level dat uit sync kan raken met het 2D-labyrint.\nconst CCTV3D_FOV=Math.PI*0.39;\nconst CCTV3D_WALL_H=3.2;\nlet cctv3dLastFrame=0;\nlet cctv3dLastDynamicFrame=0;\nlet cctvGL=null,cctvGLProgram=null,cctvStaticMesh=null,cctvGlassMesh=null,cctvDynamicMesh=null;\nlet cctv3dWorldKey='';\nlet cctvFallbackCtx=null;\n\nfunction cctv3dCompile(gl,type,source){\n  const sh=gl.createShader(type);gl.shaderSource(sh,source);gl.compileShader(sh);\n  if(!gl.getShaderParameter(sh,gl.COMPILE_STATUS)){\n    console.warn('CCTV WebGL shader error:',gl.getShaderInfoLog(sh));gl.deleteShader(sh);return null;\n  }\n  return sh;\n}\nfunction cctv3dInitGL(){\n  if(cctvGLProgram||cctvFallbackCtx||!cctv3dCanvas) return !!cctvGLProgram;\n  const gl=cctv3dCanvas.getContext('webgl',{alpha:false,antialias:!MOBILE_RENDER_MODE,preserveDrawingBuffer:false,powerPreference:'high-performance'}) || cctv3dCanvas.getContext('experimental-webgl');\n  if(!gl){\n    cctvFallbackCtx=cctv3dCanvas.getContext('2d',{alpha:false});\n    if(cctv3dMode)cctv3dMode.textContent='2D FALLBACK';\n    return false;\n  }\n  const vs=cctv3dCompile(gl,gl.VERTEX_SHADER,`\n    attribute vec3 aPosition;\n    attribute vec3 aNormal;\n    attribute vec3 aColor;\n    attribute float aMat;\n    uniform mat4 uViewProj;\n    varying vec3 vWorldPos;\n    varying vec3 vNormal;\n    varying vec3 vColor;\n    varying float vMat;\n    void main(){\n      vWorldPos=aPosition;\n      vNormal=aNormal;\n      vColor=aColor;\n      vMat=aMat;\n      gl_Position=uViewProj*vec4(aPosition,1.0);\n    }\n  `);\n  const fs=cctv3dCompile(gl,gl.FRAGMENT_SHADER,`\n    precision mediump float;\n    varying vec3 vWorldPos;\n    varying vec3 vNormal;\n    varying vec3 vColor;\n    varying float vMat;\n    uniform vec3 uCameraPos;\n    uniform vec3 uCameraDir;\n    uniform vec3 uVaultPos;\n    uniform float uVaultGlow;\n    uniform float uAlarm;\n    uniform float uFlashPower;\n    uniform float uOpacity;\n    uniform float uTime;\n    uniform vec2 uResolution;\n    float hash(vec2 p){return fract(sin(dot(p,vec2(127.1,311.7)))*43758.5453);}\n    void main(){\n      vec3 N=normalize(vNormal);\n      vec3 toEye=uCameraPos-vWorldPos;\n      float camDist=max(length(toEye),0.001);\n      vec3 V=toEye\/camDist;\n      vec3 base=vColor;\n      float mat=vMat;\n\n      float noise=hash(floor(vWorldPos.xz*4.0)+floor(vWorldPos.yy*3.0));\n      if(mat<0.5){\n        float checker=mod(floor(vWorldPos.x*2.0)+floor(vWorldPos.z*2.0),2.0);\n        base*=mix(0.88,1.06,checker);\n        float seam=max(step(0.94,fract(vWorldPos.x)),step(0.94,fract(vWorldPos.z)));\n        base*=mix(1.0,0.62,seam*0.34);\n      }else if(mat<1.5){\n        base*=0.86+noise*0.18;\n        float panel=step(0.965,fract(vWorldPos.y*0.92));base*=1.0-panel*0.22;\n      }else if(mat<2.5){\n        base*=0.88+noise*0.08;\n      }else if(mat<3.5){\n        base*=0.92+noise*0.10;\n      }else if(mat>7.5&&mat<8.5){\n        float checker=mod(floor(vWorldPos.x*3.0)+floor(vWorldPos.z*3.0),2.0);\n        base*=mix(0.82,1.16,checker);\n      }\n\n      vec3 L=normalize(vec3(-0.38,0.86,-0.31));\n      float diffuse=0.22+0.52*max(dot(N,L),0.0);\n\n      vec3 fromCam=vWorldPos-uCameraPos;\n      float dCam=max(length(fromCam),0.001);\n      vec3 dirFromCam=fromCam\/dCam;\n      float cone=smoothstep(0.70,0.965,dot(dirFromCam,normalize(uCameraDir)));\n      float headDiffuse=max(dot(N,-dirFromCam),0.0);\n      float head=cone*headDiffuse*uFlashPower\/(1.0+0.075*dCam*dCam);\n\n      vec3 vaultVec=uVaultPos-vWorldPos;\n      float vaultD=max(length(vaultVec),0.001);\n      float vaultLambert=0.28+0.72*max(dot(N,vaultVec\/vaultD),0.0);\n      float vault=uVaultGlow*vaultLambert\/(1.0+0.14*vaultD*vaultD);\n\n      float shiny=(mat>2.5&&mat<8.5)?1.0:0.0;\n      float veryShiny=step(3.5,mat);\n      float specPow=mix(18.0,54.0,veryShiny);\n      float spec=pow(max(dot(N,V),0.0),specPow)*(0.12+0.64*shiny);\n\n      vec3 col=base*diffuse;\n      col+=base*head*vec3(0.82,0.96,1.0);\n      col+=vec3(0.15,0.62,0.82)*vault;\n      col+=vec3(0.72,0.90,1.0)*spec*(0.35+head*1.35+vault*0.55);\n\n      if(mat>3.5&&mat<4.5) col+=base*0.72+vec3(0.18,0.52,0.62); \/\/ diamant\n      if(mat>6.5&&mat<7.5) col+=base*0.80; \/\/ plafondlamp\n      if(mat>5.5&&mat<6.5){col=mix(col,vec3(0.28,0.72,0.82),0.42)+spec*0.55;} \/\/ glas\n      if(uAlarm>0.5){\n        float pulse=0.5+0.5*sin(uTime*7.0);\n        col+=vec3(0.52,0.015,0.02)*pulse\/(1.0+0.055*vaultD*vaultD);\n      }\n\n      float fog=clamp((camDist-7.0)\/23.0,0.0,0.82);\n      col=mix(col,vec3(0.012,0.026,0.032),fog);\n\n      \/\/ CCTV-grade: subtiele scanline, sensorruis, vignette en koele witbalans.\n      vec2 uv=gl_FragCoord.xy\/uResolution;\n      float scan=0.975+0.025*sin(gl_FragCoord.y*3.14159);\n      float sensor=(hash(gl_FragCoord.xy+uTime*31.0)-0.5)*0.022;\n      float vig=1.0-smoothstep(0.36,0.76,length(uv-0.5))*0.34;\n      float luma=dot(col,vec3(0.25,0.64,0.11));\n      col=mix(col,vec3(luma*0.82,luma,luma*0.91),0.08);\n      col=(col+sensor)*scan*vig;\n      col=pow(max(col,0.0),vec3(0.92));\n      gl_FragColor=vec4(col,uOpacity);\n    }\n  `);\n  if(!vs||!fs){cctvFallbackCtx=cctv3dCanvas.getContext('2d',{alpha:false});return false;}\n  const p=gl.createProgram();gl.attachShader(p,vs);gl.attachShader(p,fs);gl.linkProgram(p);\n  gl.deleteShader(vs);gl.deleteShader(fs);\n  if(!gl.getProgramParameter(p,gl.LINK_STATUS)){\n    console.warn('CCTV WebGL link error:',gl.getProgramInfoLog(p));\n    cctvFallbackCtx=cctv3dCanvas.getContext('2d',{alpha:false});return false;\n  }\n  cctvGL=gl;cctvGLProgram=p;\n  p.aPosition=gl.getAttribLocation(p,'aPosition');p.aNormal=gl.getAttribLocation(p,'aNormal');p.aColor=gl.getAttribLocation(p,'aColor');p.aMat=gl.getAttribLocation(p,'aMat');\n  p.uViewProj=gl.getUniformLocation(p,'uViewProj');p.uCameraPos=gl.getUniformLocation(p,'uCameraPos');p.uCameraDir=gl.getUniformLocation(p,'uCameraDir');p.uVaultPos=gl.getUniformLocation(p,'uVaultPos');p.uVaultGlow=gl.getUniformLocation(p,'uVaultGlow');p.uAlarm=gl.getUniformLocation(p,'uAlarm');p.uFlashPower=gl.getUniformLocation(p,'uFlashPower');p.uOpacity=gl.getUniformLocation(p,'uOpacity');p.uTime=gl.getUniformLocation(p,'uTime');p.uResolution=gl.getUniformLocation(p,'uResolution');\n  gl.enable(gl.DEPTH_TEST);gl.depthFunc(gl.LEQUAL);gl.disable(gl.CULL_FACE);\n  gl.clearColor(0.004,0.011,0.015,1);\n  if(cctv3dMode)cctv3dMode.textContent='WEBGL REALTIME';\n  return true;\n}\n\nfunction cctv3dMatPerspective(fovy,aspect,near,far){\n  const f=1\/Math.tan(fovy\/2),nf=1\/(near-far),o=new Float32Array(16);\n  o[0]=f\/aspect;o[5]=f;o[10]=(far+near)*nf;o[11]=-1;o[14]=2*far*near*nf;return o;\n}\nfunction cctv3dMatLookAt(eye,center,up=[0,1,0]){\n  let zx=eye[0]-center[0],zy=eye[1]-center[1],zz=eye[2]-center[2];let zlen=Math.hypot(zx,zy,zz)||1;zx\/=zlen;zy\/=zlen;zz\/=zlen;\n  let xx=up[1]*zz-up[2]*zy,xy=up[2]*zx-up[0]*zz,xz=up[0]*zy-up[1]*zx;let xlen=Math.hypot(xx,xy,xz)||1;xx\/=xlen;xy\/=xlen;xz\/=xlen;\n  const yx=zy*xz-zz*xy,yy=zz*xx-zx*xz,yz=zx*xy-zy*xx;\n  const o=new Float32Array(16);o[0]=xx;o[1]=yx;o[2]=zx;o[3]=0;o[4]=xy;o[5]=yy;o[6]=zy;o[7]=0;o[8]=xz;o[9]=yz;o[10]=zz;o[11]=0;o[12]=-(xx*eye[0]+xy*eye[1]+xz*eye[2]);o[13]=-(yx*eye[0]+yy*eye[1]+yz*eye[2]);o[14]=-(zx*eye[0]+zy*eye[1]+zz*eye[2]);o[15]=1;return o;\n}\nfunction cctv3dMatMul(a,b){\n  const o=new Float32Array(16);for(let c=0;c<4;c++)for(let r=0;r<4;r++)o[c*4+r]=a[0*4+r]*b[c*4+0]+a[1*4+r]*b[c*4+1]+a[2*4+r]*b[c*4+2]+a[3*4+r]*b[c*4+3];return o;\n}\nfunction cctv3dBuilder(){return {data:[]};}\nfunction cctv3dV(b,p,n,col,mat){b.data.push(p[0],p[1],p[2],n[0],n[1],n[2],col[0],col[1],col[2],mat);}\nfunction cctv3dQuad(b,a,c,d,e,n,col,mat){cctv3dV(b,a,n,col,mat);cctv3dV(b,c,n,col,mat);cctv3dV(b,d,n,col,mat);cctv3dV(b,a,n,col,mat);cctv3dV(b,d,n,col,mat);cctv3dV(b,e,n,col,mat);}\nfunction cctv3dTri(b,a,c,d,col,mat){\n  const ux=c[0]-a[0],uy=c[1]-a[1],uz=c[2]-a[2],vx=d[0]-a[0],vy=d[1]-a[1],vz=d[2]-a[2];\n  let nx=uy*vz-uz*vy,ny=uz*vx-ux*vz,nz=ux*vy-uy*vx,l=Math.hypot(nx,ny,nz)||1;nx\/=l;ny\/=l;nz\/=l;\n  const n=[nx,ny,nz];cctv3dV(b,a,n,col,mat);cctv3dV(b,c,n,col,mat);cctv3dV(b,d,n,col,mat);\n}\nfunction cctv3dBox(b,x0,y0,z0,x1,y1,z1,col,mat){\n  cctv3dQuad(b,[x1,y0,z0],[x1,y1,z0],[x1,y1,z1],[x1,y0,z1],[1,0,0],col,mat);\n  cctv3dQuad(b,[x0,y0,z1],[x0,y1,z1],[x0,y1,z0],[x0,y0,z0],[-1,0,0],col,mat);\n  cctv3dQuad(b,[x0,y1,z0],[x0,y1,z1],[x1,y1,z1],[x1,y1,z0],[0,1,0],col,mat);\n  cctv3dQuad(b,[x0,y0,z1],[x0,y0,z0],[x1,y0,z0],[x1,y0,z1],[0,-1,0],col,mat);\n  cctv3dQuad(b,[x0,y0,z1],[x1,y0,z1],[x1,y1,z1],[x0,y1,z1],[0,0,1],col,mat);\n  cctv3dQuad(b,[x1,y0,z0],[x0,y0,z0],[x0,y1,z0],[x1,y1,z0],[0,0,-1],col,mat);\n}\nfunction cctv3dOrientedBox(b,cx,cz,x0,y0,z0,x1,y1,z1,angle,col,mat){\n  const f=[Math.cos(angle),Math.sin(angle)],r=[-f[1],f[0]];\n  const P=(lx,y,lz)=>[cx+r[0]*lx+f[0]*lz,y,cz+r[1]*lx+f[1]*lz];\n  const a=P(x0,y0,z0),bb=P(x1,y0,z0),c=P(x1,y0,z1),d=P(x0,y0,z1);\n  const e=P(x0,y1,z0),f1=P(x1,y1,z0),g=P(x1,y1,z1),h=P(x0,y1,z1);\n  [[a,bb,f1],[a,f1,e],[bb,c,g],[bb,g,f1],[c,d,h],[c,h,g],[d,a,e],[d,e,h],[e,f1,g],[e,g,h],[d,c,bb],[d,bb,a]].forEach(t=>cctv3dTri(b,t[0],t[1],t[2],col,mat));\n}\nfunction cctv3dSegmentBox(b,a,c,width,col,mat){\n  let vx=c[0]-a[0],vy=c[1]-a[1],vz=c[2]-a[2],vl=Math.hypot(vx,vy,vz)||1;vx\/=vl;vy\/=vl;vz\/=vl;\n  let rx,ry,rz;\n  if(Math.abs(vy)<.92){rx=-vz;ry=0;rz=vx;}else{rx=1;ry=0;rz=0;}\n  let rl=Math.hypot(rx,ry,rz)||1;rx\/=rl;ry\/=rl;rz\/=rl;\n  const ux=vy*rz-vz*ry,uy=vz*rx-vx*rz,uz=vx*ry-vy*rx,h=width*.5;\n  const pt=(p,sr,su)=>[p[0]+rx*h*sr+ux*h*su,p[1]+ry*h*sr+uy*h*su,p[2]+rz*h*sr+uz*h*su];\n  const a0=pt(a,-1,-1),a1=pt(a,1,-1),a2=pt(a,1,1),a3=pt(a,-1,1),b0=pt(c,-1,-1),b1=pt(c,1,-1),b2=pt(c,1,1),b3=pt(c,-1,1);\n  [[a0,a1,a2],[a0,a2,a3],[b1,b0,b3],[b1,b3,b2],[a0,b0,b1],[a0,b1,a1],[a1,b1,b2],[a1,b2,a2],[a2,b2,b3],[a2,b3,a3],[a3,b3,b0],[a3,b0,a0]].forEach(t=>cctv3dTri(b,t[0],t[1],t[2],col,mat));\n}\nfunction cctv3dAddRealisticBurglar(b,intr,now){\n  const x=intr.x+.5,z=intr.y+.5;\n  const moveDir=(intr.dir&&intr.dir!==DIRS.none)?intr.dir:((intr.desired&&intr.desired!==DIRS.none)?intr.desired:DIRS.down);\n  const angle=Math.atan2(moveDir.y,moveDir.x);\n  const forward=[Math.cos(angle),Math.sin(angle)],right=[-forward[1],forward[0]];\n  const moving=(intr.dir&&intr.dir!==DIRS.none)&&!intr.flashed;\n  const phase=now*.0085+intr.index*1.73;\n  const stride=moving?Math.sin(phase)*.17:0;\n  const armSwing=moving?-stride*.92:0;\n  const cloth=intr.flashed?[0.20,0.34,0.39]:[0.055,0.070,0.078];\n  const pants=intr.flashed?[0.16,0.29,0.34]:[0.035,0.045,0.052];\n  const hood=intr.flashed?[0.26,0.42,0.47]:[0.025,0.032,0.037];\n  const skin=[0.43,0.31,0.24],shoe=[0.018,0.022,0.025],accent=cctv3dHex(intr.color,[.75,.18,.22]);\n  const world=(side,y,fwd)=>[x+right[0]*side+forward[0]*fwd,y,z+right[1]*side+forward[1]*fwd];\n\n  \/\/ Schoenen, benen en heupen: afzonderlijke segmenten geven een duidelijk menselijke loopcyclus.\n  const lHip=world(-.105,.78,0),rHip=world(.105,.78,0);\n  const lKnee=world(-.11,.43,stride*.36),rKnee=world(.11,.43,-stride*.36);\n  const lAnkle=world(-.115,.12,stride),rAnkle=world(.115,.12,-stride);\n  cctv3dSegmentBox(b,lHip,lKnee,.125,pants,9);cctv3dSegmentBox(b,lKnee,lAnkle,.115,pants,9);\n  cctv3dSegmentBox(b,rHip,rKnee,.125,pants,9);cctv3dSegmentBox(b,rKnee,rAnkle,.115,pants,9);\n  cctv3dOrientedBox(b,lAnkle[0],lAnkle[2],-.075,.035,-.04,.075,.13,.19,angle,shoe,9);\n  cctv3dOrientedBox(b,rAnkle[0],rAnkle[2],-.075,.035,-.04,.075,.13,.19,angle,shoe,9);\n\n  \/\/ Broek, jas en lichte schoudervorm.\n  cctv3dOrientedBox(b,x,z,-.18,.68,-.115,.18,.88,.115,angle,pants,9);\n  cctv3dOrientedBox(b,x,z,-.225,.86,-.135,.225,1.43,.135,angle,cloth,9);\n  cctv3dSphere(b,x,1.36,z,.235,cloth,9,8,4);\n\n  \/\/ Armen met tegengestelde zwaai, donkere handschoenen.\n  const lShoulder=world(-.235,1.35,.015),rShoulder=world(.235,1.35,.015);\n  const lElbow=world(-.275,1.08,armSwing*.45),rElbow=world(.275,1.08,-armSwing*.45);\n  const lHand=world(-.245,.82,armSwing),rHand=world(.245,.82,-armSwing);\n  cctv3dSegmentBox(b,lShoulder,lElbow,.105,cloth,9);cctv3dSegmentBox(b,lElbow,lHand,.095,cloth,9);\n  cctv3dSegmentBox(b,rShoulder,rElbow,.105,cloth,9);cctv3dSegmentBox(b,rElbow,rHand,.095,cloth,9);\n  cctv3dSphere(b,lHand[0],lHand[1],lHand[2],.07,shoe,9,6,3);cctv3dSphere(b,rHand[0],rHand[1],rHand[2],.07,shoe,9,6,3);\n\n  \/\/ Kap + gezichtsmasker. Het gezicht is bewust donker\/onge\u00efdentificeerd zoals op echte CCTV.\n  cctv3dSphere(b,x,1.66,z,.205,hood,9,9,5);\n  cctv3dOrientedBox(b,x,z,-.13,1.585,.165,.13,1.705,.205,angle,[.06,.065,.065],9);\n  cctv3dOrientedBox(b,x,z,-.105,1.705,.184,.105,1.755,.208,angle,skin,9);\n\n  \/\/ Compacte rugzak en een klein kleuraccent als gameplay-identificatie, niet als tekenfilmlichaam.\n  cctv3dOrientedBox(b,x,z,-.17,.94,-.245,.17,1.34,-.13,angle,[.045,.052,.055],9);\n  cctv3dOrientedBox(b,x,z,-.075,1.08,-.257,.075,1.19,-.247,angle,[accent[0]*.55,accent[1]*.55,accent[2]*.55],7);\n}\n\nfunction cctv3dSphere(b,cx,cy,cz,r,col,mat,seg=8,rings=5){\n  for(let y=0;y<rings;y++){\n    const v0=y\/rings,v1=(y+1)\/rings,p0=(v0-.5)*Math.PI,p1=(v1-.5)*Math.PI;\n    for(let x=0;x<seg;x++){\n      const u0=x\/seg*TAU,u1=(x+1)\/seg*TAU;\n      const p=(u,p)=>[cx+Math.cos(p)*Math.cos(u)*r,cy+Math.sin(p)*r,cz+Math.cos(p)*Math.sin(u)*r];\n      const a=p(u0,p0),c=p(u1,p0),d=p(u1,p1),e=p(u0,p1);\n      cctv3dTri(b,a,c,d,col,mat);cctv3dTri(b,a,d,e,col,mat);\n    }\n  }\n}\nfunction cctv3dOcta(b,cx,cy,cz,r,col,mat,rot=0){\n  const rr=(x,z)=>[cx+x*Math.cos(rot)-z*Math.sin(rot),cy,cz+x*Math.sin(rot)+z*Math.cos(rot)];\n  const top=[cx,cy+r*1.18,cz],bot=[cx,cy-r*1.18,cz],p0=rr(r,0),p1=rr(0,r),p2=rr(-r,0),p3=rr(0,-r);\n  [ [top,p0,p1],[top,p1,p2],[top,p2,p3],[top,p3,p0],[bot,p1,p0],[bot,p2,p1],[bot,p3,p2],[bot,p0,p3] ].forEach(t=>cctv3dTri(b,t[0],t[1],t[2],col,mat));\n}\nfunction cctv3dUpload(old,b,usage){\n  const gl=cctvGL;if(!gl)return null;const mesh=old||{buffer:gl.createBuffer(),count:0};gl.bindBuffer(gl.ARRAY_BUFFER,mesh.buffer);gl.bufferData(gl.ARRAY_BUFFER,new Float32Array(b.data),usage||gl.STATIC_DRAW);mesh.count=b.data.length\/10;return mesh;\n}\nfunction cctv3dBindMesh(mesh){\n  const gl=cctvGL,p=cctvGLProgram;if(!mesh||!mesh.count)return false;gl.bindBuffer(gl.ARRAY_BUFFER,mesh.buffer);const stride=40;\n  gl.enableVertexAttribArray(p.aPosition);gl.vertexAttribPointer(p.aPosition,3,gl.FLOAT,false,stride,0);\n  gl.enableVertexAttribArray(p.aNormal);gl.vertexAttribPointer(p.aNormal,3,gl.FLOAT,false,stride,12);\n  gl.enableVertexAttribArray(p.aColor);gl.vertexAttribPointer(p.aColor,3,gl.FLOAT,false,stride,24);\n  gl.enableVertexAttribArray(p.aMat);gl.vertexAttribPointer(p.aMat,1,gl.FLOAT,false,stride,36);return true;\n}\nfunction cctv3dPassable(x,z){return z>=0&&z<MAZE_H&&x>=0&&x<MAZE_W&&state.map?.[z]?.[x]!=='#';}\nfunction cctv3dRoomHas(x,z){const r=currentDiamondRoom()||state.diamondRoom;return !!r&&roomContainsPoint(r,x,z,.01);}\n\nfunction cctv3dDoorFrames(b){\n  const metal=[0.19,0.31,0.35],h=2.55,beam=.16,post=.09;\n  const addH=(z,start,end)=>{cctv3dBox(b,start-post,0,z-.045,start+post,h,z+.045,metal,3);cctv3dBox(b,end-post,0,z-.045,end+post,h,z+.045,metal,3);cctv3dBox(b,start-post,h-beam,z-.05,end+post,h,z+.05,metal,3);};\n  const addV=(x,start,end)=>{cctv3dBox(b,x-.045,0,start-post,x+.045,h,start+post,metal,3);cctv3dBox(b,x-.045,0,end-post,x+.045,h,end+post,metal,3);cctv3dBox(b,x-.05,h-beam,start-post,x+.05,h,end+post,metal,3);};\n  for(const r of state.levelRooms||[]){\n    const sides=[\n      {kind:'h',plane:r.y,vals:Array.from({length:r.w},(_,i)=>r.x+i).filter(x=>cctv3dPassable(x,r.y)&&cctv3dPassable(x,r.y-1))},\n      {kind:'h',plane:r.y+r.h,vals:Array.from({length:r.w},(_,i)=>r.x+i).filter(x=>cctv3dPassable(x,r.y+r.h-1)&&cctv3dPassable(x,r.y+r.h))},\n      {kind:'v',plane:r.x,vals:Array.from({length:r.h},(_,i)=>r.y+i).filter(z=>cctv3dPassable(r.x,z)&&cctv3dPassable(r.x-1,z))},\n      {kind:'v',plane:r.x+r.w,vals:Array.from({length:r.h},(_,i)=>r.y+i).filter(z=>cctv3dPassable(r.x+r.w-1,z)&&cctv3dPassable(r.x+r.w,z))}\n    ];\n    for(const s of sides){\n      let run=[];const flush=()=>{if(!run.length)return;const a=run[0],e=run[run.length-1]+1;if(s.kind==='h')addH(s.plane,a,e);else addV(s.plane,a,e);run=[];};\n      for(const v of s.vals){if(run.length&&v!==run[run.length-1]+1)flush();run.push(v);}flush();\n    }\n  }\n}\nfunction cctv3dBuildStaticWorld(){\n  if(!cctvGL||!state.map?.length)return;\n  const b=cctv3dBuilder(),glass=cctv3dBuilder();\n  const wall=[0.12,0.24,0.29],wallVault=[0.10,0.31,0.36],floor=[0.105,0.14,0.155],floorVault=[0.055,0.23,0.28],ceil=[0.075,0.095,0.105];\n  for(let z=0;z<MAZE_H;z++)for(let x=0;x<MAZE_W;x++){\n    if(cctv3dPassable(x,z)){\n      const vault=cctv3dRoomHas(x,z),fc=vault?floorVault:floor,mat=vault?8:0;\n      cctv3dQuad(b,[x,0,z],[x,0,z+1],[x+1,0,z+1],[x+1,0,z],[0,1,0],fc,mat);\n      cctv3dQuad(b,[x,CCTV3D_WALL_H,z],[x+1,CCTV3D_WALL_H,z],[x+1,CCTV3D_WALL_H,z+1],[x,CCTV3D_WALL_H,z+1],[0,-1,0],ceil,2);\n      if(((x*13+z*17+state.level*7)%37===0)||vault&&((x+z)%5===0)){\n        const lc=vault?[0.34,0.92,1.0]:[0.78,0.91,0.92];\n        cctv3dBox(b,x+.20,CCTV3D_WALL_H-.09,z+.37,x+.80,CCTV3D_WALL_H-.035,z+.63,lc,7);\n      }\n      continue;\n    }\n    const nearVault=cctv3dRoomHas(x-1,z)||cctv3dRoomHas(x+1,z)||cctv3dRoomHas(x,z-1)||cctv3dRoomHas(x,z+1),wc=nearVault?wallVault:wall;\n    if(cctv3dPassable(x-1,z))cctv3dQuad(b,[x,0,z+1],[x,CCTV3D_WALL_H,z+1],[x,CCTV3D_WALL_H,z],[x,0,z],[-1,0,0],wc,1);\n    if(cctv3dPassable(x+1,z))cctv3dQuad(b,[x+1,0,z],[x+1,CCTV3D_WALL_H,z],[x+1,CCTV3D_WALL_H,z+1],[x+1,0,z+1],[1,0,0],wc,1);\n    if(cctv3dPassable(x,z-1))cctv3dQuad(b,[x+1,0,z],[x,0,z],[x,CCTV3D_WALL_H,z],[x+1,CCTV3D_WALL_H,z],[0,0,-1],wc,1);\n    if(cctv3dPassable(x,z+1))cctv3dQuad(b,[x,0,z+1],[x+1,0,z+1],[x+1,CCTV3D_WALL_H,z+1],[x,CCTV3D_WALL_H,z+1],[0,0,1],wc,1);\n  }\n  cctv3dDoorFrames(b);\n\n  if(state.diamond){\n    const dx=state.diamond.x+.5,dz=state.diamond.y+.5;\n    cctv3dBox(b,dx-.60,.02,dz-.60,dx+.60,.24,dz+.60,[0.13,0.18,0.20],3);\n    cctv3dBox(b,dx-.42,.24,dz-.42,dx+.42,.48,dz+.42,[0.18,0.25,0.27],3);\n    const post=[0.22,0.40,0.44];\n    [[-.48,-.48],[.48,-.48],[-.48,.48],[.48,.48]].forEach(([ox,oz])=>cctv3dBox(b,dx+ox-.025,.46,dz+oz-.025,dx+ox+.025,1.70,dz+oz+.025,post,3));\n    const gc=[0.24,0.74,0.88],y0=.47,y1=1.68,s=.50;\n    cctv3dQuad(glass,[dx-s,y0,dz-s],[dx-s,y1,dz-s],[dx+s,y1,dz-s],[dx+s,y0,dz-s],[0,0,-1],gc,6);\n    cctv3dQuad(glass,[dx+s,y0,dz+s],[dx+s,y1,dz+s],[dx-s,y1,dz+s],[dx-s,y0,dz+s],[0,0,1],gc,6);\n    cctv3dQuad(glass,[dx-s,y0,dz+s],[dx-s,y1,dz+s],[dx-s,y1,dz-s],[dx-s,y0,dz-s],[-1,0,0],gc,6);\n    cctv3dQuad(glass,[dx+s,y0,dz-s],[dx+s,y1,dz-s],[dx+s,y1,dz+s],[dx+s,y0,dz+s],[1,0,0],gc,6);\n    cctv3dQuad(glass,[dx-s,y1,dz-s],[dx-s,y1,dz+s],[dx+s,y1,dz+s],[dx+s,y1,dz-s],[0,1,0],gc,6);\n  }\n  cctvStaticMesh=cctv3dUpload(cctvStaticMesh,b,cctvGL.STATIC_DRAW);\n  cctvGlassMesh=cctv3dUpload(cctvGlassMesh,glass,cctvGL.STATIC_DRAW);\n}\nfunction cctv3dBuildDynamic(now,viewCam=state.player){\n  const b=cctv3dBuilder(),cam=viewCam;\n  if(state.diamond&&!state.diamond.stolen)cctv3dOcta(b,state.diamond.x+.5,1.02,state.diamond.y+.5,.27,[0.55,0.97,1.0],4,now*.00075);\n  for(const intr of state.burglars||[]){\n    if(intr.respawn>0)continue;\n    cctv3dAddRealisticBurglar(b,intr,now);\n  }\n  for(const other of state.cameras||[]){\n    if(other===cam||other.alive===false)continue;const x=other.x+.5,z=other.y+.5,col=cctv3dHex(other.color,[.55,1,.45]);\n    cctv3dBox(b,x-.24,.72,z-.17,x+.24,1.05,z+.17,col,3);cctv3dSphere(b,x,1.00,z-.19,.11,[.82,.98,1],7,7,4);\n    cctv3dBox(b,x-.035,.15,z-.035,x+.035,.72,z+.035,[.16,.24,.26],3);\n  }\n  if(state.diamondThreat&&state.diamond){\n    const x=state.diamond.x+.5,z=state.diamond.y+.5;cctv3dSphere(b,x,2.92,z,.09,[1.0,.04,.07],7,7,4);\n  }\n  cctvDynamicMesh=cctv3dUpload(cctvDynamicMesh,b,cctvGL.DYNAMIC_DRAW);\n}\nfunction cctv3dHex(hex,fallback){\n  if(typeof hex!=='string'||!hex.startsWith('#'))return fallback;const h=hex.slice(1);if(h.length!==6)return fallback;return [parseInt(h.slice(0,2),16)\/255,parseInt(h.slice(2,4),16)\/255,parseInt(h.slice(4,6),16)\/255];\n}\nfunction cctv3dFallback(now){\n  if(!cctvFallbackCtx)return;const c=cctvFallbackCtx,W=cctv3dCanvas.width,H=cctv3dCanvas.height;c.fillStyle='#020609';c.fillRect(0,0,W,H);c.strokeStyle='rgba(32,216,255,.24)';for(let y=0;y<H;y+=8){c.beginPath();c.moveTo(0,y);c.lineTo(W,y);c.stroke();}c.fillStyle='#7de8ff';c.font='bold 16px monospace';c.textAlign='center';c.fillText('WEBGL NOT AVAILABLE',W\/2,H\/2-4);c.fillStyle='#8dff75';c.font='11px monospace';c.fillText('2D GAME STILL ACTIVE',W\/2,H\/2+18);\n}\nfunction cctv3dWorldSignature(){const r=currentDiamondRoom()||state.diamondRoom,d=state.diamond;return `${state.level}|${r?.x},${r?.y},${r?.w},${r?.h}|${d?.x},${d?.y}|${state.map?.length||0}`;}\nfunction cctv3dRoomGlow(cam=state.player){const r=currentDiamondRoom()||state.diamondRoom;return !!(r&&cam&&roomContainsPoint(r,cam.x,cam.y,.08));}\nfunction renderCctv3D(now=performance.now(),force=false){\n  if(!cctv3dCanvas)return;\n  \/\/ Mobiel: de cameraview zelf op ~30 FPS voor vloeiende beweging; zware dynamische\n  \/\/ geometrie hoeft maar ~15 FPS opnieuw opgebouwd te worden. Desktop behoudt ~24 FPS.\n  const cctvFrameInterval=MOBILE_RENDER_MODE?33:41;\n  if(!force&&now-cctv3dLastFrame<cctvFrameInterval)return;\n  cctv3dLastFrame=now;\n  const cam=cctvDisplayCamera(now);\n  const eventCut=!!(state.cctvEventCamId && cam && cam.id===state.cctvEventCamId);\n  if(cctv3dClock){const d=new Date();cctv3dClock.textContent=`${String(d.getHours()).padStart(2,'0')}:${String(d.getMinutes()).padStart(2,'0')}:${String(d.getSeconds()).padStart(2,'0')}`;}\n  if(cctv3dLabel&&cam)cctv3dLabel.textContent=`CAM ${String((cam.cameraIndex||0)+1).padStart(2,'0')} \u2022 ${eventCut?'AUTO INTERCEPT':Math.round((((cam.visualAngle||0)*180\/Math.PI)+360)%360)+'\u00b0'}`;\n  if(cctv3dSceneLabel)cctv3dSceneLabel.textContent=eventCut?`AUTO INTERCEPT \u2022 RETURN CAM ${String(((state.player?.cameraIndex||0)+1)).padStart(2,'0')}`:(state.diamondThreat?'VAULT ALARM':(cctv3dRoomGlow(cam)?'DIAMOND VAULT':'MAZE FEED'));\n  if(!cctv3dInitGL()){cctv3dFallback(now);return;}\n  const gl=cctvGL,p=cctvGLProgram;\n  const targetW=MOBILE_RENDER_MODE?320:(window.innerWidth<620?360:540),targetH=Math.round(targetW*9\/16);\n  if(cctv3dCanvas.width!==targetW||cctv3dCanvas.height!==targetH){cctv3dCanvas.width=targetW;cctv3dCanvas.height=targetH;}\n  gl.viewport(0,0,cctv3dCanvas.width,cctv3dCanvas.height);gl.clear(gl.COLOR_BUFFER_BIT|gl.DEPTH_BUFFER_BIT);\n  if(!cam||cam.alive===false)return;\n\n  const key=cctv3dWorldSignature();if(key!==cctv3dWorldKey){cctv3dWorldKey=key;cctv3dBuildStaticWorld();cctv3dLastDynamicFrame=0;}\n  if(force || !cctvDynamicMesh || !MOBILE_RENDER_MODE || now-cctv3dLastDynamicFrame>=66){\n    cctv3dBuildDynamic(now,cam);\n    cctv3dLastDynamicFrame=now;\n  }\n\n  const angle=Number.isFinite(cam.visualAngle)?cam.visualAngle:facingAngle(cam.facing||DIRS.right);\n  const eye=[cam.x+.5,1.60,cam.y+.5],dir=[Math.cos(angle),-.055,Math.sin(angle)],center=[eye[0]+dir[0],eye[1]+dir[1],eye[2]+dir[2]];\n  const view=cctv3dMatLookAt(eye,center),proj=cctv3dMatPerspective(CCTV3D_FOV,cctv3dCanvas.width\/cctv3dCanvas.height,.035,42),vp=cctv3dMatMul(proj,view);\n  const room=currentDiamondRoom()||state.diamondRoom;\n  const vault=[state.diamond?state.diamond.x+.5:(room?room.x+room.w\/2:15),1.20,state.diamond?state.diamond.y+.5:(room?room.y+room.h\/2:19)];\n  const flash=cam===state.player?(state.movementFlashActive||state.flashHeld||state.sentry?2.45:1.22):(cam.autoSurveillance||cam.autoHuntTarget!==null||cam.backgroundTransit?2.45:1.22);\n  gl.useProgram(p);gl.uniformMatrix4fv(p.uViewProj,false,vp);gl.uniform3f(p.uCameraPos,eye[0],eye[1],eye[2]);gl.uniform3f(p.uCameraDir,dir[0],dir[1],dir[2]);gl.uniform3f(p.uVaultPos,vault[0],vault[1],vault[2]);gl.uniform1f(p.uVaultGlow,state.diamond&&!state.diamond.stolen?1.35:.45);gl.uniform1f(p.uAlarm,state.diamondThreat?1:0);gl.uniform1f(p.uFlashPower,flash);gl.uniform1f(p.uTime,now\/1000);gl.uniform2f(p.uResolution,cctv3dCanvas.width,cctv3dCanvas.height);\n\n  gl.disable(gl.BLEND);gl.depthMask(true);gl.uniform1f(p.uOpacity,1);\n  if(cctv3dBindMesh(cctvStaticMesh))gl.drawArrays(gl.TRIANGLES,0,cctvStaticMesh.count);\n  if(cctv3dBindMesh(cctvDynamicMesh))gl.drawArrays(gl.TRIANGLES,0,cctvDynamicMesh.count);\n\n  \/\/ Vitrine als echte transparante WebGL-geometrie, na de ondoorzichtige sc\u00e8ne.\n  if(cctvGlassMesh?.count){gl.enable(gl.BLEND);gl.blendFunc(gl.SRC_ALPHA,gl.ONE_MINUS_SRC_ALPHA);gl.depthMask(false);gl.uniform1f(p.uOpacity,.18);if(cctv3dBindMesh(cctvGlassMesh))gl.drawArrays(gl.TRIANGLES,0,cctvGlassMesh.count);gl.depthMask(true);gl.disable(gl.BLEND);}\n}\n\nfunction draw(){\n  ctx.save();\n  ctx.setTransform(DPR,0,0,DPR,0,0);\n  ctx.clearRect(0,0,VIEW_W,VIEW_H);\n  const bg=ctx.createRadialGradient(VIEW_W*.5,VIEW_H*.48,20,VIEW_W*.5,VIEW_H*.48,VIEW_H*.8);bg.addColorStop(0,'#06141d');bg.addColorStop(1,'#010407');ctx.fillStyle=bg;ctx.fillRect(0,0,VIEW_W,VIEW_H);\n  if(state.shake>0){const m=state.shake*8;ctx.translate((Math.random()-.5)*m,(Math.random()-.5)*m);}\n  ctx.save();ctx.translate(-state.viewX,-state.viewY);drawMaze();drawDots();drawDiamond();drawNavigationTarget();drawBackgroundSurveillanceCones();drawCone();for(const b of state.burglars)if(b.respawn<=0)drawBurglar(b);for(const cam of state.cameras||[])if(cam.alive!==false)drawCamera(cam);ctx.restore();\n  drawFlashScreenBloom();drawDiamondDirectionArrow();drawScreenHUD();drawBanner();ctx.restore();\n}\n\nfunction renderScores(){ \/* Local high scores intentionally disabled. *\/ }\n\nfunction togglePause(){\n  if(state.phase!=='playing') return; state.paused=!state.paused; state.banner=state.paused?'PAUSED':'RESUMED';state.bannerTime=.8;statusLine.textContent=state.paused?(state.lang==='fr'?'Pause \u2014 appuyez sur P pour continuer.':state.lang==='en'?'Paused \u2014 press P to continue.':'Pauze \u2014 druk P om verder te gaan.'):(state.lang==='fr'?`Op\u00e9rateur ${state.name} \u2014 flux en direct.`:state.lang==='en'?`Operator ${state.name} \u2014 live feed.`:`Operator ${state.name} actief \u2014 feed live.`);\n}\nfunction updateVolumeUI(){\n  const d=L(); const pct=Math.round(state.volume*100); if(volumeLevel)volumeLevel.textContent=`${pct}%`;\n  if(volumeBtn){volumeBtn.textContent=!state.sound||state.volume<=0?'\ud83d\udd07':state.volume<.8?'\ud83d\udd09':'\ud83d\udd0a';volumeBtn.setAttribute('aria-label',state.sound?d.mute:d.unmute);volumeBtn.title=state.sound?d.mute:d.unmute;}\n  if(soundBtn)soundBtn.textContent=state.sound?d.soundOn:d.soundOff;\n}\nfunction setVolume(v){state.volume=clamp(Math.round(v*5)\/5,0,5);if(state.volume>0)state.sound=true;updateVolumeUI();if(state.sound)ensureAudio();}\nfunction toggleSound(){state.sound=!state.sound;if(state.sound&&state.volume<=0)state.volume=5;updateVolumeUI();if(state.sound)ensureAudio();}\n\n\n\nwindow.addEventListener('keydown',event=>{\n  const editable=event.target instanceof HTMLInputElement || event.target instanceof HTMLTextAreaElement || event.target.isContentEditable;\n  if(editable) return;\n  if(event.code==='KeyP' && ['ready','playing'].includes(state.phase)){\n    event.preventDefault();\n    if(!event.repeat) togglePause();\n  }else if(event.code==='KeyM'){\n    event.preventDefault();\n    if(!event.repeat) toggleSound();\n  }\n},{passive:false});\n\n\nfunction shouldAutoFullscreenOnStart(){\n  const coarsePointer=window.matchMedia?.('(pointer: coarse)').matches===true;\n  const touchCapable=(navigator.maxTouchPoints||0)>0;\n  const mobileTabletSize=Math.min(window.innerWidth,window.innerHeight)<=1024 || window.innerWidth<=1180;\n  return (coarsePointer||touchCapable) && mobileTabletSize;\n}\n\nstartBtn.addEventListener('click',()=>{\n  \/\/ Op smartphone\/tablet meteen vanuit deze gebruikers-tik fullscreen aanvragen.\n  \/\/ De aanvraag gebeurt v\u00f3\u00f3r startGame(), zodat mobiele browsers de gesture behouden.\n  if(shouldAutoFullscreenOnStart() && !isGameFullscreen()){\n    enterGameFullscreen();\n  }\n\n  introTips.style.display='grid';\n  const d=L();\n  overlayEyebrow.textContent=d.online;\n  overlayTitle.innerHTML='<span class=\"white\">CCTV<\/span> <b>NIGHT WATCH<\/b>';\n  overlayText.innerHTML=d.intro;\n  startBtn.textContent=d.start;\n  startGame();\n});\nif(soundBtn) soundBtn.addEventListener('click',toggleSound);\nif(volumeBtn) volumeBtn.addEventListener('click',e=>{e.stopPropagation();toggleSound();root.focus();});\n\nfunction isGameFullscreen(){\n  return document.fullscreenElement===playArea ||\n         document.webkitFullscreenElement===playArea ||\n         playArea?.classList.contains('fullscreenFallback');\n}\n\nfunction syncFullscreenUi(){\n  const active=isGameFullscreen();\n  if(exitFullscreenBtn) exitFullscreenBtn.style.display=active?'block':'';\n}\n\nasync function enterGameFullscreen(){\n  if(!playArea || isGameFullscreen()) return;\n  try{\n    const request=playArea.requestFullscreen || playArea.webkitRequestFullscreen;\n    if(request){\n      const result=request.call(playArea);\n      if(result && typeof result.then==='function') await result;\n      syncFullscreenUi();\n      return;\n    }\n  }catch(_err){}\n  playArea.classList.add('fullscreenFallback');\n  document.body.classList.add('fullscreenFallbackActive');\n  syncFullscreenUi();\n}\n\nasync function exitGameFullscreen(){\n  if(!playArea) return;\n  if(playArea.classList.contains('fullscreenFallback')){\n    playArea.classList.remove('fullscreenFallback');\n    document.body.classList.remove('fullscreenFallbackActive');\n    syncFullscreenUi();\n    return;\n  }\n  try{\n    const exit=document.exitFullscreen || document.webkitExitFullscreen;\n    if(exit){\n      const result=exit.call(document);\n      if(result && typeof result.then==='function') await result;\n    }\n  }catch(_err){}\n  syncFullscreenUi();\n}\n\nif(fullscreenBtn) fullscreenBtn.addEventListener('click',e=>{\n  e.preventDefault();e.stopPropagation();enterGameFullscreen();\n});\nif(exitFullscreenBtn) exitFullscreenBtn.addEventListener('click',e=>{\n  e.preventDefault();e.stopPropagation();exitGameFullscreen();\n});\ndocument.addEventListener('fullscreenchange',syncFullscreenUi);\ndocument.addEventListener('webkitfullscreenchange',syncFullscreenUi);\nwindow.addEventListener('keydown',e=>{\n  if(e.code==='Escape' && playArea?.classList.contains('fullscreenFallback')) exitGameFullscreen();\n});\n\nif(volumeDownBtn) volumeDownBtn.addEventListener('click',e=>{e.stopPropagation();setVolume(state.volume-.2);root.focus();});\nif(volumeUpBtn) volumeUpBtn.addEventListener('click',e=>{e.stopPropagation();setVolume(state.volume+.2);root.focus();});\ndocument.querySelectorAll('.langBtn').forEach(btn=>btn.addEventListener('click',e=>{e.stopPropagation();setLanguage(btn.dataset.lang);root.focus();}));\n\nconst flashTouch=document.getElementById('flashTouch');\n\n\nif(stageCameraBadge){\n  stageCameraBadge.addEventListener('pointerdown',e=>{\n    e.preventDefault();\n    e.stopPropagation();\n    if(!['ready','playing'].includes(state.phase) || state.paused) return;\n    cycleToNextActiveCamera();\n    root.focus();\n  },{passive:false});\n\n  stageCameraBadge.addEventListener('keydown',e=>{\n    if(e.code!=='Enter' && e.code!=='Space') return;\n    e.preventDefault();\n    e.stopPropagation();\n    if(!['ready','playing'].includes(state.phase) || state.paused) return;\n    cycleToNextActiveCamera();\n  });\n}\n\ndocument.querySelectorAll('[data-pan]').forEach(btn=>{\n  btn.addEventListener('pointerdown',e=>{\n    e.preventDefault();e.stopPropagation();panView(btn.dataset.pan);root.focus();\n  },{passive:false});\n});\n\nconst stage=document.getElementById('stage');\ncanvas.addEventListener('pointerdown',e=>{\n  if(!['ready','playing'].includes(state.phase) || state.paused) return;\n  if(e.pointerType==='touch' && e.isPrimary===false) return;\n  \/\/ Muis: rechts = onmiddellijk volgende actieve camera; links = selecteren\/bestemming.\n  if(e.pointerType==='mouse' && e.button===2){\n    e.preventDefault();e.stopPropagation();cycleToNextActiveCamera();root.focus();return;\n  }\n  if(state.flashHeld) return;\n  if(e.pointerType==='mouse' && e.button!==0) return;\n  e.preventDefault();\n  const rect=canvas.getBoundingClientRect();\n  const sx=(e.clientX-rect.left)*(VIEW_W\/rect.width);\n  const sy=(e.clientY-rect.top)*(VIEW_H\/rect.height);\n  const wx=(sx+state.viewX)\/TILE-.5;\n  const wy=(sy+state.viewY)\/TILE-.5;\n  const clickedCamera=cameraAtWorldPoint(wx,wy,e.pointerType==='touch'?1.05:.72);\n  if(clickedCamera){\n    if(clickedCamera!==state.player) selectCamera(clickedCamera);\n    else{\n      state.banner=state.lang==='fr'?`CAM\u00c9RA ${clickedCamera.cameraIndex+1}`:state.lang==='en'?`CAMERA ${clickedCamera.cameraIndex+1}`:`CAMERA ${clickedCamera.cameraIndex+1}`;\n      state.bannerTime=.35;\n    }\n    root.focus();\n    return;\n  }\n  const col=Math.floor((sx+state.viewX)\/TILE);\n  const row=Math.floor((sy+state.viewY)\/TILE);\n  setNavigationTarget(col,row);\n  root.focus();\n},{passive:false});\n\ncanvas.addEventListener('contextmenu',e=>{\n  \/\/ Rechtermuisklik is spelbesturing; blokkeer het browsermenu boven het labyrint\/monitor.\n  e.preventDefault();\n});\n\nlet mobileSimAccumulator=0;\nfunction loop(now){\n  const rawDt=Math.min(.10,Math.max(0,(now-state.lastTime)\/1000||0));\n  state.lastTime=now;\n  if(MOBILE_RENDER_MODE){\n    \/\/ Een vaste simulatiestap voorkomt dat wisselende mobiele frame-times als kleine\n    \/\/ snelheidsstoten zichtbaar worden in de camera. Eventuele achterstand wordt begrensd.\n    mobileSimAccumulator=Math.min(.10,mobileSimAccumulator+rawDt);\n    const fixedStep=1\/60;\n    let steps=0;\n    while(mobileSimAccumulator>=fixedStep && steps<6){\n      update(fixedStep);\n      mobileSimAccumulator-=fixedStep;\n      steps++;\n    }\n    \/\/ Bij 120 Hz hoeft de simulatie niet elk displayframe te lopen; tekenen blijft wel vloeiend.\n  }else{\n    update(Math.min(.034,rawDt));\n  }\n  draw();renderCctv3D(now);requestAnimationFrame(loop);\n}\n\n\/\/ Stilstaand voorbeeld achter het startscherm.\nstate.map=generateMaze(1);\nconst previewDiamond=nearestPassable(26,4);state.diamondRoom=(state.levelRooms||[]).find(r=>roomContainsPoint(r,previewDiamond.x,previewDiamond.y,.01))||null;state.diamond={x:previewDiamond.x,y:previewDiamond.y,found:false,stolen:false,pulse:0,roomIndex:state.diamondRoom?.index??null};state.map[previewDiamond.y][previewDiamond.x]=' ';\nconst previewSpawn=safeCameraStartCell(2,MAZE_H-4,new Set());state.map[previewSpawn.y][previewSpawn.x]=' ';\nconst previewDir=chooseInitialDir(previewSpawn);state.player=makeCamera(previewSpawn.x,previewSpawn.y,0,previewDir);state.cameras=[state.player];state.burglarSpawnSerial=0;state.burglars=[0,1,2,3].map(makeBurglar);state.initialDots=1;state.dots=1;centerView(true);setLanguage('nl');renderScores();updateHUD();draw();requestAnimationFrame(loop);\n})();\n<\/script>\n<\/body>\n<\/html>\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t<div class=\"elementor-element elementor-element-b262009 e-flex e-con-boxed e-con e-parent\" data-id=\"b262009\" data-element_type=\"container\" data-e-type=\"container\">\n\t\t\t\t\t<div class=\"e-con-inner\">\n\t\t\t\t<div class=\"elementor-element elementor-element-4a4e74b elementor-widget elementor-widget-spacer\" data-id=\"4a4e74b\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"spacer.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t\t\t<div class=\"elementor-spacer\">\n\t\t\t<div class=\"elementor-spacer-inner\"><\/div>\n\t\t<\/div>\n\t\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t","protected":false},"excerpt":{"rendered":"<p>Home https:\/\/cctvacademy.be\/wp-content\/uploads\/2026\/09\/cctv-pacmaze-intro-movie-2-cctv-academy.mp4 CCTV PAC-MAZE v5.2 \u2014 Night Watch + Directed WebGL CCTV CCTV PAC-MAZEv5.2 NIGHT WATCH \/\/ LIVE FEEDSECURITY MAZE CHASE NL FR ENG Level01 Cams\ud83d\udcf9 \u00d71 Doel0 \/ 5 LEVEL 1: 1 CAMERA \u2022 EET 5 INBREKERS0 \/ 5 \u2212 \ud83d\udd0a 500% + LEVEL1 SWITCH CAMERA1\/1 GEGETEN0\/5 \u26f6 FULLSCREEN \u2715 EXIT INBREKER OPGEGETEN 1 [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"elementor_canvas","meta":{"_acf_changed":false,"zakra_page_container_layout":"customizer","zakra_page_sidebar_layout":"customizer","zakra_remove_content_margin":false,"zakra_sidebar":"customizer","zakra_transparent_header":"customizer","zakra_logo":0,"zakra_main_header_style":"default","zakra_menu_item_color":"","zakra_menu_item_hover_color":"","zakra_menu_item_active_color":"","zakra_menu_active_style":"","zakra_page_header":true,"h5ap_radio_sources":[],"_glsr_average":0,"_glsr_ranking":0,"_glsr_reviews":0,"footnotes":""},"class_list":["post-51471","page","type-page","status-publish","hentry"],"acf":[],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v27.8 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>CCTV Night Watch3 - RAS\/AIS CCTV Academy<\/title>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/cctvacademy.be\/nl\/cctv-night-watch3\/\" \/>\n<meta property=\"og:locale\" content=\"nl_BE\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"CCTV Night Watch3 - RAS\/AIS CCTV Academy\" \/>\n<meta property=\"og:description\" content=\"Home https:\/\/cctvacademy.be\/wp-content\/uploads\/2026\/09\/cctv-pacmaze-intro-movie-2-cctv-academy.mp4 CCTV PAC-MAZE v5.2 \u2014 Night Watch + Directed WebGL CCTV CCTV PAC-MAZEv5.2 NIGHT WATCH \/\/ LIVE FEEDSECURITY MAZE CHASE NL FR ENG Level01 Cams\ud83d\udcf9 \u00d71 Doel0 \/ 5 LEVEL 1: 1 CAMERA \u2022 EET 5 INBREKERS0 \/ 5 \u2212 \ud83d\udd0a 500% + LEVEL1 SWITCH CAMERA1\/1 GEGETEN0\/5 \u26f6 FULLSCREEN \u2715 EXIT INBREKER OPGEGETEN 1 [&hellip;]\" \/>\n<meta property=\"og:url\" content=\"https:\/\/cctvacademy.be\/nl\/cctv-night-watch3\/\" \/>\n<meta property=\"og:site_name\" content=\"RAS\/AIS CCTV Academy\" \/>\n<meta property=\"article:publisher\" content=\"https:\/\/www.facebook.com\/profile.php?id=61576769152561\" \/>\n<meta property=\"article:modified_time\" content=\"2026-09-20T17:46:32+00:00\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/cctvacademy.be\\\/cctv-night-watch3\\\/\",\"url\":\"https:\\\/\\\/cctvacademy.be\\\/cctv-night-watch3\\\/\",\"name\":\"CCTV Night Watch3 - RAS\\\/AIS CCTV Academy\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/cctvacademy.be\\\/#website\"},\"datePublished\":\"2026-09-20T17:16:01+00:00\",\"dateModified\":\"2026-09-20T17:46:32+00:00\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/cctvacademy.be\\\/cctv-night-watch3\\\/#breadcrumb\"},\"inLanguage\":\"nl-BE\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/cctvacademy.be\\\/cctv-night-watch3\\\/\"]}]},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/cctvacademy.be\\\/cctv-night-watch3\\\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\\\/\\\/cctvacademy.be\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"CCTV Night Watch3\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/cctvacademy.be\\\/#website\",\"url\":\"https:\\\/\\\/cctvacademy.be\\\/\",\"name\":\"CCTV Academy\",\"description\":\"CCTV Opleidingen en CCTV Micro Learning\",\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/cctvacademy.be\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"nl-BE\"}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"CCTV Night Watch3 - RAS\/AIS CCTV Academy","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/cctvacademy.be\/nl\/cctv-night-watch3\/","og_locale":"nl_BE","og_type":"article","og_title":"CCTV Night Watch3 - RAS\/AIS CCTV Academy","og_description":"Home https:\/\/cctvacademy.be\/wp-content\/uploads\/2026\/09\/cctv-pacmaze-intro-movie-2-cctv-academy.mp4 CCTV PAC-MAZE v5.2 \u2014 Night Watch + Directed WebGL CCTV CCTV PAC-MAZEv5.2 NIGHT WATCH \/\/ LIVE FEEDSECURITY MAZE CHASE NL FR ENG Level01 Cams\ud83d\udcf9 \u00d71 Doel0 \/ 5 LEVEL 1: 1 CAMERA \u2022 EET 5 INBREKERS0 \/ 5 \u2212 \ud83d\udd0a 500% + LEVEL1 SWITCH CAMERA1\/1 GEGETEN0\/5 \u26f6 FULLSCREEN \u2715 EXIT INBREKER OPGEGETEN 1 [&hellip;]","og_url":"https:\/\/cctvacademy.be\/nl\/cctv-night-watch3\/","og_site_name":"RAS\/AIS CCTV Academy","article_publisher":"https:\/\/www.facebook.com\/profile.php?id=61576769152561","article_modified_time":"2026-09-20T17:46:32+00:00","twitter_card":"summary_large_image","schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"WebPage","@id":"https:\/\/cctvacademy.be\/cctv-night-watch3\/","url":"https:\/\/cctvacademy.be\/cctv-night-watch3\/","name":"CCTV Night Watch3 - RAS\/AIS CCTV Academy","isPartOf":{"@id":"https:\/\/cctvacademy.be\/#website"},"datePublished":"2026-09-20T17:16:01+00:00","dateModified":"2026-09-20T17:46:32+00:00","breadcrumb":{"@id":"https:\/\/cctvacademy.be\/cctv-night-watch3\/#breadcrumb"},"inLanguage":"nl-BE","potentialAction":[{"@type":"ReadAction","target":["https:\/\/cctvacademy.be\/cctv-night-watch3\/"]}]},{"@type":"BreadcrumbList","@id":"https:\/\/cctvacademy.be\/cctv-night-watch3\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/cctvacademy.be\/"},{"@type":"ListItem","position":2,"name":"CCTV Night Watch3"}]},{"@type":"WebSite","@id":"https:\/\/cctvacademy.be\/#website","url":"https:\/\/cctvacademy.be\/","name":"CCTV Academy","description":"CCTV Opleidingen en CCTV Micro Learning","potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/cctvacademy.be\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"nl-BE"}]}},"jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/cctvacademy.be\/nl\/wp-json\/wp\/v2\/pages\/51471","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/cctvacademy.be\/nl\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/cctvacademy.be\/nl\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/cctvacademy.be\/nl\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/cctvacademy.be\/nl\/wp-json\/wp\/v2\/comments?post=51471"}],"version-history":[{"count":19,"href":"https:\/\/cctvacademy.be\/nl\/wp-json\/wp\/v2\/pages\/51471\/revisions"}],"predecessor-version":[{"id":51492,"href":"https:\/\/cctvacademy.be\/nl\/wp-json\/wp\/v2\/pages\/51471\/revisions\/51492"}],"wp:attachment":[{"href":"https:\/\/cctvacademy.be\/nl\/wp-json\/wp\/v2\/media?parent=51471"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}