{"id":431,"date":"2026-07-28T18:04:09","date_gmt":"2026-07-28T18:04:09","guid":{"rendered":"https:\/\/staging.mqr.digital\/smart-classroom\/"},"modified":"2026-07-28T18:38:16","modified_gmt":"2026-07-28T18:38:16","slug":"classroom-3d-tour","status":"publish","type":"page","link":"https:\/\/digitalmqr.com\/ar\/classroom-3d-tour\/","title":{"rendered":"\u0627\u0644\u0635\u0641 \u0627\u0644\u0630\u0643\u064a"},"content":{"rendered":"\t\t<div data-elementor-type=\"wp-page\" data-elementor-id=\"431\" class=\"elementor elementor-431\">\n\t\t\t\t\t\t<section class=\"elementor-section elementor-top-section elementor-element elementor-element-bade47e elementor-section-boxed elementor-section-height-default elementor-section-height-default\" data-id=\"bade47e\" data-element_type=\"section\" data-e-type=\"section\">\n\t\t\t\t\t\t<div class=\"elementor-container elementor-column-gap-no\">\n\t\t\t\t\t<div class=\"elementor-column elementor-col-100 elementor-top-column elementor-element elementor-element-51e2b82\" data-id=\"51e2b82\" data-element_type=\"column\" data-e-type=\"column\">\n\t\t\t<div class=\"elementor-widget-wrap elementor-element-populated\">\n\t\t\t\t\t\t<div class=\"elementor-element elementor-element-6b08677 elementor-widget elementor-widget-shortcode\" data-id=\"6b08677\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"shortcode.default\">\n\t\t\t\t\t\t\t<div class=\"elementor-shortcode\">\t<div class=\"arx\" id=\"arx\">\n\t\t<a class=\"arx-skip\" href=\"#arx-after\">Skip the 3D tour<\/a>\n\t\t<div class=\"arx-stage\" id=\"arxStage\" role=\"region\"\n\t\t\taria-label=\"Interactive 3D tour of a Digital MQR smart classroom\">\n\t\t\t<p class=\"arx-sr\" id=\"arxLive\" role=\"status\" aria-live=\"polite\"><\/p>\n\t\t\t<canvas id=\"arxCanvas\"><\/canvas>\n\n\t\t\t<!-- premium DOM hotspot layer (projected from 3D anchors) -->\n\t\t\t<div class=\"arxHot\" id=\"arxHot\" aria-label=\"Classroom technology markers\"><\/div>\n\n\t\t\t<!-- Phase 4: AR-style overlay anchored to the active station -->\n\t\t\t<div class=\"arxAR\" id=\"arxAR\" aria-hidden=\"true\"><div class=\"arxARin\"><span class=\"arxARbar\"><\/span><b><\/b><\/div><\/div>\n\n\t\t\t<!-- cinematic intro -->\n\t\t\t<div class=\"arxIntroX\" id=\"arxIntroX\">\n\t\t\t\t<canvas class=\"ptc\"><\/canvas>\n\t\t\t\t<div class=\"arxIntroTxt\">\n\t\t\t\t\t<div>\n\t\t\t\t\t\t<div class=\"ln s1\">\n\t\t\t\t\t\t\t<h2>THE CLASSROOM OF THE <b>FUTURE<\/b><\/h2>\n\t\t\t\t\t\t\t<div class=\"rule\"><\/div>\n\t\t\t\t\t\t<\/div>\n\t\t\t\t\t\t<div class=\"ln s2\">\n\t\t\t\t\t\t\t<h2><b>Digital MQR<\/b><\/h2>\n\t\t\t\t\t\t\t<p>Smart Classroom Experience<\/p>\n\t\t\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<button type=\"button\" class=\"arxSkip\">Skip intro<\/button>\n\t\t\t<\/div>\n\n\t\t\t<i class=\"arx-vig\" aria-hidden=\"true\"><\/i>\n\n\t\t\t<div class=\"arx-hud\" aria-hidden=\"true\">\n\t\t\t\t<i class=\"c tl\"><\/i><i class=\"c tr\"><\/i><i class=\"c bl\"><\/i><i class=\"c br\"><\/i>\n\t\t\t\t<div class=\"arx-badge\"><span class=\"dot\"><\/span>AR CLASSROOM TOUR<\/div>\n\t\t\t<\/div>\n\n\t\t\t<button type=\"button\" class=\"arx-exit\" id=\"arxExit\" hidden><span>&#8249;<\/span> Exit tour<\/button>\n\n\t\t\t<div class=\"arx-intro\" id=\"arxIntro\">\n\t\t\t\t<div class=\"arx-intro-in\">\n\t\t\t\t\t<div class=\"arx-kicker\"><span class=\"ln\"><\/span><span>Digital MQR \u00b7 Smart Classroom<\/span><\/div>\n\t\t\t\t\t<h1>Step inside the <em>smart classroom<\/em><\/h1>\n\t\t\t\t\t<p>A live 3D tour of a Digital MQR classroom \u2014 14 technologies, one room.<\/p>\n\t\t\t\t\t<button type=\"button\" class=\"btn btn-primary\" id=\"arxEnter\"><span>Enter the classroom<\/span> <span class=\"arr\">&rarr;<\/span><\/button>\n\t\t\t\t<\/div>\n\t\t\t<\/div>\n\n\t\t\t<div class=\"arx-card\" id=\"arxCard\">\n\t\t\t\t<div class=\"arx-card-top\"><span class=\"arx-card-step\" id=\"arxStep\">01<\/span><span class=\"arx-card-label\" id=\"arxLabel\"><\/span>\n\t\t\t\t\t<button type=\"button\" class=\"arx-card-more\" id=\"arxCardToggle\" aria-expanded=\"false\" aria-controls=\"arxCard\"\n\t\t\t\t\t\taria-label=\"Show details\">\n\t\t\t\t\t\t<svg viewBox=\"0 0 24 24\" aria-hidden=\"true\"><path d=\"M12 15.4 5.6 9l1.4-1.4 5 5 5-5L18.4 9 12 15.4Z\"\/><\/svg>\n\t\t\t\t\t<\/button>\n\t\t\t\t<\/div>\n\t\t\t\t<h2 id=\"arxTitle\"><\/h2>\n\t\t\t\t<p id=\"arxBody\"><\/p>\n\t\t\t\t<div class=\"arx-chips\" id=\"arxChips\"><\/div>\n\t\t\t\t<ul class=\"arx-feat\" id=\"arxFeat\"><\/ul>\n\t\t\t\t<div class=\"arx-shots\" id=\"arxShots\"><\/div>\n\t\t\t\t<div class=\"arx-ctas\" id=\"arxCta\"><\/div>\n\t\t\t\t<a class=\"arx-more\" id=\"arxMore\" href=\"#\" hidden>View full product details <span class=\"arr\">&rarr;<\/span><\/a>\n\t\t\t<\/div>\n\n\t\t\t<div class=\"arx-bar\" id=\"arxBar\">\n\t\t\t\t<button type=\"button\" id=\"arxPrev\" class=\"nav\" aria-label=\"Previous station\"><span>&#8249;<\/span><em>Prev<\/em><\/button>\n\t\t\t\t<button type=\"button\" id=\"arxPlay\" class=\"play\" aria-label=\"Start the guided tour\" title=\"Guided tour\"><span id=\"arxPlayIc\">&#9654;<\/span><\/button>\n\t\t\t\t<button type=\"button\" id=\"arxNext\" class=\"nav\" aria-label=\"Next station\"><em>Next<\/em><span>&#8250;<\/span><\/button>\n\t\t\t\t<span class=\"arx-count\"><b id=\"arxNum\">01<\/b>\/<i>14<\/i><\/span>\n\t\t\t\t<i class=\"arx-sep\" aria-hidden=\"true\"><\/i>\n\t\t\t\t<button type=\"button\" class=\"tool\" id=\"arxBtnTransform\" title=\"Transform the classroom\" aria-label=\"Transform the classroom\">\n\t\t\t\t\t<svg viewBox=\"0 0 24 24\" aria-hidden=\"true\"><path d=\"M13 2 4.5 13.2h5.1L9 22l8.6-11.3h-5.2L13 2Z\"\/><\/svg><em>Transform<\/em><\/button>\n\t\t\t\t<button type=\"button\" class=\"tool\" id=\"arxBtnMap\" title=\"Classroom map\" aria-label=\"Toggle the classroom map\" aria-pressed=\"false\">\n\t\t\t\t\t<svg viewBox=\"0 0 24 24\" aria-hidden=\"true\"><path d=\"M9 3 3 5.4v15.1L9 18l6 2.5 6-2.4V3l-6 2.5L9 3Zm0 2.3 6 2.5v10.4l-6-2.5V5.3Z\"\/><\/svg><em>Map<\/em><\/button>\n\t\t\t\t<button type=\"button\" class=\"tool\" id=\"arxBtnReset\" title=\"Reset the view\" aria-label=\"Reset the view\">\n\t\t\t\t\t<svg viewBox=\"0 0 24 24\" aria-hidden=\"true\"><path d=\"M12 5V2L7.5 6.5 12 11V8a5.5 5.5 0 1 1-5.5 5.5H4A8 8 0 1 0 12 5Z\"\/><\/svg><em>Reset<\/em><\/button>\n\t\t\t\t<button type=\"button\" class=\"tool ar\" id=\"arxBtnAR\" hidden title=\"View in your room\" aria-label=\"View the classroom in your own room\">\n\t\t\t\t\t<svg viewBox=\"0 0 24 24\" aria-hidden=\"true\"><path d=\"M12 2 3 6.8v10.4L12 22l9-4.8V6.8L12 2Zm0 2.3 6.4 3.4L12 11.2 5.6 7.7 12 4.3ZM5 9.5l6 3.2v6.6l-6-3.2V9.5Zm8 9.8v-6.6l6-3.2v6.6l-6 3.2Z\"\/><\/svg>\n\t\t\t\t\t<em>AR<\/em><\/button>\n\t\t\t\t<button type=\"button\" class=\"tool\" id=\"arxBtnSound\" title=\"Sound\" aria-label=\"Turn sound on\" aria-pressed=\"false\">\n\t\t\t\t\t<svg viewBox=\"0 0 24 24\" aria-hidden=\"true\" class=\"on\"><path d=\"M4 9v6h4l5 4V5L8 9H4Zm11.5 3a3.5 3.5 0 0 0-2-3.15v6.3A3.5 3.5 0 0 0 15.5 12Zm-2 -7.6v2.06A5.5 5.5 0 0 1 13.5 17.5v2.06A7.5 7.5 0 0 0 13.5 4.4Z\"\/><\/svg>\n\t\t\t\t\t<svg viewBox=\"0 0 24 24\" aria-hidden=\"true\" class=\"off\"><path d=\"M4 9v6h4l5 4V5L8 9H4Zm15.6 3 2.1-2.1-1.4-1.4L18.2 10.6 16.1 8.5l-1.4 1.4 2.1 2.1-2.1 2.1 1.4 1.4 2.1-2.1 2.1 2.1 1.4-1.4L19.6 12Z\"\/><\/svg>\n\t\t\t\t\t<em>Sound<\/em><\/button>\n\t\t\t\t<button type=\"button\" class=\"tool\" id=\"arxBtnFull\" title=\"Fullscreen\" aria-label=\"Toggle fullscreen\" aria-pressed=\"false\">\n\t\t\t\t\t<svg viewBox=\"0 0 24 24\" aria-hidden=\"true\"><path d=\"M3 9V3h6v2.2H5.2V9H3Zm12-6h6v6h-2.2V5.2H15V3ZM3 15h2.2v3.8H9V21H3v-6Zm15.8 0H21v6h-6v-2.2h3.8V15Z\"\/><\/svg><em>Full<\/em><\/button>\n\t\t\t<\/div>\n\n\t\t\t<!-- Phase 4: interactive floor plan -->\n\t\t\t<div class=\"arxMap\" id=\"arxMap\" hidden>\n\t\t\t\t<div class=\"arxMapTop\"><span>Classroom plan<\/span>\n\t\t\t\t\t<button type=\"button\" id=\"arxMapX\" aria-label=\"Close the map\">&times;<\/button><\/div>\n\t\t\t\t<div class=\"arxMapPlan\" id=\"arxMapPlan\">\n\t\t\t\t\t<span class=\"arxMapBoard\"><\/span>\n\t\t\t\t\t<i class=\"arxMapMe\" id=\"arxMapMe\"><\/i>\n\t\t\t\t<\/div>\n\t\t\t\t<p class=\"arxMapTip\">Tap any point to fly there<\/p>\n\t\t\t<\/div>\n\n\t\t\t<!-- Phase 4: guided tour ribbon -->\n\t\t\t<div class=\"arxGuide\" id=\"arxGuide\" hidden role=\"region\" aria-label=\"Guided tour\">\n\t\t\t\t<div class=\"arxGuideBar\"><i id=\"arxGBar\"><\/i><\/div>\n\t\t\t\t<div class=\"arxGuideIn\">\n\t\t\t\t\t<span class=\"arxGStep\" id=\"arxGStep\">01 \/ 06<\/span>\n\t\t\t\t\t<p id=\"arxGText\"><\/p>\n\t\t\t\t\t<div class=\"arxGBtns\">\n\t\t\t\t\t\t<button type=\"button\" id=\"arxGPrev\">Previous<\/button>\n\t\t\t\t\t\t<button type=\"button\" id=\"arxGNext\" class=\"p\">Next &rarr;<\/button>\n\t\t\t\t\t\t<button type=\"button\" id=\"arxGExit\" class=\"x\">Exit tour<\/button>\n\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t<\/div>\n\n\t\t\t<!-- Phase 4: TRANSFORM CLASSROOM captions -->\n\t\t\t<div class=\"arxTrans\" id=\"arxTrans\" hidden aria-live=\"polite\"><b id=\"arxTransT\"><\/b><span class=\"arxTransSub\" id=\"arxTransS\"><\/span><\/div>\n\n\t\t\t<div class=\"arx-dots\" id=\"arxDots\">\n\t\t\t\t\t\t\t\t\t<button type=\"button\" data-i=\"0\"><span><\/span><em>Overview<\/em><\/button>\n\t\t\t\t\t\t\t\t\t<button type=\"button\" data-i=\"1\"><span><\/span><em>Interactive Panel<\/em><\/button>\n\t\t\t\t\t\t\t\t\t<button type=\"button\" data-i=\"2\"><span><\/span><em>UST Projector<\/em><\/button>\n\t\t\t\t\t\t\t\t\t<button type=\"button\" data-i=\"3\"><span><\/span><em>Interactive Floor<\/em><\/button>\n\t\t\t\t\t\t\t\t\t<button type=\"button\" data-i=\"4\"><span><\/span><em>AR Sandbox<\/em><\/button>\n\t\t\t\t\t\t\t\t\t<button type=\"button\" data-i=\"5\"><span><\/span><em>VR Corner<\/em><\/button>\n\t\t\t\t\t\t\t\t\t<button type=\"button\" data-i=\"6\"><span><\/span><em>Robotics &amp; 3D<\/em><\/button>\n\t\t\t\t\t\t\t\t\t<button type=\"button\" data-i=\"7\"><span><\/span><em>Science Lab<\/em><\/button>\n\t\t\t\t\t\t\t\t\t<button type=\"button\" data-i=\"8\"><span><\/span><em>Zero-Client Lab<\/em><\/button>\n\t\t\t\t\t\t\t\t\t<button type=\"button\" data-i=\"9\"><span><\/span><em>Teacher Station<\/em><\/button>\n\t\t\t\t\t\t\t\t\t<button type=\"button\" data-i=\"10\"><span><\/span><em>Signage<\/em><\/button>\n\t\t\t\t\t\t\t\t\t<button type=\"button\" data-i=\"11\"><span><\/span><em>Smart Access<\/em><\/button>\n\t\t\t\t\t\t\t\t\t<button type=\"button\" data-i=\"12\"><span><\/span><em>Charging Cart<\/em><\/button>\n\t\t\t\t\t\t\t\t\t<button type=\"button\" data-i=\"13\"><span><\/span><em>School Uniform<\/em><\/button>\n\t\t\t\t\t\t\t<\/div>\n\n\t\t\t<div class=\"arx-hint\" id=\"arxHint\">Drag to look \u00b7 Scroll to zoom \u00b7 Tap a marker \u00b7 G tour \u00b7 T transform \u00b7 M map<\/div>\n\n\t\t\t<div class=\"arx-fallback\" id=\"arxFallback\" hidden>\n\t\t\t\t<p>The 3D tour could not start in this browser.<\/p>\n\t\t\t\t<a class=\"btn btn-primary\" href=\"https:\/\/digitalmqr.com\/ar\/solutions\/\"><span>Our Solutions<\/span><\/a>\n\t\t\t<\/div>\n\t\t<\/div>\n\n\n\t\t<section class=\"arx-story\" id=\"arx-story\">\n\t\t\t<div class=\"container\">\n\t\t\t\t<p class=\"arx-story-kicker\">From survey to first lesson<\/p>\n\t\t\t\t<h2 class=\"arx-story-lead\">A smart classroom is a project, not a purchase.<\/h2>\n\t\t\t\t<p class=\"arx-story-sub\">Every room you just walked through is delivered end to end by one team, across Oman.<\/p>\n\n\t\t\t\t<ol class=\"arx-steps\">\n\t\t\t\t\t<li>\n\t\t\t\t\t\t<span class=\"n\">01<\/span>\n\t\t\t\t\t\t<h3>We survey the room<\/h3>\n\t\t\t\t\t\t<p>Dimensions, light, power, network and how the class actually moves \u2014 measured on site before anything is specified.<\/p>\n\t\t\t\t\t<\/li>\n\t\t\t\t\t<li>\n\t\t\t\t\t\t<span class=\"n\">02<\/span>\n\t\t\t\t\t\t<h3>We design around the curriculum<\/h3>\n\t\t\t\t\t\t<p>The technology follows the subjects and the age group, not a catalogue. You get a layout, a spec and a price before you commit.<\/p>\n\t\t\t\t\t<\/li>\n\t\t\t\t\t<li>\n\t\t\t\t\t\t<span class=\"n\">03<\/span>\n\t\t\t\t\t\t<h3>We install and integrate<\/h3>\n\t\t\t\t\t\t<p>Mounting, cabling, network, accounts and the school platform \u2014 commissioned as one working room, not a pile of boxes.<\/p>\n\t\t\t\t\t<\/li>\n\t\t\t\t\t<li>\n\t\t\t\t\t\t<span class=\"n\">04<\/span>\n\t\t\t\t\t\t<h3>We train the teachers<\/h3>\n\t\t\t\t\t\t<p>Hands-on sessions in the actual room, with lesson-ready material. Confidence on day one is what makes the investment land.<\/p>\n\t\t\t\t\t<\/li>\n\t\t\t\t\t<li>\n\t\t\t\t\t\t<span class=\"n\">05<\/span>\n\t\t\t\t\t\t<h3>We stay for the support<\/h3>\n\t\t\t\t\t\t<p>Warranty, spares, updates and a number that answers \u2014 for the life of the room.<\/p>\n\t\t\t\t\t<\/li>\n\t\t\t\t<\/ol>\n\t\t\t<\/div>\n\t\t<\/section>\n\n\t\t<section class=\"arx-cta\" id=\"arx-after\" tabindex=\"-1\">\n\t\t\t<div class=\"container center\">\n\t\t\t\t<h2>Want this classroom in your school?<\/h2>\n\t\t\t\t<p>We survey, design, install, train and support \u2014 end to end, across Oman.<\/p>\n\t\t\t\t<a class=\"btn btn-light\" href=\"https:\/\/digitalmqr.com\/ar\/contact\/\"><span>Book a live demo<\/span> <span class=\"arr\">&rarr;<\/span><\/a>\n\t\t\t<\/div>\n\t\t<\/section>\n\t<\/div>\n\n\t<style>\n\t.arx{--arx-e:cubic-bezier(.22,.85,.28,1)}\n\t\/* Sticky + a scroll-aware height (set in JS) means the stage always fills\n\t   exactly the space the visitor can see \u2014 never taller, never shorter \u2014 so\n\t   every control stays on screen without scrolling. *\/\n\t\/* NOT sticky: the stage sits at the top of its own section so the story\n\t   and the call to action below it are reachable by scrolling. The height\n\t   is measured against whatever is obstructing the viewport (see fitStage),\n\t   which is what keeps every control on screen without scrolling. *\/\n\t.arx-stage{position:relative;height:var(--arx-h,100svh);min-height:420px;overflow:hidden;\n\t\tscroll-margin-top:var(--arx-top,0px);\n\t\tbackground:radial-gradient(70% 90% at 18% 8%,#3d2477 0%,transparent 55%),\n\t\t           radial-gradient(60% 80% at 85% 90%,#243a8f 0%,transparent 55%),\n\t\t           linear-gradient(165deg,#160e33 0%,#1d1245 45%,#12102e 100%)}\n\t#arxCanvas{position:absolute;inset:0;width:100%;height:100%;display:block;touch-action:none;cursor:grab}\n\t#arxCanvas.drag{cursor:grabbing}\n\t.arx-hud{position:absolute;inset:0;pointer-events:none;z-index:5}\n\t.arx-hud .c{position:absolute;width:30px;height:30px;border:2px solid rgba(185,167,255,.4)}\n\t.arx-hud .c.tl{top:16px;left:16px;border-right:0;border-bottom:0}\n\t.arx-hud .c.tr{top:16px;right:16px;border-left:0;border-bottom:0}\n\t.arx-hud .c.bl{bottom:16px;left:16px;border-right:0;border-top:0}\n\t.arx-hud .c.br{bottom:16px;right:16px;border-left:0;border-top:0}\n\t.arx-badge{position:absolute;top:22px;left:50%;transform:translateX(-50%);display:flex;gap:9px;align-items:center;\n\t\tfont:800 .66rem\/1 inherit;letter-spacing:.22em;color:#c9bdf2;background:rgba(15,10,31,.5);\n\t\tborder:1px solid rgba(185,167,255,.28);border-radius:999px;padding:8px 15px;backdrop-filter:blur(6px)}\n\t.arx-badge .dot{width:7px;height:7px;border-radius:50%;background:#39e29a;box-shadow:0 0 10px #39e29a;animation:arxBlink 1.6s infinite}\n\t@keyframes arxBlink{0%,100%{opacity:1}50%{opacity:.35}}\n\t.arx-exit{position:absolute;z-index:8;top:20px;left:20px;display:inline-flex;align-items:center;gap:8px;\n\t\tbackground:rgba(15,10,31,.55)!important;border:1px solid rgba(185,167,255,.3)!important;color:#fff;border-radius:999px;\n\t\tpadding:9px 16px;font:600 .82rem\/1 inherit;cursor:pointer;backdrop-filter:blur(8px);transition:.25s;font-family:inherit}\n\t.arx-exit:hover{background:rgba(112,144,224,.35)!important}\n\t.arx-exit[hidden]{display:none!important}\n\t.arx-exit span{font-size:1.1rem;line-height:0}\n\t.arx-intro{position:absolute;inset:0;z-index:9;display:grid;place-items:center;text-align:center;\n\t\tbackground:radial-gradient(60% 60% at 50% 40%,rgba(80,32,143,.45),transparent 70%),linear-gradient(180deg,rgba(13,8,29,.78),rgba(13,8,29,.92));\n\t\ttransition:opacity .8s var(--arx-e),visibility .8s}\n\t.arx-intro.off{opacity:0;visibility:hidden;pointer-events:none}\n\t.arx-intro-in{max-width:640px;padding:24px}\n\t.arx-kicker{display:flex;justify-content:center;align-items:center;gap:12px;font:800 .72rem\/1 inherit;letter-spacing:.2em;text-transform:uppercase;color:#c9bdf2;margin-bottom:18px}\n\t.arx-kicker .ln{width:44px;height:2px;background:linear-gradient(90deg,#7090E0,transparent)}\n\t.arx-intro h1{color:#fff;font-size:clamp(2rem,5.6vw,3.8rem);line-height:1.05;letter-spacing:-.02em;margin:0 0 14px}\n\t.arx-intro h1 em{font-style:normal;background:linear-gradient(100deg,#b9a7ff,#7090E0 60%,#fff);-webkit-background-clip:text;background-clip:text;color:transparent}\n\t.arx-intro p{color:rgba(255,255,255,.8);margin:0 0 26px}\n\t.arx-card{position:absolute;z-index:7;left:22px;bottom:104px;width:min(420px,calc(100% - 44px));\n\t\tbackground:linear-gradient(165deg,rgba(26,16,52,.88),rgba(20,13,42,.82));\n\t\tborder:1px solid rgba(185,167,255,.3);border-radius:20px;padding:20px 22px;color:#fff;backdrop-filter:blur(12px);\n\t\tbox-shadow:0 24px 60px rgba(8,4,20,.5);\n\t\topacity:0;transform:translateY(18px);transition:opacity .5s var(--arx-e),transform .5s var(--arx-e);pointer-events:none}\n\t.arx-card.on{opacity:1;transform:none;pointer-events:auto}\n\t.arx-card-top{display:flex;align-items:center;gap:10px;margin-bottom:8px}\n\t.arx-card-step{font:800 .72rem\/1 inherit;letter-spacing:.24em;color:#8fa7ee}\n\t.arx-card-label{font:700 .64rem\/1 inherit;letter-spacing:.14em;text-transform:uppercase;color:#0d0a1e;\n\t\tbackground:linear-gradient(120deg,#b9a7ff,#8fb0ff);border-radius:999px;padding:5px 11px}\n\t.arx-card h2{color:#fff;font-size:1.28rem;margin:0 0 8px;line-height:1.25}\n\t.arx-card p{color:rgba(255,255,255,.82);font-size:.9rem;line-height:1.65;margin:0 0 12px}\n\t.arx-chips{display:flex;gap:7px;flex-wrap:wrap;margin-bottom:4px}\n\t.arx-chips span{font:600 .72rem\/1 inherit;color:#d9d2f5;background:rgba(255,255,255,.08);\n\t\tborder:1px solid rgba(185,167,255,.25);border-radius:999px;padding:6px 11px}\n\t.arx-shots{display:flex;gap:7px;flex-wrap:wrap;margin-top:12px}\n\t.arx-shots a{width:62px;height:46px;border-radius:9px;overflow:hidden;border:1px solid rgba(185,167,255,.35);\n\t\tflex:none;opacity:0;transform:translateY(8px);animation:arxShotIn .45s cubic-bezier(.22,.85,.28,1) forwards;transition:transform .25s,border-color .25s}\n\t.arx-shots a:hover{transform:translateY(-3px) scale(1.06);border-color:#b9a7ff;z-index:2}\n\t.arx-shots img{width:100%;height:100%;object-fit:cover;display:block}\n\t@keyframes arxShotIn{to{opacity:1;transform:none}}\n\t.arx-more{display:inline-flex;align-items:center;gap:6px;margin-top:10px;color:#b9a7ff;font:700 .84rem\/1 inherit;transition:.2s}\n\t.arx-more:hover{color:#fff}\n\t.arx-more[hidden]{display:none!important}\n\t.arx-bar{position:absolute;z-index:8;left:50%;bottom:22px;transform:translateX(-50%);display:flex;align-items:center;gap:10px;\n\t\tbackground:rgba(15,10,31,.55);border:1px solid rgba(185,167,255,.28);border-radius:999px;padding:8px 14px;backdrop-filter:blur(10px)}\n\t.arx-bar .nav{display:inline-flex;align-items:center;gap:7px;background:rgba(255,255,255,.08)!important;border:1px solid rgba(185,167,255,.25)!important;\n\t\tcolor:#fff;border-radius:999px;padding:10px 16px;font:600 .82rem\/1 inherit;cursor:pointer;transition:.2s;font-family:inherit}\n\t.arx-bar .nav:hover{background:rgba(112,144,224,.35)!important}\n\t.arx-bar .nav span{font-size:1.15rem;line-height:1}\n\t.arx-bar .play{width:52px;height:52px;border-radius:50%;border:0;background:linear-gradient(135deg,#50208F,#7090E0)!important;\n\t\tcolor:#fff;font-size:1.1rem;cursor:pointer;display:grid;place-items:center;box-shadow:0 10px 26px rgba(80,32,143,.5);transition:.25s;font-family:inherit}\n\t.arx-bar .play:hover{transform:scale(1.06)}\n\t.arx-count{color:#cfc6ee;font:700 .8rem\/1 inherit;letter-spacing:.08em;padding-inline:6px}\n\t.arx-count b{color:#fff}.arx-count i{font-style:normal;opacity:.6}\n\t.arx-dots{position:absolute;z-index:8;right:16px;top:50%;transform:translateY(-50%);display:flex;flex-direction:column;gap:7px;max-height:82%;overflow-y:auto;scrollbar-width:none;align-items:flex-end}\n\t.arx-dots::-webkit-scrollbar{display:none}\n\t.arx-dots button{display:inline-flex;align-items:center;gap:8px;background:rgba(15,10,31,.6)!important;border:1px solid rgba(185,167,255,.28)!important;\n\t\tborder-radius:999px;padding:8px 13px 8px 10px;cursor:pointer;transition:.25s;backdrop-filter:blur(8px);font-family:inherit;box-shadow:none!important}\n\t.arx-dots button:hover{background:rgba(112,144,224,.4)!important}\n\t.arx-dots span{width:8px;height:8px;border-radius:50%;background:rgba(255,255,255,.45);transition:.3s;flex:none}\n\t.arx-dots em{font:600 .72rem\/1 inherit;font-style:normal;color:#e6e0fa;background:none!important;border:0!important;padding:0!important;\n\t\topacity:1!important;transform:none!important;transition:.25s;white-space:nowrap;pointer-events:none}\n\t.arx-dots button.on{background:linear-gradient(135deg,#50208F,#7090E0)!important;border-color:transparent!important;box-shadow:0 8px 22px rgba(80,32,143,.45)!important}\n\t.arx-dots button.on span{background:#fff;box-shadow:0 0 10px #fff}\n\t.arx-dots button.on em{color:#fff}\n\t.arx-hint{position:absolute;z-index:6;left:50%;bottom:90px;transform:translateX(-50%);color:rgba(255,255,255,.5);\n\t\tfont-size:.74rem;letter-spacing:.04em;transition:opacity .6s;pointer-events:none;text-align:center;padding:0 12px}\n\t.arx-hint.off{opacity:0}\n\t.arx-fallback{position:absolute;inset:0;z-index:10;display:grid;place-items:center;text-align:center;color:#fff;background:#141026;padding:24px}\n\t.arx-fallback[hidden]{display:none!important}\n\t.arx-cta{padding:clamp(60px,9vw,110px) 0;background:linear-gradient(135deg,#1a1040,#2d1b69 55%,#363996);color:#fff}\n\t.arx-cta h2{color:#fff;font-size:clamp(1.7rem,4vw,2.7rem);margin:0 0 10px}\n\t.arx-cta p{color:rgba(255,255,255,.8);margin:0 0 24px}\n\t\/* short screens (laptops, landscape phones): keep every control on-screen *\/\n\t@media(max-height:760px){\n\t\t.arx-stage{min-height:420px}\n\t\t.arx-card{bottom:92px;padding:15px 17px;max-height:calc(100% - 180px);overflow:auto}\n\t\t.arx-card h2{font-size:1.12rem}.arx-card p{font-size:.84rem;line-height:1.55;margin-bottom:9px}\n\t\t.arx-shots a{width:54px;height:40px}\n\t\t.arx-bar{bottom:14px;padding:6px 11px;gap:8px}\n\t\t.arx-bar .play{width:44px;height:44px}\n\t\t.arx-bar .nav{padding:8px 13px}\n\t\t.arx-hint{bottom:76px}\n\t\t.arx-dots{gap:5px;max-height:70%}\n\t\t.arx-dots button{padding:6px 11px 6px 9px}\n\t\t.arx-dots em{font-size:.68rem}\n\t}\n\t@media(max-height:600px){\n\t\t.arx-card{display:none}\n\t\t.arx-bar .tool em{display:none}\n\t}\n\t@media(max-width:760px){\n\t\t.arx-stage{min-height:480px}\n\t\t.arx-card{left:12px;right:12px;width:auto;bottom:120px;padding:14px 16px;max-height:34svh;overflow:auto}\n\t\t.arx-card h2{font-size:1.05rem}.arx-card p{font-size:.82rem}\n\t\t.arx-dots{top:auto;right:auto;bottom:78px;left:50%;transform:translateX(-50%);flex-direction:row;gap:6px;max-height:none;\n\t\t\tmax-width:94vw;overflow-x:auto;overflow-y:hidden;padding:4px 6px;align-items:center}\n\t\t.arx-dots button{padding:7px 11px 7px 9px;flex:none}\n\t\t.arx-dots em{font-size:.66rem}\n\t\t.arx-dots span{width:7px;height:7px}\n\t\t.arx-bar{bottom:14px;padding:6px 10px;gap:7px}\n\t\t.arx-bar .nav em{display:none}\n\t\t.arx-bar .nav{padding:10px 12px}\n\t\t.arx-bar .play{width:46px;height:46px}\n\t\t.arx-hint{display:none}\n\t\t.arx-exit{top:14px;left:14px;padding:8px 13px;font-size:.76rem}\n\t}\n\n\/* ================================================================\n   AR\/3D CLASSROOM \u2014 PHASE 3 CORE\n   Cinematic intro \u00b7 premium DOM hotspots \u00b7 focus system \u00b7 panels\n   ================================================================ *\/\n\n.arx{--e-out:cubic-bezier(.16,1,.3,1);--e-io:cubic-bezier(.65,0,.35,1)}\n\n\/* ---------------- 1. CINEMATIC INTRO ---------------- *\/\n.arxIntroX{position:absolute;inset:0;z-index:40;pointer-events:none;overflow:hidden;\n  display:none;                       \/* only shown once the visitor presses Enter *\/\n  background:radial-gradient(120% 90% at 50% 45%,#1b1140 0%,#0d0820 55%,#070312 100%)}\n.arxIntroX.playing{display:block}\n.arxIntroX.done{opacity:0;visibility:hidden;transition:opacity 1s var(--e-out),visibility 1s}\n.arxIntroX canvas.ptc{position:absolute;inset:0;width:100%;height:100%}\n.arxIntroTxt{position:absolute;inset:0;display:grid;place-items:center;text-align:center;padding:24px}\n.arxIntroTxt .ln{opacity:0;transform:translateY(14px);filter:blur(6px)}\n.arxIntroTxt .ln.on{opacity:1;transform:none;filter:blur(0);\n  transition:opacity 1s var(--e-out),transform 1.1s var(--e-out),filter 1s var(--e-out)}\n.arxIntroTxt .ln.off{opacity:0;transform:translateY(-10px);filter:blur(5px);\n  transition:opacity .7s ease,transform .8s ease,filter .7s ease}\n.arxIntroTxt h2{color:#fff;font-size:clamp(1.5rem,4.4vw,3.1rem);letter-spacing:.02em;margin:0;line-height:1.15;font-weight:800}\n.arxIntroTxt h2 b{background:linear-gradient(100deg,#c9bcff,#8fb0ff 55%,#fff);\n  -webkit-background-clip:text;background-clip:text;color:transparent;font-weight:800}\n.arxIntroTxt p{color:rgba(255,255,255,.62);margin:14px 0 0;font-size:.94rem;letter-spacing:.22em;text-transform:uppercase}\n.arxIntroTxt .rule{width:0;height:1px;margin:22px auto 0;\n  background:linear-gradient(90deg,transparent,#8fb0ff,transparent);transition:width 1.4s var(--e-out)}\n.arxIntroTxt .rule.on{width:min(340px,62vw)}\n\/* skip *\/\n.arxSkip{position:absolute;z-index:44;right:22px;bottom:22px;pointer-events:auto;\n  background:rgba(255,255,255,.08);border:1px solid rgba(185,167,255,.34);color:#e8e3fb;\n  border-radius:999px;padding:9px 18px;font:600 .78rem\/1 inherit;cursor:pointer;backdrop-filter:blur(8px);transition:.25s}\n.arxSkip:hover{background:rgba(255,255,255,.16);color:#fff}\n[dir=\"rtl\"] .arxSkip{right:auto;left:22px}\n\n\/* ---------------- 2. PREMIUM HOTSPOTS (DOM overlay) ---------------- *\/\n.arxHot{position:absolute;inset:0;z-index:6;pointer-events:none;overflow:hidden}\n.hot{position:absolute;transform:translate(-50%,-50%);pointer-events:auto;background:none;border:0;\n  padding:0;cursor:pointer;font-family:inherit;transition:opacity .35s ease}\n.hot .ring{position:relative;display:block;width:30px;height:30px;border-radius:50%;\n  border:1.5px solid rgba(201,188,255,.85);background:rgba(20,12,44,.42);backdrop-filter:blur(3px);\n  transition:transform .35s var(--e-out),border-color .3s,background .3s}\n.hot .ring::before{content:\"\";position:absolute;inset:9px;border-radius:50%;\n  background:linear-gradient(135deg,#b9a7ff,#8fb0ff);transition:inset .35s var(--e-out)}\n\/* calm outward pulse \u2014 one element, GPU-only *\/\n.hot .ring::after{content:\"\";position:absolute;inset:-1.5px;border-radius:50%;\n  border:1.5px solid rgba(159,140,255,.55);animation:hotPulse 2.8s var(--e-out) infinite}\n@keyframes hotPulse{0%{transform:scale(1);opacity:.55}70%{transform:scale(2.1);opacity:0}100%{opacity:0}}\n.hot .lbl{position:absolute;left:44px;top:50%;transform:translateY(-50%) translateX(-6px);\n  white-space:nowrap;opacity:0;pointer-events:none;\n  background:rgba(15,10,31,.72);border:1px solid rgba(185,167,255,.3);border-radius:999px;\n  padding:7px 14px;color:#fff;font:600 .78rem\/1 inherit;backdrop-filter:blur(10px);\n  transition:opacity .3s var(--e-out),transform .35s var(--e-out)}\n\/* connector line from marker to label *\/\n.hot .lnk{position:absolute;left:15px;top:50%;width:0;height:1px;\n  background:linear-gradient(90deg,rgba(185,167,255,.9),rgba(185,167,255,.15));\n  transition:width .35s var(--e-out)}\n.hot:hover .ring,.hot:focus-visible .ring{transform:scale(1.22);border-color:#fff;background:rgba(80,32,143,.5)}\n.hot:hover .ring::before,.hot:focus-visible .ring::before{inset:7px}\n.hot:hover .lbl,.hot:focus-visible .lbl{opacity:1;transform:translateY(-50%) translateX(0)}\n.hot:hover .lnk,.hot:focus-visible .lnk{width:26px}\n.hot:focus-visible{outline:none}\n.hot:focus-visible .ring{box-shadow:0 0 0 3px rgba(143,176,255,.5)}\n.hot.is-active .ring{border-color:#fff;transform:scale(1.16)}\n.hot.is-active .ring::before{background:#fff}\n.hot.is-active .ring::after{animation-duration:1.8s}\n.hot.is-dim{opacity:.18}\n.hot.is-hidden{opacity:0;pointer-events:none}\n\/* RTL: labels flip to the other side *\/\n[dir=\"rtl\"] .hot .lbl{left:auto;right:44px;transform:translateY(-50%) translateX(6px)}\n[dir=\"rtl\"] .hot:hover .lbl,[dir=\"rtl\"] .hot:focus-visible .lbl{transform:translateY(-50%) translateX(0)}\n[dir=\"rtl\"] .hot .lnk{left:auto;right:15px;background:linear-gradient(270deg,rgba(185,167,255,.9),rgba(185,167,255,.15))}\n\n\/* ---------------- 3. PRODUCT PANEL ---------------- *\/\n.arx-card{\n  \/* refined choreography: blur + lift + scale *\/\n  transform:translateY(22px) scale(.985);filter:blur(8px);\n  transition:opacity .55s var(--e-out),transform .65s var(--e-out),filter .55s var(--e-out)}\n.arx-card.on{transform:none;filter:blur(0)}\n.arx-feat{list-style:none;margin:12px 0 0;padding:0;display:grid;gap:8px}\n.arx-feat li{display:flex;gap:9px;align-items:flex-start;color:rgba(255,255,255,.84);\n  font-size:.85rem;line-height:1.5;opacity:0;transform:translateY(8px)}\n.arx-card.on .arx-feat li{opacity:1;transform:none;\n  transition:opacity .5s var(--e-out),transform .5s var(--e-out)}\n.arx-card.on .arx-feat li:nth-child(1){transition-delay:.18s}\n.arx-card.on .arx-feat li:nth-child(2){transition-delay:.26s}\n.arx-card.on .arx-feat li:nth-child(3){transition-delay:.34s}\n.arx-card.on .arx-feat li:nth-child(4){transition-delay:.42s}\n.arx-card.on .arx-feat li:nth-child(5){transition-delay:.50s}\n.arx-feat i{flex:none;width:15px;height:15px;margin-top:3px;border-radius:4px;\n  background:linear-gradient(135deg,#b9a7ff,#8fb0ff);position:relative}\n.arx-feat i::after{content:\"\";position:absolute;left:4.5px;top:2px;width:4px;height:8px;\n  border:solid #1a1040;border-width:0 1.6px 1.6px 0;transform:rotate(45deg)}\n.arx-ctas{display:flex;gap:8px;flex-wrap:wrap;margin-top:16px}\n.arx-ctas a{display:inline-flex;align-items:center;gap:6px;border-radius:999px;padding:9px 16px;\n  font:700 .78rem\/1 inherit;transition:.25s;white-space:nowrap}\n.arx-ctas .primary{background:linear-gradient(135deg,#50208F,#7090E0);color:#fff;box-shadow:0 8px 20px rgba(80,32,143,.42)}\n.arx-ctas .primary:hover{transform:translateY(-2px);box-shadow:0 12px 26px rgba(80,32,143,.55)}\n.arx-ctas .ghost{background:rgba(255,255,255,.09);border:1px solid rgba(185,167,255,.32);color:#e9e4fb}\n.arx-ctas .ghost:hover{background:rgba(255,255,255,.17);color:#fff}\n\n\/* ---------------- accessibility \/ reduced motion ---------------- *\/\n@media (prefers-reduced-motion: reduce){\n  .hot .ring::after{animation:none}\n  .arxIntroTxt .ln,.arx-card,.arx-feat li{transition-duration:.01ms!important}\n}\n@media(max-width:760px){\n  .hot .ring{width:26px;height:26px}\n  .hot .lbl{display:none}\n  .hot .lnk{display:none}\n  .arxSkip{right:14px;bottom:88px;padding:8px 14px}\n  [dir=\"rtl\"] .arxSkip{left:14px;right:auto}\n}\n\n\n\/* ================================================================\n   AR\/3D CLASSROOM \u2014 PHASE 4\n   Guided tour \u00b7 TRANSFORM set-piece \u00b7 AR overlays \u00b7 floor map \u00b7 tools\n   ================================================================ *\/\n\n\/* ---------- control-bar tools ---------- *\/\n.arx-sep{width:1px;height:26px;background:rgba(185,167,255,.28);margin:0 2px;flex:none}\n.arx-bar .tool{display:inline-flex;align-items:center;gap:6px;background:rgba(255,255,255,.06)!important;\n  border:1px solid rgba(185,167,255,.22)!important;color:#e7e1fb;border-radius:999px;padding:9px 13px;\n  font:600 .76rem\/1 inherit;font-family:inherit;cursor:pointer;transition:.22s;box-shadow:none!important}\n.arx-bar .tool svg{width:15px;height:15px;fill:currentColor;flex:none}\n#arxBtnAR[hidden]{display:none!important}\n.arx-bar .tool.ar{background:linear-gradient(135deg,rgba(80,32,143,.65),rgba(112,144,224,.65))!important;\n  border-color:rgba(201,188,255,.45)!important;color:#fff}\n.arx-bar .tool.ar:hover{background:linear-gradient(135deg,#50208F,#7090E0)!important}\n.arx-ar-note{position:absolute;z-index:13;left:50%;bottom:calc(var(--arx-barh,68px) + 26px);transform:translateX(-50%);\n  background:rgba(15,10,31,.82);border:1px solid rgba(185,167,255,.34);border-radius:999px;color:#fff;\n  padding:10px 18px;font:600 .8rem\/1.4 inherit;backdrop-filter:blur(10px);max-width:min(420px,92%);text-align:center}\n.arx-ar-note[hidden]{display:none!important}\n#arxBtnSound svg.on{display:none}\n#arxBtnSound[aria-pressed=\"true\"] svg.on{display:block}\n#arxBtnSound[aria-pressed=\"true\"] svg.off{display:none}\n.arx-bar .tool:hover{background:rgba(112,144,224,.36)!important;color:#fff}\n.arx-bar .tool[aria-pressed=\"true\"]{background:linear-gradient(135deg,#50208F,#7090E0)!important;border-color:transparent!important;color:#fff}\n.arx-bar .tool:focus-visible{outline:2px solid #8fb0ff;outline-offset:2px}\n.arx-bar{flex-wrap:nowrap;max-width:calc(100% - 24px)}\n\n\/* ---------- AR-style overlay ---------- *\/\n.arxAR{position:absolute;z-index:6;left:0;top:0;pointer-events:none;opacity:0;\n  transition:opacity .45s var(--e-out,cubic-bezier(.16,1,.3,1))}\n.arxAR.on{opacity:1}\n.arxARin{transform:translate(-50%,-100%);display:flex;flex-direction:column;align-items:center;gap:7px}\n.arxAR b{font:800 .74rem\/1 inherit;letter-spacing:.26em;text-transform:uppercase;color:#fff;white-space:nowrap;\n  background:linear-gradient(120deg,rgba(80,32,143,.86),rgba(112,144,224,.86));border:1px solid rgba(201,188,255,.55);\n  border-radius:999px;padding:8px 15px;backdrop-filter:blur(8px);box-shadow:0 10px 26px rgba(10,5,26,.5)}\n.arxAR b.flip{animation:arxARflip .5s var(--e-out,cubic-bezier(.16,1,.3,1))}\n@keyframes arxARflip{0%{opacity:0;transform:translateY(7px) scale(.96)}100%{opacity:1;transform:none}}\n.arxARbar{width:1px;height:26px;background:linear-gradient(180deg,rgba(201,188,255,.9),transparent);order:2}\n.arxAR.down .arxARin{transform:translate(-50%,0)}\n.arxAR.down .arxARbar{order:-1;background:linear-gradient(0deg,rgba(201,188,255,.9),transparent)}\n\n\/* ---------- floor map ---------- *\/\n.arxMap{position:absolute;z-index:9;left:16px;top:70px;width:212px;padding:11px 11px 8px;\n  background:rgba(15,10,31,.78);border:1px solid rgba(185,167,255,.3);border-radius:16px;\n  backdrop-filter:blur(12px);box-shadow:0 20px 46px rgba(8,4,20,.55);\n  opacity:0;transform:translateY(-8px) scale(.97);transition:opacity .3s,transform .3s var(--e-out,cubic-bezier(.16,1,.3,1))}\n.arxMap.on{opacity:1;transform:none}\n.arxMap[hidden]{display:none!important}\n[dir=\"rtl\"] .arxMap{left:auto;right:16px}\n.arxMapTop{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px}\n.arxMapTop span{font:700 .68rem\/1 inherit;letter-spacing:.14em;text-transform:uppercase;color:#c9bdf2}\n.arxMapTop button{background:none!important;border:0!important;color:#c9bdf2;font-size:1.15rem;line-height:1;\n  cursor:pointer;padding:0 2px;box-shadow:none!important;font-family:inherit}\n.arxMapTop button:hover{color:#fff}\n.arxMapPlan{position:relative;width:100%;aspect-ratio:16\/11;border-radius:10px;\n  background:linear-gradient(160deg,rgba(80,32,143,.3),rgba(54,57,150,.24));\n  border:1px solid rgba(185,167,255,.3);overflow:hidden}\n.arxMapBoard{position:absolute;left:32%;top:3px;width:36%;height:5px;border-radius:3px;background:rgba(201,188,255,.75)}\n.arxMapPlan .pt{position:absolute;width:15px;height:15px;margin:-7.5px 0 0 -7.5px;border-radius:50%;border:0;padding:0;\n  background:rgba(201,188,255,.5)!important;cursor:pointer;transition:.22s;box-shadow:none!important}\n.arxMapPlan .pt::after{content:attr(data-n);position:absolute;inset:0;display:grid;place-items:center;\n  font:800 .5rem\/1 inherit;color:#1a1040}\n.arxMapPlan .pt:hover{background:#fff!important;transform:scale(1.35)}\n.arxMapPlan .pt.on{background:linear-gradient(135deg,#b9a7ff,#8fb0ff)!important;transform:scale(1.4);\n  box-shadow:0 0 0 4px rgba(143,176,255,.28)!important}\n.arxMapPlan .pt:focus-visible{outline:2px solid #fff;outline-offset:1px}\n.arxMapMe{position:absolute;width:9px;height:9px;margin:-4.5px 0 0 -4.5px;border-radius:50%;\n  background:#39e29a;box-shadow:0 0 0 3px rgba(57,226,154,.28),0 0 12px #39e29a;transition:left .5s ease,top .5s ease;pointer-events:none}\n.arxMapTip{margin:7px 0 0;font-size:.62rem;letter-spacing:.06em;color:rgba(255,255,255,.5);text-align:center}\n\n\/* ---------- guided tour ribbon ---------- *\/\n.arxGuide{position:absolute;z-index:11;left:50%;bottom:92px;transform:translateX(-50%) translateY(14px);\n  width:min(640px,calc(100% - 32px));background:linear-gradient(165deg,rgba(26,16,52,.94),rgba(20,13,42,.9));\n  border:1px solid rgba(185,167,255,.34);border-radius:18px;overflow:hidden;color:#fff;\n  backdrop-filter:blur(14px);box-shadow:0 26px 60px rgba(8,4,20,.6);\n  opacity:0;transition:opacity .4s,transform .5s var(--e-out,cubic-bezier(.16,1,.3,1))}\n.arxGuide.on{opacity:1;transform:translateX(-50%)}\n.arxGuide[hidden]{display:none!important}\n.arxGuideBar{height:3px;background:rgba(255,255,255,.1)}\n.arxGuideBar i{display:block;height:100%;width:0;background:linear-gradient(90deg,#b9a7ff,#8fb0ff);\n  transition:width .55s var(--e-out,cubic-bezier(.16,1,.3,1))}\n.arxGuideIn{padding:15px 18px 16px}\n.arxGStep{font:800 .66rem\/1 inherit;letter-spacing:.24em;color:#8fa7ee}\n.arxGuide p{margin:8px 0 13px;font-size:.92rem;line-height:1.6;color:rgba(255,255,255,.9)}\n.arxGBtns{display:flex;gap:8px;flex-wrap:wrap;align-items:center}\n.arxGBtns button{border-radius:999px;padding:9px 16px;font:700 .78rem\/1 inherit;font-family:inherit;cursor:pointer;\n  background:rgba(255,255,255,.09)!important;border:1px solid rgba(185,167,255,.3)!important;color:#e9e4fb;\n  transition:.22s;box-shadow:none!important}\n.arxGBtns button:hover:not(:disabled){background:rgba(112,144,224,.4)!important;color:#fff}\n.arxGBtns button:disabled{opacity:.35;cursor:not-allowed}\n.arxGBtns .p{background:linear-gradient(135deg,#50208F,#7090E0)!important;border-color:transparent!important;color:#fff;\n  box-shadow:0 8px 20px rgba(80,32,143,.45)!important}\n.arxGBtns .x{margin-inline-start:auto;background:none!important;border-color:rgba(255,255,255,.16)!important;color:rgba(255,255,255,.62)}\n.arx-stage.arx-guiding .arx-card,.arx-stage.arx-mapping .arx-card{opacity:0!important;pointer-events:none!important}\n.arx-stage.arx-guiding .arx-hint{opacity:0}\n\n\/* ---------- TRANSFORM captions ---------- *\/\n.arxTrans{position:absolute;z-index:12;inset:0;display:grid;place-items:center;text-align:center;\n  pointer-events:none;padding:24px}\n.arxTrans[hidden]{display:none!important}\n.arxTrans b{display:block;font:800 clamp(1.3rem,3.6vw,2.5rem)\/1.15 inherit;letter-spacing:.02em;color:#fff;\n  text-shadow:0 6px 30px rgba(8,4,22,.8);opacity:0;transform:translateY(12px);filter:blur(5px)}\n.arxTrans b.on{opacity:1;transform:none;filter:blur(0);\n  transition:opacity .55s var(--e-out,cubic-bezier(.16,1,.3,1)),transform .7s var(--e-out,cubic-bezier(.16,1,.3,1)),filter .55s}\n.arxTransSub{display:block;margin-top:10px;font:600 .74rem\/1 inherit;letter-spacing:.3em;text-transform:uppercase;\n  color:rgba(201,188,255,.8);opacity:0;transition:opacity .5s .15s}\n.arxTransSub.on{opacity:1}\n.arx-stage.arx-cine .arx-bar,.arx-stage.arx-cine .arx-dots,.arx-stage.arx-cine .arxHot,\n.arx-stage.arx-cine .arx-card,.arx-stage.arx-cine .arx-hint,.arx-stage.arx-cine .arxAR{\n  opacity:0!important;pointer-events:none!important;transition:opacity .45s ease}\n\n\/* ---------- responsive ---------- *\/\n@media(max-width:1100px){\n  .arx-bar .tool em{display:none}\n  .arx-bar .tool{padding:9px 11px}\n}\n@media(max-width:760px){\n  .arx-bar{max-width:calc(100% - 16px);overflow-x:auto;scrollbar-width:none;justify-content:flex-start}\n  .arx-bar::-webkit-scrollbar{display:none}\n  .arx-bar .tool{padding:8px 10px}\n  .arx-sep{height:22px}\n  .arxMap{right:10px;left:10px;top:56px;width:auto}\n  [dir=\"rtl\"] .arxMap{left:10px;right:10px}\n  .arxGuide{bottom:78px;width:calc(100% - 20px)}\n  .arxGuideIn{padding:13px 15px 14px}\n  .arxGuide p{font-size:.85rem;margin:7px 0 11px}\n  .arxGBtns button{padding:8px 13px;font-size:.74rem}\n  .arxAR b{font-size:.62rem;letter-spacing:.18em;padding:6px 11px}\n}\n\n\n\n\/* ---------- mobile: the card is a sheet, not a curtain ----------\n   On a phone the full product card covered most of the 3D view, so it now\n   shows only the step and the product name until the visitor asks for more. *\/\n.arx-card-more{display:none}\n@media(max-width:760px){\n  .arx-card-more{display:grid;place-items:center;flex:none;margin-inline-start:auto;\n    width:30px;height:30px;border-radius:50%;cursor:pointer;\n    background:rgba(255,255,255,.1)!important;border:1px solid rgba(185,167,255,.32)!important;\n    color:#fff;box-shadow:none!important;padding:0;transition:background .22s}\n  .arx-card-more svg{width:14px;height:14px;fill:currentColor;\n    transform:rotate(180deg);            \/* collapsed: points up \u2014 \"open me\" *\/\n    transition:transform .3s var(--e-out,cubic-bezier(.16,1,.3,1))}\n  .arx-card-more:hover{background:rgba(112,144,224,.4)!important}\n  .arx-card.open .arx-card-more svg{transform:rotate(0deg)}   \/* open: points down \u2014 \"close me\" *\/\n\n  \/* collapsed: header + title only, so the classroom stays visible *\/\n  .arx-card{max-height:none;overflow:visible}\n  .arx-card:not(.open) #arxBody,\n  .arx-card:not(.open) .arx-feat,\n  .arx-card:not(.open) .arx-shots,\n  .arx-card:not(.open) .arx-ctas,\n  .arx-card:not(.open) .arx-more{display:none!important}\n  .arx-card:not(.open) h2{margin-bottom:0}\n  .arx-card.open{max-height:min(48svh,340px);overflow:auto;-webkit-overflow-scrolling:touch}\n  .arx-card h2{font-size:1rem;line-height:1.3}\n  .arx-card-top{margin-bottom:6px}\n  .arx-shots a{width:52px;height:38px}\n  .arx-ctas a{padding:8px 13px;font-size:.74rem}\n}\n\n\/* ---------- RTL: horizontal arrows point the other way ----------\n   Arabic reads right-to-left, so \"next\" is to the left and \"previous\"\n   is to the right. The glyphs are mirrored rather than swapped so there\n   is only one place to maintain. *\/\n[dir=\"rtl\"] .arx-bar .nav span,\n[dir=\"rtl\"] .arx-exit span,\n[dir=\"rtl\"] .arx .arx-more .arr,\n[dir=\"rtl\"] .arx-ctas .arr{display:inline-block;transform:scaleX(-1)}\n\n\n\n\/* inspect: the room is gone, so the reading furniture goes with it *\/\n.arx-stage.arx-inspect .arxHot,\n.arx-stage.arx-inspect .arxAR{opacity:0!important;pointer-events:none!important}\n.arx-stage.arx-inspect .arx-hint{opacity:0}\n.arx-stage.arx-inspect .arx-dots{opacity:.35}\n.arx-ctas .inspect{cursor:pointer;font-family:inherit}\n.arx-stage.arx-inspect .arx-ctas .inspect{background:linear-gradient(135deg,#50208F,#7090E0)!important;\n  border-color:transparent!important;color:#fff}\n\n\/* plan view: the room is the subject, so the reading furniture steps back *\/\n.arx-stage.arx-plan .arx-card,\n.arx-stage.arx-plan .arxAR,\n.arx-stage.arx-plan .arxHot{opacity:0!important;pointer-events:none!important}\n.arx-stage.arx-plan .arx-hint{opacity:0}\n\n\/* ---------- accessibility ---------- *\/\n.arx-sr{position:absolute!important;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;\n  clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap;border:0}\n.arx-skip{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);clip-path:inset(50%);\n  white-space:nowrap;z-index:60;left:50%;top:12px;padding:0;border:0}\n.arx-skip:focus{width:auto;height:auto;overflow:visible;clip:auto;clip-path:none;\n  transform:translateX(-50%);background:#fff;color:#2d1b69;border-radius:999px;padding:11px 22px;\n  font:700 .82rem\/1 inherit;box-shadow:0 10px 30px rgba(8,4,20,.45);outline:2px solid #7090E0;outline-offset:2px}\n\/* one consistent focus ring on every control in the experience *\/\n.arx-stage button:focus-visible,.arx-stage a:focus-visible{\n  outline:2px solid #a9c2ff;outline-offset:3px;border-radius:999px}\n\n\/* ---------- micro-interactions ---------- *\/\n.arx-bar .nav:active,.arx-bar .tool:active,.arx-dots button:active,.arxGBtns button:active{transform:scale(.96)}\n.arx-bar .play:active{transform:scale(.94)}\n.arx-bar .nav,.arx-bar .tool,.arx-dots button,.arxGBtns button,.arx-bar .play{\n  transition:background .22s,color .22s,transform .12s cubic-bezier(.3,1.4,.5,1),box-shadow .22s}\n.arx-dots button:hover{transform:translateX(-3px)}\n[dir=\"rtl\"] .arx-dots button:hover{transform:translateX(3px)}\n.arx-dots button.on:hover{transform:none}\n.arx-ctas a:active{transform:translateY(0) scale(.97)}\n.arx-shots a:focus-visible{outline:2px solid #a9c2ff;outline-offset:2px;border-radius:9px}\n@media (prefers-reduced-motion: reduce){\n  .arx-bar .nav:active,.arx-bar .tool:active,.arx-dots button:active,\n  .arxGBtns button:active,.arx-bar .play:active,.arx-dots button:hover{transform:none}\n}\n@media (hover:none){\n  .arx-dots button:hover{transform:none}\n}\n\n\n\/* ---------- scroll storytelling ---------- *\/\n.arx-story{background:linear-gradient(180deg,#12102e 0%,#1a1040 100%);color:#fff;padding:clamp(64px,9vw,120px) 0}\n.arx-story-kicker{font:800 .72rem\/1 inherit;letter-spacing:.24em;text-transform:uppercase;color:#8fa7ee;margin:0 0 14px}\n.arx-story-lead{color:#fff;font-size:clamp(1.7rem,4vw,2.8rem);line-height:1.15;margin:0 0 14px;max-width:20ch}\n.arx-story-sub{color:rgba(255,255,255,.72);font-size:1.02rem;margin:0 0 clamp(34px,5vw,58px);max-width:56ch}\n.arx-steps{list-style:none;margin:0;padding:0;display:grid;gap:clamp(16px,2vw,22px);\n  \/* five steps: one row on desktop, then 3 \/ 2 \/ 1 as it narrows *\/\n  grid-template-columns:repeat(auto-fit,minmax(min(100%,196px),1fr))}\n@media(min-width:1180px){.arx-steps{grid-template-columns:repeat(5,1fr)}}\n.arx-steps li{position:relative;background:linear-gradient(165deg,rgba(255,255,255,.07),rgba(255,255,255,.03));\n  border:1px solid rgba(185,167,255,.22);border-radius:18px;padding:24px 22px 22px;min-width:0;\n  opacity:0;transform:translateY(26px);\n  transition:opacity .7s var(--e-out,cubic-bezier(.16,1,.3,1)),transform .8s var(--e-out,cubic-bezier(.16,1,.3,1)),border-color .3s}\n.arx-steps li.in{opacity:1;transform:none}\n.arx-steps li:nth-child(2){transition-delay:.08s}\n.arx-steps li:nth-child(3){transition-delay:.16s}\n.arx-steps li:nth-child(4){transition-delay:.24s}\n.arx-steps li:nth-child(5){transition-delay:.32s}\n.arx-steps li:hover{border-color:rgba(185,167,255,.5)}\n.arx-steps .n{display:inline-block;font:800 .78rem\/1 inherit;letter-spacing:.18em;\n  background:linear-gradient(120deg,#b9a7ff,#8fb0ff);-webkit-background-clip:text;background-clip:text;color:transparent;margin-bottom:12px}\n.arx-steps h3{color:#fff;font-size:1.05rem;line-height:1.3;margin:0 0 8px}\n.arx-steps p{color:rgba(255,255,255,.74);font-size:.9rem;line-height:1.62;margin:0}\n@media (prefers-reduced-motion: reduce){\n  .arx-steps li{opacity:1;transform:none;transition:none}\n}\n\n\/* ---------- depth: a soft vignette so the eye lands in the middle ---------- *\/\n.arx-vig{position:absolute;inset:0;z-index:4;pointer-events:none;display:block;\n  background:radial-gradient(115% 95% at 50% 42%,transparent 52%,rgba(10,5,26,.30) 84%,rgba(8,4,20,.52) 100%)}\n\n\/* ---------- control bar: fixed, predictable control heights ---------- *\/\n.arx-bar{align-items:center}\n.arx-bar .nav,.arx-bar .tool{height:42px;padding-top:0;padding-bottom:0;line-height:1}\n.arx-bar .play{width:50px;height:50px;flex:none}\n.arx-bar .arx-count{line-height:1}\n@media(max-width:760px){\n  .arx-bar .nav,.arx-bar .tool{height:38px}\n  .arx-bar .play{width:44px;height:44px}\n}\n\/* the station pills and the product card sit clear of the bar, whatever its\n   height ends up being on the current breakpoint (--arx-barh is measured in JS) *\/\n.arx-hint{bottom:calc(var(--arx-barh,68px) + 24px)}\n.arx-card{bottom:calc(var(--arx-barh,68px) + 38px)}\n.arxGuide{bottom:calc(var(--arx-barh,68px) + 26px)}\n@media(max-width:760px){\n  .arx-dots{bottom:calc(var(--arx-barh,56px) + 24px)}\n  .arx-card{bottom:calc(var(--arx-barh,56px) + 74px)}\n  .arxGuide{bottom:calc(var(--arx-barh,56px) + 22px)}\n}\n@media (prefers-reduced-motion: reduce){\n  .arxAR b.flip{animation:none}\n  .arxTrans b,.arxGuide,.arxMap{transition-duration:.01ms!important}\n}\n\t<\/style>\n\n\t<script src=\"https:\/\/digitalmqr.com\/wp-content\/themes\/mqr-theme\/assets\/js\/three.min.js?ver=r128a\"><\/script>\n\t<script src=\"https:\/\/digitalmqr.com\/wp-content\/themes\/mqr-theme\/assets\/js\/GLTFLoader.js?ver=r128a\"><\/script>\n\t<script src=\"https:\/\/digitalmqr.com\/wp-content\/themes\/mqr-theme\/assets\/js\/postprocessing.js?ver=3.13.0\"><\/script>\n\t<script>\n\t\/* Scroll storytelling: reveal each step as it enters the viewport.\n\t   Deliberately independent of the 3D bootstrap so the narrative still\n\t   works without WebGL, and it does not rely on IntersectionObserver\n\t   alone \u2014 a rect check on scroll plus two safety passes guarantee the\n\t   copy is never left invisible if observer callbacks are delayed. *\/\n\t(function(){\n\t\tvar items=[], done=false;\n\t\tfunction reveal(){\n\t\t\tif(done) return;\n\t\t\tvar vh=window.innerHeight||document.documentElement.clientHeight, left=0;\n\t\t\tfor(var i=0;i<items.length;i++){\n\t\t\t\tvar li=items[i];\n\t\t\t\tif(li.classList.contains('in')) continue;\n\t\t\t\tvar r=li.getBoundingClientRect();\n\t\t\t\tif(r.top < vh*0.92 && r.bottom > 0) li.classList.add('in'); else left++;\n\t\t\t}\n\t\t\tif(!left) done=true;\n\t\t}\n\t\tfunction start(){\n\t\t\titems=[].slice.call(document.querySelectorAll('.arx-steps li'));\n\t\t\tif(!items.length) return;\n\t\t\tif(window.IntersectionObserver){\n\t\t\t\tvar io=new IntersectionObserver(function(es){\n\t\t\t\t\tes.forEach(function(e){ if(e.isIntersecting){ e.target.classList.add('in'); io.unobserve(e.target); } });\n\t\t\t\t},{ threshold:.16, rootMargin:'0px 0px -8% 0px' });\n\t\t\t\titems.forEach(function(li){ io.observe(li); });\n\t\t\t}\n\t\t\tvar queued=false;\n\t\t\tfunction onScroll(){\n\t\t\t\tif(queued) return; queued=true;\n\t\t\t\t(window.requestAnimationFrame||setTimeout)(function(){ queued=false; reveal(); }, 16);\n\t\t\t}\n\t\t\twindow.addEventListener('scroll', onScroll, {passive:true});\n\t\t\twindow.addEventListener('resize', onScroll);\n\t\t\treveal();\n\t\t\tsetTimeout(reveal, 900);\n\t\t\tsetTimeout(reveal, 3000);\n\t\t\twindow.MQR_STORY_REVEAL = reveal;   \/* testable hook *\/\n\t\t}\n\t\tif(document.readyState!=='loading') start();\n\t\telse document.addEventListener('DOMContentLoaded', start);\n\t})();\n\t(function(){\n\t\tfunction fail(msg){\n\t\t\tvar f=document.getElementById('arxFallback');\n\t\t\tif(f){ f.hidden=false; var p=f.querySelector('p'); if(p&&msg) p.textContent=msg; }\n\t\t}\n\t\tfunction start(){ setTimeout(function(){ window.__arxInit ? window.__arxInit() : fail(); },0); }\n\t\tif(window.THREE){ start(); }\n\t\telse{\n\t\t\tvar s=document.createElement('script');\n\t\t\ts.src='https:\/\/unpkg.com\/three@0.128.0\/build\/three.min.js';\n\t\t\ts.onload=function(){ window.THREE?start():fail('The 3D engine could not start. Try another browser.'); };\n\t\t\ts.onerror=function(){ fail('The 3D engine was blocked (ad-blocker \/ network filter). Disable blockers and reload.'); };\n\t\t\tdocument.head.appendChild(s);\n\t\t}\n\t})();\n\n\twindow.__arxInit=function(){\n\t\tif(window.__arxStarted) return; window.__arxStarted=true;\n\n\t\/* ================================================================\n\t   MQR.Core \u2014 Phase 3\n\t   Modular systems bolted onto the existing engine.\n\t   Each system is independent and degrades safely on its own.\n\t   ================================================================ *\/\n\tvar MQR = window.MQR = window.MQR || {};\n\tvar REDUCED = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;\n\n\t\/* ---------- shared easing library ---------- *\/\n\tMQR.Ease = {\n\t\toutExpo:  function(t){ return t===1?1:1-Math.pow(2,-10*t); },\n\t\tinOutCubic:function(t){ return t<.5 ? 4*t*t*t : 1-Math.pow(-2*t+2,3)\/2; },\n\t\toutCubic: function(t){ return 1-Math.pow(1-t,3); },\n\t\tinOutQuint:function(t){ return t<.5 ? 16*t*t*t*t*t : 1-Math.pow(-2*t+2,5)\/2; }\n\t};\n\n\t\/* ================================================================\n\t   MQR.Intro \u2014 4-scene cinematic opening\n\t   Scene 1 particles \u00b7 2 branding \u00b7 3 room materialises \u00b7 4 camera glide\n\t   ================================================================ *\/\n\tMQR.Intro = (function(){\n\t\tvar el, ptc, ctx, raf, parts=[], running=false, onDone=null, skipped=false;\n\n\t\tfunction buildParticles(){\n\t\t\tif(!ptc) return;\n\t\t\tvar w = ptc.width = ptc.offsetWidth, h = ptc.height = ptc.offsetHeight;\n\t\t\tctx = ptc.getContext('2d');\n\t\t\tparts = [];\n\t\t\tvar n = REDUCED ? 0 : Math.min(90, Math.round(w*h\/16000));\n\t\t\tfor(var i=0;i<n;i++){\n\t\t\t\tparts.push({\n\t\t\t\t\tx:Math.random()*w, y:Math.random()*h,\n\t\t\t\t\tr:Math.random()*1.7+.4,\n\t\t\t\t\tvx:(Math.random()-.5)*.16, vy:-(Math.random()*.3+.06),\n\t\t\t\t\ta:Math.random()*.5+.15, ph:Math.random()*6.28\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t\tfunction drawParticles(t){\n\t\t\tif(!ctx) return;\n\t\t\tvar w=ptc.width, h=ptc.height;\n\t\t\tctx.clearRect(0,0,w,h);\n\t\t\tfor(var i=0;i<parts.length;i++){\n\t\t\t\tvar p=parts[i];\n\t\t\t\tp.x+=p.vx; p.y+=p.vy;\n\t\t\t\tif(p.y< -10){ p.y=h+10; p.x=Math.random()*w; }\n\t\t\t\tif(p.x< -10) p.x=w+10; if(p.x>w+10) p.x=-10;\n\t\t\t\tvar tw = .55 + Math.sin(t*.0015 + p.ph)*.45;\n\t\t\t\tctx.beginPath();\n\t\t\t\tctx.arc(p.x,p.y,p.r,0,6.283);\n\t\t\t\tctx.fillStyle='rgba(185,167,255,'+(p.a*tw).toFixed(3)+')';\n\t\t\t\tctx.fill();\n\t\t\t}\n\t\t\traf=requestAnimationFrame(drawParticles);\n\t\t}\n\n\t\tfunction show(sel){ var n=el.querySelector(sel); if(n){ n.classList.add('on'); n.classList.remove('off'); } }\n\t\tfunction hide(sel){ var n=el.querySelector(sel); if(n){ n.classList.remove('on'); n.classList.add('off'); } }\n\n\t\tfunction finish(){\n\t\t\tif(skipped) return; skipped=true;\n\t\t\tif(raf) cancelAnimationFrame(raf);\n\t\t\tel.classList.add('done');\n\t\t\tsetTimeout(function(){ if(el && el.parentNode) el.style.display='none'; }, 1100);\n\t\t\tif(onDone) onDone();\n\t\t}\n\n\t\treturn {\n\t\t\t\/* timeline: returns immediately, calls done() when the room should take over *\/\n\t\t\tplay:function(root, done){\n\t\t\t\tel = root.querySelector('.arxIntroX');\n\t\t\t\tif(!el){ done && done(); return; }\n\t\t\t\tel.classList.add('playing');   \/* make the cinematic layer visible *\/\n\t\t\t\tptc = el.querySelector('canvas.ptc');\n\t\t\t\tonDone = done; running = true;\n\t\t\t\tbuildParticles();\n\t\t\t\twindow.addEventListener('resize', buildParticles);\n\t\t\t\tif(!REDUCED) raf=requestAnimationFrame(drawParticles);\n\n\t\t\t\tvar skip = el.querySelector('.arxSkip');\n\t\t\t\tif(skip) skip.addEventListener('click', finish);\n\n\t\t\t\tif(REDUCED){ finish(); return; }\n\n\t\t\t\t\/* Scene 1 \u2192 2 : light, then brand *\/\n\t\t\t\tsetTimeout(function(){ show('.s1'); show('.rule'); }, 260);\n\t\t\t\tsetTimeout(function(){ hide('.s1'); }, 2100);\n\t\t\t\tsetTimeout(function(){ show('.s2'); }, 2350);\n\t\t\t\t\/* Scene 3 : hand control to the room build-up while text lingers *\/\n\t\t\t\tsetTimeout(function(){ if(onDone) onDone(); onDone=null; }, 2900);\n\t\t\t\t\/* Scene 4 : clear the overlay, camera glide continues underneath *\/\n\t\t\t\tsetTimeout(function(){ hide('.s2'); }, 4300);\n\t\t\t\tsetTimeout(finish, 4900);\n\t\t\t},\n\t\t\tskip: finish\n\t\t};\n\t})();\n\n\t\/* ================================================================\n\t   MQR.Hotspots \u2014 DOM markers projected from 3D anchors.\n\t   Replaces sprite billboards: real glow, connector line, label,\n\t   keyboard focus and hover states become possible.\n\t   ================================================================ *\/\n\tMQR.Hotspots = (function(){\n\t\tvar layer, cam, items=[], vec, onPick, active=-1, visible=true;\n\n\t\tfunction project(pos){\n\t\t\tvec.copy(pos).project(cam);\n\t\t\treturn { x:(vec.x*.5+.5), y:(-vec.y*.5+.5), z:vec.z };\n\t\t}\n\t\treturn {\n\t\t\tinit:function(layerEl, camera, THREE_, defs, pickHandler){\n\t\t\t\tlayer=layerEl; cam=camera; onPick=pickHandler;\n\t\t\t\tvec=new THREE_.Vector3();\n\t\t\t\titems=[];\n\t\t\t\tdefs.forEach(function(d){\n\t\t\t\t\tvar b=document.createElement('button');\n\t\t\t\t\tb.type='button'; b.className='hot';\n\t\t\t\t\tb.setAttribute('aria-label', d.label);\n\t\t\t\t\tb.setAttribute('data-i', d.i);\n\t\t\t\t\tb.innerHTML='<span class=\"ring\"><\/span><span class=\"lnk\"><\/span><span class=\"lbl\">'+d.label+'<\/span>';\n\t\t\t\t\tb.addEventListener('click', function(e){ e.stopPropagation(); onPick(d.i); });\n\t\t\t\t\tlayer.appendChild(b);\n\t\t\t\t\titems.push({ el:b, pos:d.pos, i:d.i });\n\t\t\t\t});\n\t\t\t},\n\t\t\t\/* called every frame \u2014 cheap: one project + transform per marker *\/\n\t\t\tupdate:function(camPos){\n\t\t\t\tif(!visible) return;\n\t\t\t\tvar W=layer.clientWidth, H=layer.clientHeight;\n\t\t\t\tfor(var k=0;k<items.length;k++){\n\t\t\t\t\tvar it=items[k], p=project(it.pos.clone());\n\t\t\t\t\tvar behind = p.z>1;\n\t\t\t\t\tvar dist = camPos.distanceTo(it.pos);\n\t\t\t\t\tvar el=it.el;\n\t\t\t\t\tif(behind || dist>17){ el.classList.add('is-hidden'); continue; }\n\t\t\t\t\tel.classList.remove('is-hidden');\n\t\t\t\t\tel.style.transform='translate('+(p.x*W)+'px,'+(p.y*H)+'px) translate(-50%,-50%)';\n\t\t\t\t\t\/* fade with distance so far markers never clutter *\/\n\t\t\t\t\tel.style.opacity = Math.max(.15, Math.min(1,(16-dist)\/7));\n\t\t\t\t}\n\t\t\t},\n\t\t\tsetActive:function(i){\n\t\t\t\tactive=i;\n\t\t\t\titems.forEach(function(it){\n\t\t\t\t\tit.el.classList.toggle('is-active', it.i===i);\n\t\t\t\t\tit.el.classList.toggle('is-dim', i>=0 && it.i!==i);\n\t\t\t\t});\n\t\t\t},\n\t\t\tshow:function(v){ visible=v; layer.style.display = v?'':'none'; }\n\t\t};\n\t})();\n\n\t\/* ================================================================\n\t   MQR.Focus \u2014 makes the selected product visually dominant.\n\t   Dims the room, lifts the target's emissive, adds a ground ring.\n\t   ================================================================ *\/\n\tMQR.Focus = (function(){\n\t\tvar scene, ambient, targets={}, current=null, orig=new Map(), ring=null, ambBase=.34;\n\tvar rig=[], rigBase=[];      \/* every room light, and the level it was built at *\/\n\n\t\tfunction eachMat(obj, fn){\n\t\t\tobj.traverse(function(o){\n\t\t\t\tif(!o.isMesh || !o.material) return;\n\t\t\t\t(Array.isArray(o.material)?o.material:[o.material]).forEach(fn);\n\t\t\t});\n\t\t}\n\t\treturn {\n\t\t\tinit:function(sc, amb, map, ringMesh, lights){\n\t\t\t\tscene=sc; ambient=amb; targets=map; ring=ringMesh;\n\t\t\t\tambBase = amb ? amb.intensity : 0;   \/* whatever the rig was built with *\/\n\t\t\t\trig = lights || (amb ? [amb] : []);\n\t\t\t\trigBase = rig.map(function(l){ return l.intensity; });\n\t\t\t},\n\t\t\t\/* key: store originals once so we can always restore cleanly *\/\n\t\t\tset:function(key){\n\t\t\t\tif(current===key) return;\n\t\t\t\tthis.clear();\n\t\t\t\tvar g = targets[key];\n\t\t\t\tif(!g) { current=null; return; }\n\t\t\t\tcurrent=key;\n\t\t\t\teachMat(g, function(m){\n\t\t\t\t\tif(!orig.has(m)) orig.set(m,{e:m.emissive?m.emissive.getHex():null, ei:m.emissiveIntensity});\n\t\t\t\t\tif(m.emissive){ m.emissive.setHex(0x6a4fd8); m.emissiveIntensity=Math.max(m.emissiveIntensity||0,.34); }\n\t\t\t\t});\n\t\t\t\t\/* pull the whole room down so the follow-spot on the product reads *\/\n\t\t\t\trig.forEach(function(l,i){ l.intensity = rigBase[i]*.45; });\n\t\t\t\tif(ring){\n\t\t\t\t\tvar b=new THREE.Box3().setFromObject(g), c=b.getCenter(new THREE.Vector3());\n\t\t\t\t\tring.position.set(c.x, .06, c.z);\n\t\t\t\t\tring.material.opacity=.85; ring.visible=true;\n\t\t\t\t}\n\t\t\t},\n\t\t\tclear:function(){\n\t\t\t\torig.forEach(function(v,m){\n\t\t\t\t\tif(m.emissive && v.e!==null){ m.emissive.setHex(v.e); m.emissiveIntensity=v.ei; }\n\t\t\t\t});\n\t\t\t\torig.clear();\n\t\t\t\trig.forEach(function(l,i){ l.intensity = rigBase[i]; });\n\t\t\t\tif(ring){ ring.material.opacity=0; ring.visible=false; }\n\t\t\t\tcurrent=null;\n\t\t\t}\n\t\t};\n\t})();\n\n\t\/* ================================================================\n\t   MQR.Models \u2014 optional GLB upgrade path.\n\t   Drop a file at assets\/models\/<slug>.glb and it replaces the\n\t   procedural stand-in automatically. Missing files are ignored.\n\t   ================================================================ *\/\n\tMQR.Models = (function(){\n\t\tvar loader=null, base='';\n\t\treturn {\n\t\t\tinit:function(baseUrl){\n\t\t\t\tbase=baseUrl;\n\t\t\t\tif(typeof THREE!=='undefined' && THREE.GLTFLoader){ loader=new THREE.GLTFLoader(); }\n\t\t\t\treturn !!loader;\n\t\t\t},\n\t\t\t\/* swap(group, 'interactive-smart-panels') \u2014 keeps position\/scale of the stand-in *\/\n\t\t\tswap:function(group, spec, scene){\n\t\t\t\tif(!loader || !group || !spec) return;\n\t\t\t\tvar file = spec.f || spec;\n\t\t\t\tloader.load(base+file+'.glb', function(gltf){\n\t\t\t\t\tvar m=gltf.scene;\n\t\t\t\t\tgroup.updateMatrixWorld(true);\n\t\t\t\t\tvar stand=new THREE.Box3().setFromObject(group);\n\t\t\t\t\tvar mb=new THREE.Box3().setFromObject(m);\n\t\t\t\t\tvar ms=mb.getSize(new THREE.Vector3());\n\t\t\t\t\t\/* scale from the stated real width, not from the stand-in *\/\n\t\t\t\t\tvar want=spec.w || Math.max(stand.getSize(new THREE.Vector3()).x, .5);\n\t\t\t\t\tvar s=(ms.x>1e-4)? want\/ms.x : 1;\n\t\t\t\t\tif(!(isFinite(s) && s>0)) s=1;\n\t\t\t\t\tm.scale.setScalar(s);\n\n\t\t\t\t\t\/* place it: centred horizontally on the stand-in, and either\n\t\t\t\t\t   sitting on its base or holding its height *\/\n\t\t\t\t\tvar sc_=stand.getCenter(new THREE.Vector3());\n\t\t\t\t\tvar mc=mb.getCenter(new THREE.Vector3()).multiplyScalar(s);\n\t\t\t\t\tvar target=sc_.clone();\n\t\t\t\t\tif(spec.anchor==='floor'){\n\t\t\t\t\t\ttarget.y = stand.min.y + (mb.getSize(new THREE.Vector3()).y*s)\/2;\n\t\t\t\t\t}\n\t\t\t\t\tgroup.worldToLocal(target);\n\t\t\t\t\tm.position.set(target.x-mc.x, target.y-mc.y, target.z-mc.z);\n\n\t\t\t\t\tm.traverse(function(o){ if(o.isMesh){ o.castShadow=true; o.receiveShadow=true; } });\n\t\t\t\t\t\/* the replacement lives inside the group so focus, inspect,\n\t\t\t\t\t   micro-animation, framing and contact shadows all still work *\/\n\t\t\t\t\tgroup.children.slice().forEach(function(ch){ ch.visible=false; });\n\t\t\t\t\tgroup.add(m);\n\t\t\t\t\t\/* the footprint just changed \u2014 resize the pool under it,\n\t\t\t\t\t   otherwise a 0.6m device keeps the 3m stand-in's shadow *\/\n\t\t\t\t\tif(typeof MQR.Models.onSwapped==='function') MQR.Models.onSwapped(group);\n\t\t\t\t}, undefined, function(){ \/* no model supplied \u2014 keep the procedural one *\/ });\n\t\t\t\t\t\t}\n\t\t};\n\t})();\n\n\t\tvar stage=document.getElementById('arxStage'), canvas=document.getElementById('arxCanvas');\n\t\tvar gl=null;\n\t\ttry{ var tc=document.createElement('canvas'); gl=tc.getContext('webgl2')||tc.getContext('webgl')||tc.getContext('experimental-webgl'); }catch(e){}\n\t\tif(!gl){\n\t\t\tvar f=document.getElementById('arxFallback'); f.hidden=false;\n\t\t\tf.querySelector('p').textContent='3D graphics are disabled (hardware acceleration off). Enable it in browser settings.';\n\t\t\treturn;\n\t\t}\n\n\t\tvar V=0x50208F,P=0x7090E0,GLOW=0x9f8cff,\n\t\t\tFLOOR=0xc79356,WALL=0xfdf3e3,WALL_B=0xf6e8d2,WOOD=0xc8843c,WOOD_D=0x8f5a26,\n\t\t\tDARK=0x241a3e,WHITE=0xffffff,MINT=0x59d4a8,CORAL=0xff8f6b,SUN=0xffc94d;\n\n\t\t\/* Budget the renderer to the device. Phones and low-core laptops get a\n\t\t   lower pixel ratio and no shadow pass \u2014 the biggest cost by far \u2014 while\n\t\t   desktops keep the full-quality look. *\/\n\t\tvar COARSE = window.matchMedia && window.matchMedia('(pointer:coarse)').matches;\n\t\t\/* ?lowfx=1 forces the reduced-effects path so it can actually be\n\t\t   verified without a phone in hand *\/\n\t\tvar LOW_END = COARSE || (navigator.hardwareConcurrency||8) <= 4\n\t\t\t|| Math.min(screen.width,screen.height) < 500\n\t\t\t|| \/[?&]lowfx=1\/.test(location.search);\n\t\tvar renderer=new THREE.WebGLRenderer({canvas:canvas,antialias:!LOW_END,alpha:true,powerPreference:'high-performance'});\n\t\trenderer.setPixelRatio(Math.min(window.devicePixelRatio, LOW_END ? 1.5 : 1.8));\n\t\trenderer.shadowMap.enabled=!LOW_END; renderer.shadowMap.type=THREE.PCFSoftShadowMap;\n\t\trenderer.outputEncoding=THREE.sRGBEncoding;\n\t\t\/* Filmic tone curve: rolls off the blown-out whites on the walls and\n\t\t   gives the pastel palette real contrast instead of a flat wash. *\/\n\t\tif(THREE.ACESFilmicToneMapping!==undefined){\n\t\t\trenderer.toneMapping=THREE.ACESFilmicToneMapping;\n\t\t\trenderer.toneMappingExposure=0.92;\n\t\t}\n\t\tvar scene=new THREE.Scene();\n\t\tvar camera=new THREE.PerspectiveCamera(52,1,0.1,140);\n\n\t\t\/* ---- image-based lighting ----------------------------------------\n\t\t   A few emissive planes in a throwaway scene, run through PMREM, give\n\t\t   every PBR surface something to reflect. This is the cheapest single\n\t\t   thing that stops plastic from looking like painted cardboard. *\/\n\t\t(function(){\n\t\t\tif(typeof THREE.PMREMGenerator!=='function') return;\n\t\t\ttry{\n\t\t\t\tvar pm=new THREE.PMREMGenerator(renderer);\n\t\t\t\tvar es=new THREE.Scene();\n\t\t\t\tes.add(new THREE.Mesh(new THREE.BoxGeometry(26,15,26),\n\t\t\t\t\tnew THREE.MeshBasicMaterial({color:0x3a3460,side:THREE.BackSide})));\n\t\t\t\tfunction slab(hex,x,y,z,sx,sy,sz){\n\t\t\t\t\tvar m=new THREE.Mesh(new THREE.BoxGeometry(sx,sy,sz),\n\t\t\t\t\t\tnew THREE.MeshBasicMaterial({color:hex}));\n\t\t\t\t\tm.position.set(x,y,z); es.add(m);\n\t\t\t\t}\n\t\t\t\tslab(0xffffff, 0, 6.8, 0, 17, .3, 17);   \/* ceiling bounce   *\/\n\t\t\t\tslab(0xfff1da,-9.5, 3, 0, .3, 5.5, 15);  \/* warm window side *\/\n\t\t\t\tslab(0xd2ddff, 9.5, 3, 0, .3, 5.5, 15);  \/* cool fill side   *\/\n\t\t\t\tslab(0xe9e3ff, 0, 1.6,-9.5, 15, 4.5, .3);\/* soft back wall   *\/\n\t\t\t\tvar rt=pm.fromScene(es, .03);\n\t\t\t\tscene.environment=rt.texture;\n\t\t\t\tes.traverse(function(o){ if(o.geometry) o.geometry.dispose(); });\n\t\t\t\tpm.dispose();\n\t\t\t}catch(e){ \/* IBL is a bonus, never a requirement *\/ }\n\t\t})();\n\n\t\t\/* ---- light rig -------------------------------------------------\n\t\t   key (warm sun) + cool sky bounce + violet fill + a rim light that\n\t\t   separates the furniture from the back wall. Levels live here only;\n\t\t   the focus and transform systems scale from whatever is set. *\/\n\t\tvar ambL=new THREE.AmbientLight(0xffffff,.26); scene.add(ambL);\n\t\tvar hemiL=new THREE.HemisphereLight(0xdcecff,0x6b5a3a,.38); scene.add(hemiL);\n\t\tvar sun=new THREE.DirectionalLight(0xfff0d8,1.25); sun.position.set(10,15,8);\n\t\tsun.castShadow=true; sun.shadow.mapSize.set(1024,1024);\n\t\tsun.shadow.camera.left=-16;sun.shadow.camera.right=16;sun.shadow.camera.top=16;sun.shadow.camera.bottom=-16;\n\t\tsun.shadow.bias=-0.0009; sun.shadow.normalBias=0.02; sun.shadow.radius=2;\n\t\tscene.add(sun);\n\t\tvar fillL=new THREE.PointLight(0xb9a7ff,.55,30); fillL.position.set(-5,6,5); scene.add(fillL);\n\t\t\/* cool rim from behind-left lifts edges off the wall *\/\n\t\tvar rimL=new THREE.DirectionalLight(0x9fb4ff,.5); rimL.position.set(-9,7,-11); scene.add(rimL);\n\n\t\t\/* Materials sit a little glossier than before so the environment map\n\t\t   below actually shows up; anything that wants matte overrides it. *\/\n\t\tfunction mat(c,o){ o=o||{}; return new THREE.MeshStandardMaterial(\n\t\t\tObject.assign({color:c,roughness:.58,metalness:.08,envMapIntensity:.42},o)); }\n\n\t\t\/* ---- bevelled box ------------------------------------------------\n\t\t   Every edge gets a ~1.5cm chamfer built from an extruded rounded\n\t\t   rectangle. Geometry is cached by dimension, so the 24 identical\n\t\t   chair legs cost one buffer, not 24. *\/\n\t\tvar _boxGeo={};\n\t\tfunction roundedBoxGeo(w,h,d){\n\t\t\tvar key=w.toFixed(3)+'|'+h.toFixed(3)+'|'+d.toFixed(3);\n\t\t\tif(_boxGeo[key]) return _boxGeo[key];\n\t\t\tvar small=Math.min(w,h,d);\n\t\t\tvar b=Math.min(.014, small*.16);\n\t\t\tvar g;\n\t\t\tif(!(b>.0015)){                      \/* too thin to bevel meaningfully *\/\n\t\t\t\tg=new THREE.BoxGeometry(w,h,d);\n\t\t\t}else{\n\t\t\t\tvar sw=w-b*2, sh=h-b*2, sd=d-b*2;\n\t\t\t\tvar r=Math.max(.002, Math.min(.022, Math.min(sw,sh)*.12));\n\t\t\t\tvar sx=sw\/2, sy=sh\/2;\n\t\t\t\tvar s=new THREE.Shape();\n\t\t\t\ts.moveTo(-sx+r,-sy);\n\t\t\t\ts.lineTo(sx-r,-sy);  s.quadraticCurveTo(sx,-sy,sx,-sy+r);\n\t\t\t\ts.lineTo(sx,sy-r);   s.quadraticCurveTo(sx,sy,sx-r,sy);\n\t\t\t\ts.lineTo(-sx+r,sy);  s.quadraticCurveTo(-sx,sy,-sx,sy-r);\n\t\t\t\ts.lineTo(-sx,-sy+r); s.quadraticCurveTo(-sx,-sy,-sx+r,-sy);\n\t\t\t\tg=new THREE.ExtrudeGeometry(s,{ depth:sd, steps:1, curveSegments:3,\n\t\t\t\t\tbevelEnabled:true, bevelThickness:b, bevelSize:b, bevelSegments:2 });\n\t\t\t\tg.center();\n\t\t\t}\n\t\t\t_boxGeo[key]=g;\n\t\t\treturn g;\n\t\t}\n\t\tfunction box(w,h,d,c,o){ var m=new THREE.Mesh(roundedBoxGeo(w,h,d),mat(c,o)); m.castShadow=true; m.receiveShadow=true; return m; }\n\t\tfunction cyl(rt,rb,h,c,o,seg){ var m=new THREE.Mesh(new THREE.CylinderGeometry(rt,rb,h,seg||18),mat(c,o)); m.castShadow=true; m.receiveShadow=true; return m; }\n\t\tfunction sph(r,c,o,w,h){ var m=new THREE.Mesh(new THREE.SphereGeometry(r,w||16,h||12),mat(c,o)); m.castShadow=true; return m; }\n\n\n\t\t\/* ---- material families ---------------------------------------------\n\t\t   Linear-space noise used as roughness variation. Reflectivity is what\n\t\t   separates brushed steel from a fabric seat far more than colour does,\n\t\t   so each family is defined by how it breaks up the environment. *\/\n\t\tfunction texLin(w,h,draw){\n\t\t\tvar cv=document.createElement('canvas'); cv.width=w; cv.height=h;\n\t\t\tdraw(cv.getContext('2d'),w,h);\n\t\t\tvar t=new THREE.CanvasTexture(cv);\n\t\t\tt.wrapS=t.wrapT=THREE.RepeatWrapping;\n\t\t\treturn t;\n\t\t}\n\t\t\/* fine vertical streaks \u2014 brushed metal *\/\n\t\tvar roughBrushed=texLin(128,128,function(g,w,h){\n\t\t\tg.fillStyle='#7a7a7a'; g.fillRect(0,0,w,h);\n\t\t\tfor(var i=0;i<420;i++){\n\t\t\t\tvar x=Math.random()*w, a=Math.random()*.5;\n\t\t\t\tg.strokeStyle='rgba('+(Math.random()<.5?'255,255,255':'0,0,0')+','+a.toFixed(2)+')';\n\t\t\t\tg.lineWidth=Math.random()*1.6+.2;\n\t\t\t\tg.beginPath(); g.moveTo(x,0); g.lineTo(x+(Math.random()-.5)*4,h); g.stroke();\n\t\t\t}\n\t\t});\n\t\troughBrushed.repeat.set(1,3);\n\t\t\/* cross-hatch \u2014 woven fabric *\/\n\t\tvar roughWeave=texLin(128,128,function(g,w,h){\n\t\t\tg.fillStyle='#dcdcdc'; g.fillRect(0,0,w,h);\n\t\t\tfor(var i=0;i<w;i+=3){\n\t\t\t\tg.fillStyle='rgba(0,0,0,.10)'; g.fillRect(i,0,1.4,h);\n\t\t\t\tg.fillStyle='rgba(255,255,255,.10)'; g.fillRect(0,i,w,1.4);\n\t\t\t}\n\t\t\tfor(var s=0;s<900;s++){\n\t\t\t\tg.fillStyle='rgba(0,0,0,'+(Math.random()*.10).toFixed(2)+')';\n\t\t\t\tg.fillRect(Math.random()*w,Math.random()*h,1,1);\n\t\t\t}\n\t\t});\n\t\troughWeave.repeat.set(5,5);\n\t\t\/* long wavering bands \u2014 timber grain *\/\n\t\tvar roughGrain=texLin(256,128,function(g,w,h){\n\t\t\tg.fillStyle='#9a9a9a'; g.fillRect(0,0,w,h);\n\t\t\tfor(var i=0;i<70;i++){\n\t\t\t\tvar y=Math.random()*h;\n\t\t\t\tg.strokeStyle='rgba('+(Math.random()<.5?'255,255,255':'0,0,0')+','+(Math.random()*.28).toFixed(2)+')';\n\t\t\t\tg.lineWidth=Math.random()*2.4+.4;\n\t\t\t\tg.beginPath(); g.moveTo(0,y);\n\t\t\t\tfor(var x=0;x<=w;x+=16) g.lineTo(x, y+Math.sin(x*.05+i)*2.2);\n\t\t\t\tg.stroke();\n\t\t\t}\n\t\t});\n\t\troughGrain.repeat.set(2,2);\n\n\t\tvar MTL={\n\t\t\t\/* satin steel: legs, frames, mounts. Metal with no environment to\n\t\t\t   reflect renders black, so the metalness drops if PMREM failed. *\/\n\t\t\tsteel:function(c,o){ return mat(c||0xb4b1c4, Object.assign(\n\t\t\t\t{roughness:.34,metalness:(scene.environment?.9:.35),\n\t\t\t\t envMapIntensity:1.35,roughnessMap:roughBrushed},o||{})); },\n\t\t\t\/* upholstery: seats, backs, stools *\/\n\t\t\tfabric:function(c,o){ return mat(c, Object.assign(\n\t\t\t\t{roughness:.86,metalness:0,envMapIntensity:.22,roughnessMap:roughWeave},o||{})); },\n\t\t\t\/* timber: teacher desk, benches *\/\n\t\t\twood:function(c,o){ return mat(c, Object.assign(\n\t\t\t\t{roughness:.62,metalness:.02,envMapIntensity:.55,roughnessMap:roughGrain},o||{})); },\n\t\t\t\/* laminate desktop: harder and glossier than timber *\/\n\t\t\tlaminate:function(c,o){ return mat(c, Object.assign(\n\t\t\t\t{roughness:.28,metalness:.03,envMapIntensity:.95},o||{})); },\n\t\t\t\/* moulded plastic housings *\/\n\t\t\tplastic:function(c,o){ return mat(c, Object.assign(\n\t\t\t\t{roughness:.52,metalness:.04,envMapIntensity:.7},o||{})); }\n\t\t};\n\t\t\/* a lacquered box in one of the families above *\/\n\t\tfunction boxM(w,h,d,m){ var x=new THREE.Mesh(roundedBoxGeo(w,h,d),m); x.castShadow=true; x.receiveShadow=true; return x; }\n\t\tfunction cylM(rt,rb,hh,m,seg){ var x=new THREE.Mesh(new THREE.CylinderGeometry(rt,rb,hh,seg||18),m); x.castShadow=true; x.receiveShadow=true; return x; }\n\t\t\/* glass over a display: the emissive image stays, a clearcoat layer\n\t\t   on top gives it the wet sheen a real panel has *\/\n\t\tfunction screenMat(o){\n\t\t\tif(typeof THREE.MeshPhysicalMaterial==='function'){\n\t\t\t\treturn new THREE.MeshPhysicalMaterial(Object.assign(\n\t\t\t\t\t{roughness:.22,metalness:0,clearcoat:1,clearcoatRoughness:.05,envMapIntensity:1.1},o));\n\t\t\t}\n\t\t\treturn new THREE.MeshStandardMaterial(o);\n\t\t}\n\n\n\t\t\/* ---- animated canvas textures --------------------------------------\n\t\t   texAnim() is tex()'s moving cousin. The draw function receives a\n\t\t   time in seconds and returns the number of seconds it wants before\n\t\t   the next redraw, so an idle screen costs one upload per second and\n\t\t   an animating one costs about twelve. *\/\n\t\tvar SCREENS=[];\n\t\tfunction texAnim(w,h,draw){\n\t\t\tvar cv=document.createElement('canvas'); cv.width=w; cv.height=h;\n\t\t\tvar g=cv.getContext('2d');\n\t\t\tvar t=new THREE.CanvasTexture(cv);\n\t\t\tt.encoding=THREE.sRGBEncoding;\n\t\t\tvar rec={ g:g, w:w, h:h, tex:t, draw:draw, next:0 };\n\t\t\tdraw(g,w,h,0);\n\t\t\tSCREENS.push(rec);\n\t\t\treturn t;\n\t\t}\n\t\tfunction tickScreens(now){\n\t\t\tif(!SCREENS.length) return;\n\t\t\tvar t=now\/1000;\n\t\t\tvar budget = LOW_END ? 1 : 2;      \/* redraws allowed per frame *\/\n\t\t\tfor(var i=0;i<SCREENS.length && budget>0;i++){\n\t\t\t\tvar s=SCREENS[i];\n\t\t\t\tif(t < s.next) continue;\n\t\t\t\tvar wait=s.draw(s.g, s.w, s.h, t);\n\t\t\t\ts.tex.needsUpdate=true;\n\t\t\t\ts.next = t + (wait||1) * (LOW_END?2:1);\n\t\t\t\tbudget--;\n\t\t\t}\n\t\t}\n\n\t\t\/* shared chrome so every screen looks like it belongs to one system *\/\n\t\tfunction scrPanelChrome(g,w,h,slide,slides){\n\t\t\tvar bg=g.createLinearGradient(0,0,w,h);\n\t\t\tbg.addColorStop(0,'#241a52'); bg.addColorStop(1,'#3b2a78');\n\t\t\tg.fillStyle=bg; g.fillRect(0,0,w,h);\n\t\t\tg.fillStyle='rgba(255,255,255,.06)'; g.fillRect(0,0,w,64);\n\t\t\tfor(var i=0;i<slides;i++){\n\t\t\t\tg.fillStyle='rgba(255,255,255,'+(i===slide?'.9':'.3')+')';\n\t\t\t\tg.beginPath(); g.arc(44+i*54,32,14,0,7); g.fill();\n\t\t\t}\n\t\t}\n\t\tfunction scrTitle(g,txt,x,y,underline){\n\t\t\tg.fillStyle='#ffffff'; g.font='700 44px sans-serif'; g.fillText(txt,x,y);\n\t\t\tif(underline>0){\n\t\t\t\tg.strokeStyle='#b9a7ff'; g.lineWidth=6;\n\t\t\t\tg.beginPath(); g.moveTo(x,y+30); g.lineTo(x+underline,y+30); g.stroke();\n\t\t\t}\n\t\t}\n\t\tfunction easeOut(p){ return 1-Math.pow(1-Math.max(0,Math.min(1,p)),3); }\n\n\t\tfunction tex(w,h,draw){ var cv=document.createElement('canvas'); cv.width=w; cv.height=h; draw(cv.getContext('2d'),w,h); var t=new THREE.CanvasTexture(cv); t.encoding=THREE.sRGBEncoding; return t; }\n\t\tvar panelTex=texAnim(1024,576,function(g,w,h,t){\n\t\t\t\/* three slides, each drawing itself in before it rests *\/\n\t\t\tvar SLIDE=7.5, n=3;\n\t\t\tvar idx=Math.floor(t\/SLIDE)%n, p=(t%SLIDE)\/SLIDE;\n\t\t\tvar draw=easeOut(p\/0.28);                 \/* the build-in phase *\/\n\t\t\tscrPanelChrome(g,w,h,idx,n);\n\n\t\t\tif(idx===0){\n\t\t\t\tscrTitle(g,'Photosynthesis',60,150,360*draw);\n\t\t\t\tg.save(); g.beginPath(); g.rect(60,200,(430-60)*draw,300); g.clip();\n\t\t\t\tg.strokeStyle='rgba(255,255,255,.85)'; g.lineWidth=5;\n\t\t\t\tg.beginPath(); g.moveTo(80,320); g.bezierCurveTo(180,220,260,420,380,300); g.stroke();\n\t\t\t\tg.restore();\n\t\t\t\tvar pulse=1+Math.sin(t*1.7)*0.05;\n\t\t\t\tg.fillStyle='#59d4a8'; g.beginPath(); g.arc(300,300,46*draw*pulse,0,7); g.fill();\n\t\t\t\tg.fillStyle='#ffc94d'; g.beginPath(); g.arc(160,420,30*draw,0,7); g.fill();\n\t\t\t\tvar bars=[120,200,160,240];\n\t\t\t\tfor(var b=0;b<4;b++){\n\t\t\t\t\tvar bh=bars[b]*easeOut((p\/0.28)-b*0.12);\n\t\t\t\t\tg.fillStyle='rgba(143,176,238,.9)'; g.fillRect(560+b*100,480-bh,56,bh);\n\t\t\t\t}\n\t\t\t\tg.fillStyle='rgba(255,255,255,.7)'; g.font='500 26px sans-serif';\n\t\t\t\tif(p>0.3) g.fillText('CO\u2082 + H\u2082O + light \u2192 glucose + O\u2082',60,520);\n\t\t\t} else if(idx===1){\n\t\t\t\tscrTitle(g,'The Water Cycle',60,150,340*draw);\n\t\t\t\tvar cx=520, cy=340, R=150;\n\t\t\t\tg.strokeStyle='rgba(185,167,255,.75)'; g.lineWidth=6;\n\t\t\t\tg.beginPath(); g.arc(cx,cy,R,-1.6,-1.6+6.283*draw); g.stroke();\n\t\t\t\tvar labels=[['Evaporation',-1.6],['Condensation',0.5],['Precipitation',2.6],['Collection',4.4]];\n\t\t\t\tg.font='600 26px sans-serif';\n\t\t\t\tfor(var l=0;l<4;l++){\n\t\t\t\t\tvar a=labels[l][1], on=draw>(l\/4);\n\t\t\t\t\tif(!on) continue;\n\t\t\t\t\tvar x=cx+Math.cos(a)*R, y=cy+Math.sin(a)*R;\n\t\t\t\t\tg.fillStyle='#8fb0ee'; g.beginPath(); g.arc(x,y,13,0,7); g.fill();\n\t\t\t\t\tg.fillStyle='rgba(255,255,255,.9)';\n\t\t\t\t\tg.fillText(labels[l][0], x+(Math.cos(a)>0?24:-24-g.measureText(labels[l][0]).width), y+9);\n\t\t\t\t}\n\t\t\t\tg.fillStyle='rgba(255,255,255,.55)'; g.font='500 24px sans-serif';\n\t\t\t\tg.fillText('Tap a stage to expand',60,520);\n\t\t\t} else {\n\t\t\t\tscrTitle(g,'Quick check',60,150,240*draw);\n\t\t\t\tvar qs=['Which gas do plants take in?','Oxygen','Carbon dioxide','Nitrogen'];\n\t\t\t\tg.font='600 30px sans-serif';\n\t\t\t\tg.fillStyle='rgba(255,255,255,.92)'; g.fillText(qs[0],60,230);\n\t\t\t\tfor(var a2=1;a2<4;a2++){\n\t\t\t\t\tvar top=270+(a2-1)*80, show=easeOut((p\/0.28)-(a2-1)*0.18);\n\t\t\t\t\tif(show<=0) continue;\n\t\t\t\t\tvar right=(a2===2), lit=right && p>0.55;\n\t\t\t\t\tg.fillStyle= lit ? 'rgba(89,212,168,.28)' : 'rgba(255,255,255,.10)';\n\t\t\t\t\tg.fillRect(60,top,760*show,60);\n\t\t\t\t\tg.fillStyle= lit ? '#8ff0c8' : 'rgba(255,255,255,.85)';\n\t\t\t\t\tg.font='600 28px sans-serif';\n\t\t\t\t\tif(show>0.6) g.fillText(qs[a2],86,top+40);\n\t\t\t\t}\n\t\t\t}\n\t\t\t\/* animating hard during the build, idling once it settles *\/\n\t\t\treturn p<0.32 ? 0.08 : 0.5;\n\t\t});\n\t\tvar lessonTex=tex(1024,640,function(g,w,h){\n\t\t\tg.fillStyle='#fdfcff'; g.fillRect(0,0,w,h);\n\t\t\tg.strokeStyle='rgba(80,32,143,.12)'; g.lineWidth=2;\n\t\t\tfor(var x=0;x<w;x+=64){ g.beginPath(); g.moveTo(x,0); g.lineTo(x,h); g.stroke(); }\n\t\t\tfor(var y=0;y<h;y+=64){ g.beginPath(); g.moveTo(0,y); g.lineTo(w,y); g.stroke(); }\n\t\t\tg.strokeStyle='#50208F'; g.lineWidth=8; g.beginPath(); g.moveTo(180,480); g.lineTo(420,160); g.lineTo(660,480); g.closePath(); g.stroke();\n\t\t\tg.fillStyle='#363996'; g.font='700 52px sans-serif'; g.fillText('a\u00b2 + b\u00b2 = c\u00b2',600,180);\n\t\t\tg.fillStyle='#ff8f6b'; g.beginPath(); g.arc(760,420,64,0,7); g.fill();\n\t\t\tg.fillStyle='#241a3e'; g.font='600 34px sans-serif'; g.fillText('Interactive Geometry',180,580);\n\t\t});\n\t\tvar signTex=texAnim(512,1024,function(g,w,h,t){\n\t\t\tvar bg=g.createLinearGradient(0,0,0,h);\n\t\t\tbg.addColorStop(0,'#50208F'); bg.addColorStop(.6,'#363996'); bg.addColorStop(1,'#1d1245');\n\t\t\tg.fillStyle=bg; g.fillRect(0,0,w,h);\n\t\t\tg.fillStyle='#ffffff'; g.font='800 64px sans-serif'; g.fillText('Welcome',80,180);\n\t\t\tg.fillStyle='rgba(255,255,255,.85)'; g.font='600 40px sans-serif'; g.fillText('Digital MQR',80,250);\n\n\t\t\t\/* the schedule scrolls one row at a time *\/\n\t\t\tvar rows=[['08:00','Science Lab'],['10:00','Robotics'],['12:00','VR Journey'],\n\t\t\t          ['13:30','Smart Panel Training'],['15:00','3D Printing Club']];\n\t\t\tvar STEP=4.0, k=Math.floor(t\/STEP)%rows.length, p=(t%STEP)\/STEP;\n\t\t\tvar slide=p<0.18 ? (1-easeOut(p\/0.18))*170 : 0;\n\t\t\tg.save(); g.beginPath(); g.rect(40,300,432,560); g.clip();\n\t\t\tfor(var i=0;i<3;i++){\n\t\t\t\tvar r=rows[(k+i)%rows.length], y=320+i*180+slide;\n\t\t\t\tg.fillStyle='rgba(255,255,255,'+(i===0?'.20':'.12')+')'; g.fillRect(50,y,412,150);\n\t\t\t\tg.fillStyle='#b9a7ff'; g.font='700 36px sans-serif'; g.fillText(r[0],80,y+70);\n\t\t\t\tg.fillStyle='rgba(255,255,255,.92)'; g.font='600 32px sans-serif'; g.fillText(r[1],80,y+118);\n\t\t\t}\n\t\t\tg.restore();\n\t\t\t\/* live dot, and a clock that actually ticks *\/\n\t\t\tg.fillStyle= (Math.floor(t)%2) ? '#39e29a' : 'rgba(57,226,154,.35)';\n\t\t\tg.beginPath(); g.arc(440,140,16,0,7); g.fill();\n\t\t\tvar d=new Date();\n\t\t\tg.fillStyle='rgba(255,255,255,.7)'; g.font='600 30px sans-serif';\n\t\t\tg.fillText(('0'+d.getHours()).slice(-2)+':'+('0'+d.getMinutes()).slice(-2),80,930);\n\t\t\treturn p<0.2 ? 0.08 : 0.5;\n\t\t});\n\t\tvar topoTex=tex(512,512,function(g,w,h){\n\t\t\tg.fillStyle='#d9c69a'; g.fillRect(0,0,w,h);\n\t\t\tvar cols=['#3fa8c9','#59d4a8','#a8d95c','#e8c85a','#e09a54'];\n\t\t\tfor(var r=5;r>=1;r--){ g.fillStyle=cols[r-1]; g.beginPath();\n\t\t\t\tg.ellipse(w\/2+((r%2)?26:-18),h\/2+((r%2)?-14:20),r*46,r*34,.5,0,7); g.fill(); }\n\t\t\tg.strokeStyle='rgba(60,40,20,.35)'; g.lineWidth=3;\n\t\t\tfor(var r2=5;r2>=1;r2--){ g.beginPath(); g.ellipse(w\/2+((r2%2)?26:-18),h\/2+((r2%2)?-14:20),r2*46,r2*34,.5,0,7); g.stroke(); }\n\t\t});\n\t\tvar floorGameTex=tex(512,512,function(g,w,h){\n\t\t\tvar rg=g.createRadialGradient(w\/2,h\/2,20,w\/2,h\/2,250); rg.addColorStop(0,'#6d43c9'); rg.addColorStop(1,'#2a1b52');\n\t\t\tg.fillStyle=rg; g.fillRect(0,0,w,h);\n\t\t\tg.strokeStyle='rgba(255,255,255,.5)'; g.lineWidth=6;\n\t\t\tg.beginPath(); g.arc(w\/2,h\/2,190,0,7); g.stroke();\n\t\t\tg.beginPath(); g.arc(w\/2,h\/2,120,0,7); g.stroke();\n\t\t\tvar cols=['#ffc94d','#59d4a8','#ff8f6b','#8fb0ff'];\n\t\t\tfor(var i=0;i<8;i++){ var a=i*Math.PI\/4; g.fillStyle=cols[i%4]; g.beginPath();\n\t\t\t\tg.arc(w\/2+Math.cos(a)*155,h\/2+Math.sin(a)*155,26,0,7); g.fill(); }\n\t\t\tg.fillStyle='#fff'; g.font='800 60px sans-serif'; g.textAlign='center'; g.fillText('7 + 5',w\/2,h\/2-10);\n\t\t\tg.font='700 40px sans-serif'; g.fillText('= ?',w\/2,h\/2+50);\n\t\t});\n\t\tvar labTex=tex(512,320,function(g,w,h){\n\t\t\tg.fillStyle='#10233f'; g.fillRect(0,0,w,h);\n\t\t\tg.fillStyle='#8fd4ff'; g.font='700 30px sans-serif'; g.fillText('Virtual Chemistry',24,44);\n\t\t\tfor(var i=0;i<3;i++){ g.strokeStyle=['#59d4a8','#ffc94d','#ff8f6b'][i]; g.lineWidth=5;\n\t\t\t\tg.beginPath(); g.arc(90+i*160,180,52,0,7); g.stroke();\n\t\t\t\tg.beginPath(); g.arc(90+i*160,180,20,0,7); g.fillStyle=['#59d4a8','#ffc94d','#ff8f6b'][i]; g.fill(); }\n\t\t\tg.fillStyle='rgba(255,255,255,.6)'; g.font='500 22px sans-serif'; g.fillText('H\u2082SO\u2084 + 2NaOH \u2192 Na\u2082SO\u2084 + 2H\u2082O',24,290);\n\t\t});\n\t\tvar pcTex=tex(512,320,function(g,w,h){\n\t\t\tvar bg=g.createLinearGradient(0,0,w,h); bg.addColorStop(0,'#1c2a4d'); bg.addColorStop(1,'#232a5c'); g.fillStyle=bg; g.fillRect(0,0,w,h);\n\t\t\tg.fillStyle='rgba(255,255,255,.1)'; g.fillRect(0,0,w,44);\n\t\t\tg.fillStyle='#8fb0ff'; g.font='700 26px sans-serif'; g.fillText('Coding \u2014 Lesson 4',20,32);\n\t\t\tg.font='600 24px monospace';\n\t\t\tvar lines=['for i in range(10):','    draw(star, i)','    move(20)','run()'];\n\t\t\tfor(var i=0;i<4;i++){ g.fillStyle=['#59d4a8','#ffc94d','#ffc94d','#ff8f6b'][i]; g.fillText(lines[i],28,96+i*46); }\n\t\t\tg.fillStyle='#39e29a'; g.beginPath(); g.arc(470,300,10,0,7); g.fill();\n\t\t});\n\n\t\t\/* ---------------- world ---------------- *\/\n\t\tvar world=new THREE.Group(); scene.add(world);\n\t\tvar RX=8, RZ=5.5; \/* half-extents of the bigger room *\/\n\t\tvar groundTex=tex(512,512,function(g,w,h){\n\t\t\tvar rg=g.createRadialGradient(w\/2,h\/2,40,w\/2,h\/2,256);\n\t\t\trg.addColorStop(0,'rgba(46,32,92,.95)'); rg.addColorStop(.65,'rgba(30,20,64,.55)'); rg.addColorStop(1,'rgba(30,20,64,0)');\n\t\t\tg.fillStyle=rg; g.fillRect(0,0,w,h);\n\t\t});\n\t\tvar ground=new THREE.Mesh(new THREE.CircleGeometry(30,64),\n\t\t\tnew THREE.MeshBasicMaterial({map:groundTex,transparent:true,depthWrite:false}));\n\t\tground.rotation.x=-Math.PI\/2; ground.position.y=-.16; world.add(ground);\n\t\tvar slab=box(RX*2+1,.3,RZ*2+1,0x9b90c9,{roughness:.9}); slab.position.y=-.15; world.add(slab);\n\t\t\/* Fake occlusion where surfaces meet \u2014 a baking pipeline in two canvases. *\/\n\t\tvar floorAO=tex(512,512,function(g,w,h){\n\t\t\tg.fillStyle='#ffffff'; g.fillRect(0,0,w,h);\n\t\t\tvar gr=g.createRadialGradient(w\/2,h\/2,w*.16,w\/2,h\/2,w*.76);\n\t\t\tgr.addColorStop(0,'rgba(0,0,0,0)'); gr.addColorStop(1,'rgba(0,0,0,.34)');\n\t\t\tg.fillStyle=gr; g.fillRect(0,0,w,h);\n\t\t});\n\t\tvar wallAO=tex(256,256,function(g,w,h){\n\t\t\tg.fillStyle='#ffffff'; g.fillRect(0,0,w,h);\n\t\t\tvar gb=g.createLinearGradient(0,h,0,h*.52);\n\t\t\tgb.addColorStop(0,'rgba(0,0,0,.32)'); gb.addColorStop(1,'rgba(0,0,0,0)');\n\t\t\tg.fillStyle=gb; g.fillRect(0,0,w,h);\n\t\t\tvar gt=g.createLinearGradient(0,0,0,h*.2);\n\t\t\tgt.addColorStop(0,'rgba(0,0,0,.2)'); gt.addColorStop(1,'rgba(0,0,0,0)');\n\t\t\tg.fillStyle=gt; g.fillRect(0,0,w,h);\n\t\t});\n\t\tvar floor=new THREE.Mesh(new THREE.BoxGeometry(RX*2,.06,RZ*2),mat(FLOOR,{roughness:.95,metalness:0,map:floorAO}));\n\t\tfloor.position.y=.03; floor.receiveShadow=true; world.add(floor);\n\t\tvar rug=new THREE.Mesh(new THREE.CircleGeometry(2,40),mat(0xcabcf0,{roughness:1}));\n\t\trug.rotation.x=-Math.PI\/2; rug.position.set(-5.6,.065,3.4); rug.receiveShadow=true; world.add(rug);\n\n\t\tfunction wallPlane(w,h,c){\n\t\t\tvar m=new THREE.Mesh(new THREE.PlaneGeometry(w,h),mat(c,{roughness:.92,metalness:0,map:wallAO,side:THREE.FrontSide}));\n\t\t\tm.receiveShadow=true; return m;\n\t\t}\n\t\tvar wb=wallPlane(RX*2,3.5,WALL); wb.position.set(0,1.78,-RZ); world.add(wb);\n\t\tvar wl=wallPlane(RZ*2,3.5,WALL_B); wl.rotation.y=Math.PI\/2; wl.position.set(-RX,1.78,0); world.add(wl);\n\t\tvar wr=wallPlane(RZ*2,3.5,WALL_B); wr.rotation.y=-Math.PI\/2; wr.position.set(RX,1.78,0); world.add(wr);\n\t\t\/* ---- close the box -------------------------------------------------\n\t\t   These four surfaces face inward only. From the overview camera \u2014\n\t\t   above and outside the room \u2014 they are backfaces and get culled, so\n\t\t   the dollhouse view is untouched. From inside they are what stops\n\t\t   the frame filling with empty background above the wall line. *\/\n\t\tvar ceilAO=tex(256,256,function(g,w,h){\n\t\t\tg.fillStyle='#ffffff'; g.fillRect(0,0,w,h);\n\t\t\tvar gr=g.createRadialGradient(w\/2,h\/2,w*.12,w\/2,h\/2,w*.68);\n\t\t\tgr.addColorStop(0,'rgba(0,0,0,0)'); gr.addColorStop(1,'rgba(0,0,0,.26)');\n\t\t\tg.fillStyle=gr; g.fillRect(0,0,w,h);\n\t\t});\n\t\tvar ceil=new THREE.Mesh(new THREE.PlaneGeometry(RX*2,RZ*2),\n\t\t\tmat(0xdad5e9,{roughness:.97,metalness:0,map:ceilAO,side:THREE.BackSide}));\n\t\tceil.rotation.x=-Math.PI\/2; ceil.position.y=3.52; world.add(ceil);\n\n\t\tvar wf=new THREE.Mesh(new THREE.PlaneGeometry(RX*2,3.5),\n\t\t\tmat(WALL_B,{roughness:.92,metalness:0,map:wallAO,side:THREE.BackSide}));\n\t\twf.position.set(0,1.78,RZ); world.add(wf);\n\n\t\t\/* recessed ceiling light panels \u2014 the room reads as lit rather than\n\t\t   glowing from nowhere, and they give the glossy surfaces something\n\t\t   rectangular to catch *\/\n\t\tvar panelLightMat=new THREE.MeshBasicMaterial({color:0xf4eede});\n\t\tfor(var cl=0;cl<6;cl++){\n\t\t\tvar lp=new THREE.Mesh(new THREE.PlaneGeometry(2.2,.5), panelLightMat);\n\t\t\tlp.rotation.x=Math.PI\/2;\n\t\t\tlp.position.set(((cl%3)-1)*4.6, 3.5, (Math.floor(cl\/3)?1:-1)*2.4);\n\t\t\tworld.add(lp);\n\t\t}\n\n\t\t\/* skirting board: a 9cm break where wall meets floor *\/\n\t\tfunction skirt(len,x,z,ry){\n\t\t\tvar s=box(len,.09,.035,0xe6e1f4,{roughness:.7,metalness:0});\n\t\t\ts.position.set(x,.075,z); s.rotation.y=ry||0; world.add(s);\n\t\t}\n\t\tskirt(RX*2,0,-RZ+.02,0);\n\t\tskirt(RZ*2,-RX+.02,0,Math.PI\/2);\n\t\tskirt(RZ*2, RX-.02,0,Math.PI\/2);\n\n\t\tfunction strip(w,x,z,ry,c,y,h){ var s=box(w,h||.16,.05,c,{roughness:.6}); s.position.set(x,y||.11,z); s.rotation.y=ry||0; world.add(s); }\n\t\tstrip(RX*2,0,-RZ+.03,0,V); strip(RZ*2,-RX+.03,0,Math.PI\/2,V); strip(RZ*2,RX-.03,0,Math.PI\/2,V);\n\t\tstrip(RX*2,0,-RZ+.04,0,0xffffff,3.38,.14);\n\t\t\/* windows on right wall *\/\n\t\tfor(var wi=0;wi<3;wi++){\n\t\t\tvar wf=box(.08,1.35,1.7,0xffffff,{roughness:.4}); wf.position.set(RX-.05,2.1,-3+wi*2.6); world.add(wf);\n\t\t\tvar wg=new THREE.Mesh(new THREE.PlaneGeometry(1.5,1.15),\n\t\t\t\tnew THREE.MeshStandardMaterial({color:0xbfd8ff,emissive:0x9cc1ff,emissiveIntensity:.55,roughness:.15}));\n\t\t\twg.rotation.y=-Math.PI\/2; wg.position.set(RX-.1,2.1,-3+wi*2.6); world.add(wg);\n\t\t\tvar shaft=new THREE.Mesh(new THREE.PlaneGeometry(1.6,3.6),\n\t\t\t\tnew THREE.MeshBasicMaterial({color:0xcfe0ff,transparent:true,opacity:.05,depthWrite:false,side:THREE.DoubleSide}));\n\t\t\tshaft.position.set(RX-1.3,1.4,-3+wi*2.6); shaft.rotation.z=.5; shaft.rotation.y=Math.PI\/2; world.add(shaft);\n\t\t}\n\t\t\/* door on left wall *\/\n\t\tvar door=box(.09,2.35,1.1,0x8a6cc9,{roughness:.5}); door.position.set(-RX+.06,1.18,4.2); world.add(door);\n\t\tvar knob=sph(.045,0xffe28a,{metalness:.6,roughness:.3}); knob.position.set(-RX+.14,1.15,3.8); world.add(knob);\n\t\t\/* clock + art *\/\n\t\tvar clock1=cyl(.28,.28,.05,0xffffff,{roughness:.3}); clock1.rotation.x=Math.PI\/2; clock1.position.set(-4.4,2.85,-RZ+.06); world.add(clock1);\n\t\tvar art=box(1.1,.8,.05,0xffffff,{roughness:.4}); art.position.set(4.4,2.45,-RZ+.05); world.add(art);\n\t\tvar artIn=new THREE.Mesh(new THREE.PlaneGeometry(.95,.65),mat(0xb9a7ff,{roughness:.6})); artIn.position.set(4.4,2.45,-RZ+.09); world.add(artIn);\n\t\t\/* pendant lights *\/\n\t\tfor(var pl=0;pl<4;pl++){\n\t\t\tvar wire=cyl(.012,.012,.9,0x555577); wire.position.set(-4.5+pl*3,3.25,.4); world.add(wire);\n\t\t\tvar shade=cyl(.02,.26,.3,0x2d2350,{roughness:.4}); shade.position.set(-4.5+pl*3,2.7,.4); world.add(shade);\n\t\t\tvar bulb=new THREE.Mesh(new THREE.SphereGeometry(.09,12,10),new THREE.MeshBasicMaterial({color:0xfff2cc})); bulb.position.set(-4.5+pl*3,2.6,.4); world.add(bulb);\n\t\t}\n\t\tfunction plant(x,z,s){\n\t\t\tvar g=new THREE.Group();\n\t\t\tvar pot=cyl(.22*s,.16*s,.3*s,0xd97f5f,{roughness:.8}); pot.position.y=.15*s; g.add(pot);\n\t\t\tfor(var i=0;i<5;i++){ var leaf=sph(.16*s,0x3f9e63,{roughness:.9},10,8);\n\t\t\t\tleaf.position.set(Math.cos(i*1.3)*.12*s,.42*s+Math.sin(i*2.1)*.08*s,Math.sin(i*1.3)*.12*s); g.add(leaf); }\n\t\t\tg.position.set(x,0,z); world.add(g); return g;\n\t\t}\n\t\tplant(7.3,-5,1.15); plant(-7.4,-4.9,1); plant(7.4,5,1.05);\n\t\t\/* bookshelf *\/\n\t\tvar shelf=new THREE.Group();\n\t\tvar sf=box(1.7,1.7,.4,0xefe9dd,{roughness:.7}); sf.position.y=.85; shelf.add(sf);\n\t\tvar bookCols=[V,P,MINT,CORAL,SUN,0x8a6cc9];\n\t\tfor(var r3=0;r3<3;r3++){ for(var b3=0;b3<5;b3++){\n\t\t\tvar bk=box(.16,.34,.3,bookCols[(r3*5+b3)%6],{roughness:.7});\n\t\t\tbk.position.set(-.6+b3*.3,(r3*.52)+.36,.02); shelf.add(bk);\n\t\t}}\n\t\tshelf.position.set(-6.6,0,-5.15); world.add(shelf);\n\t\t\/* backpack hooks on left wall *\/\n\t\tvar packCols=[CORAL,MINT,SUN];\n\t\tfor(var bp=0;bp<3;bp++){\n\t\t\tvar hook=box(.06,.06,.14,0x8a8398); hook.position.set(-RX+.1,1.6,.4+bp*.7); world.add(hook);\n\t\t\tvar pack=box(.3,.44,.2,packCols[bp],{roughness:.85});\n\t\t\tpack.position.set(-RX+.26,1.32,.4+bp*.7); pack.rotation.z=.06-bp*.05; world.add(pack);\n\t\t\tvar flap=box(.3,.16,.21,0xffffff,{roughness:.8}); flap.position.set(-RX+.26,1.48,.4+bp*.7); flap.rotation.z=.06-bp*.05; world.add(flap);\n\t\t}\n\t\t\/* floating dust *\/\n\t\tvar dustG=new THREE.BufferGeometry(), dustN=70, dustPos=new Float32Array(dustN*3);\n\t\tfor(var dp=0;dp<dustN;dp++){ dustPos[dp*3]=(Math.random()*2-1)*RX*.9; dustPos[dp*3+1]=Math.random()*3; dustPos[dp*3+2]=(Math.random()*2-1)*RZ*.9; }\n\t\tdustG.setAttribute('position',new THREE.BufferAttribute(dustPos,3));\n\t\tvar dust=new THREE.Points(dustG,new THREE.PointsMaterial({color:0xbfb3ff,size:.045,transparent:true,opacity:.4,depthWrite:false}));\n\t\tworld.add(dust);\n\n\t\tvar parts=[];\n\t\tfunction part(g){ parts.push(g); world.add(g); return g; }\n\n\t\t\/* ---- 1 panel ---- *\/\n\t\tvar panel=new THREE.Group();\n\t\tvar pFrame=box(3.6,2.1,.1,0x1c1533,{roughness:.35}); pFrame.position.y=1.95; panel.add(pFrame);\n\t\tvar pScreen=new THREE.Mesh(new THREE.PlaneGeometry(3.4,1.9),\n\t\t\tscreenMat({map:panelTex,emissive:0xffffff,emissiveMap:panelTex,emissiveIntensity:.55,roughness:.3}));\n\t\tpScreen.position.set(0,1.95,.06); panel.add(pScreen);\n\t\tvar pBar=box(3.6,.09,.12,0x35295e,{roughness:.4}); pBar.position.set(0,.84,.01); panel.add(pBar);\n\t\tvar pGlow=new THREE.PointLight(GLOW,.7,5.5); pGlow.position.set(0,1.9,1); panel.add(pGlow);\n\t\tpanel.position.set(0,0,-RZ+.08); part(panel);\n\n\t\t\/* ---- 2 projector (left wall) ---- *\/\n\t\tvar proj=new THREE.Group();\n\t\tvar pjBody=box(.72,.2,.5,WHITE,{roughness:.3}); pjBody.position.set(.5,2.66,0); proj.add(pjBody);\n\t\tvar pjLens=cyl(.05,.05,.06,0x222244); pjLens.rotation.z=Math.PI\/2; pjLens.position.set(.32,2.57,0); proj.add(pjLens);\n\t\tvar pjArm=box(.06,.42,.06,0xd8d8e6); pjArm.position.set(.5,2.9,0); proj.add(pjArm);\n\t\tvar pjImg=new THREE.Mesh(new THREE.PlaneGeometry(2.7,1.7),\n\t\t\tscreenMat({map:lessonTex,emissive:0xffffff,emissiveMap:lessonTex,emissiveIntensity:.35,roughness:.5}));\n\t\tpjImg.rotation.y=Math.PI\/2; pjImg.position.set(.06,1.88,0); proj.add(pjImg);\n\t\tvar pjFrame=box(.04,1.9,2.86,0xffffff,{roughness:.5}); pjFrame.position.set(.03,1.88,0); proj.add(pjFrame);\n\t\tvar beam=new THREE.Mesh(new THREE.ConeGeometry(1.35,1.9,4,1,true),\n\t\t\tnew THREE.MeshBasicMaterial({color:GLOW,transparent:true,opacity:.08,side:THREE.DoubleSide,depthWrite:false}));\n\t\tbeam.rotation.z=Math.PI\/2; beam.rotation.y=Math.PI\/4; beam.position.set(.55,2.25,0); proj.add(beam);\n\t\tproj.position.set(-RX,0,-2.8); part(proj);\n\n\t\t\/* ---- 3 Roomie floor ---- *\/\n\t\tvar rm=new THREE.Group();\n\t\tvar game=new THREE.Mesh(new THREE.CircleGeometry(1.5,48),\n\t\t\tnew THREE.MeshStandardMaterial({map:floorGameTex,emissive:0xffffff,emissiveMap:floorGameTex,emissiveIntensity:.5,roughness:.4}));\n\t\tgame.rotation.x=-Math.PI\/2; game.position.y=.075; rm.add(game); rm.userData.game=game;\n\t\tvar ring2=new THREE.Mesh(new THREE.RingGeometry(1.55,1.72,48),\n\t\t\tnew THREE.MeshBasicMaterial({color:GLOW,transparent:true,opacity:.85,side:THREE.DoubleSide}));\n\t\tring2.rotation.x=-Math.PI\/2; ring2.position.y=.08; rm.add(ring2);\n\t\tvar shapes=new THREE.Group();\n\t\tvar sGeos=[new THREE.IcosahedronGeometry(.15),new THREE.TorusGeometry(.13,.05,10,20),new THREE.OctahedronGeometry(.14),new THREE.SphereGeometry(.12,16,12)];\n\t\tvar sCols=[SUN,MINT,CORAL,0x8fb0ff];\n\t\tfor(var s4=0;s4<4;s4++){\n\t\t\tvar sm=new THREE.Mesh(sGeos[s4],mat(sCols[s4],{roughness:.35,emissive:sCols[s4],emissiveIntensity:.3}));\n\t\t\tsm.position.set(Math.cos(s4*1.57)*.9,.5,Math.sin(s4*1.57)*.9); sm.userData.ph=s4*1.3; shapes.add(sm);\n\t\t}\n\t\trm.add(shapes); rm.userData.shapes=shapes;\n\t\tvar ceilProj=box(.34,.16,.34,WHITE,{roughness:.35}); ceilProj.position.y=3.15; rm.add(ceilProj);\n\t\tvar rmBeam=new THREE.Mesh(new THREE.ConeGeometry(1.5,3.05,24,1,true),\n\t\t\tnew THREE.MeshBasicMaterial({color:V,transparent:true,opacity:.06,depthWrite:false,side:THREE.DoubleSide}));\n\t\trmBeam.position.y=1.6; rm.add(rmBeam);\n\t\trm.position.set(2.2,0,2.6); part(rm);\n\n\t\t\/* ---- 4 sandbox ---- *\/\n\t\tvar sb=new THREE.Group();\n\t\tvar sbBase=boxM(1.7,.75,1.15,MTL.wood(0x9c8468)); sbBase.position.y=.38; sb.add(sbBase);\n\t\tvar sbTop=new THREE.Mesh(new THREE.PlaneGeometry(1.5,.95),\n\t\t\tnew THREE.MeshStandardMaterial({map:topoTex,emissive:0xffffff,emissiveMap:topoTex,emissiveIntensity:.3,roughness:.7}));\n\t\tsbTop.rotation.x=-Math.PI\/2; sbTop.position.y=.765; sb.add(sbTop);\n\t\tvar sbArm=cyl(.035,.035,1.3,0x777799); sbArm.position.set(.72,1.4,0); sb.add(sbArm);\n\t\tvar sbHead=box(.24,.12,.2,DARK,{roughness:.4}); sbHead.position.set(.6,2.05,0); sb.add(sbHead);\n\t\tsb.position.set(-4.6,0,-3.9); sb.rotation.y=.5; part(sb);\n\n\t\t\/* ---- 5 VR corner ---- *\/\n\t\tvar vr=new THREE.Group();\n\t\tfunction beanbag(x,z,c){ var m=sph(.42,c,{roughness:.95},18,14); m.scale.y=.55; m.position.set(x,.23,z); return m; }\n\t\tvr.add(beanbag(-.55,.3,V)); vr.add(beanbag(.4,-.2,P)); vr.add(beanbag(.15,.75,0x8a6cc9));\n\t\tfunction headset(x,z,ry){\n\t\t\tvar g=new THREE.Group();\n\t\t\tvar stand=cyl(.035,.05,.8,0xd0d0dd,{metalness:.4,roughness:.3}); stand.position.y=.4; g.add(stand);\n\t\t\tvar base=cyl(.14,.14,.02,0xd0d0dd); base.position.y=.01; g.add(base);\n\t\t\tvar hs=box(.32,.17,.19,0xf4f4f8,{roughness:.3}); hs.position.y=.88; g.add(hs);\n\t\t\tvar visor=new THREE.Mesh(new THREE.PlaneGeometry(.28,.11),new THREE.MeshBasicMaterial({color:GLOW})); visor.position.set(0,.88,.1); g.add(visor);\n\t\t\tvar strap=new THREE.Mesh(new THREE.TorusGeometry(.13,.025,8,20),mat(0x3a2f5c)); strap.rotation.y=Math.PI\/2; strap.position.y=.88; g.add(strap);\n\t\t\tg.position.set(x,0,z); g.rotation.y=ry; return g;\n\t\t}\n\t\tvr.add(headset(-.95,-.35,.5)); vr.add(headset(.95,.35,-.4)); vr.add(headset(0,-.8,0));\n\t\tvr.position.set(-5.6,0,3.4); part(vr);\n\n\t\t\/* ---- 6 robotics bench + drone ---- *\/\n\t\tvar rb=new THREE.Group();\n\t\tvar bench=box(2.7,.09,1.25,WOOD,{roughness:.75}); bench.position.y=.97; rb.add(bench);\n\t\t[[-1.2,.5],[1.2,.5],[-1.2,-.5],[1.2,-.5]].forEach(function(l){\n\t\t\tvar leg=cylM(.045,.045,.97,MTL.steel(0xa9a5bb)); leg.position.set(l[0],.485,l[1]); rb.add(leg);\n\t\t});\n\t\tvar pr=new THREE.Group();\n\t\tvar prF=box(.6,.66,.55,0x2b2150,{roughness:.35}); prF.position.y=.33; pr.add(prF);\n\t\tvar prI=box(.44,.5,.45,0x0e0a1e,{emissive:0xff8c42,emissiveIntensity:.55}); prI.position.set(0,.33,.04); pr.add(prI);\n\t\tvar prObj=new THREE.Mesh(new THREE.ConeGeometry(.1,.18,16),mat(SUN,{emissive:SUN,emissiveIntensity:.4})); prObj.position.set(0,.22,.04); pr.add(prObj);\n\t\tvar prHead=box(.12,.06,.1,0xff8c42,{emissive:0xff8c42,emissiveIntensity:.6}); prHead.position.set(0,.5,.04); pr.userData.head=prHead; pr.add(prHead);\n\t\tpr.position.set(-.85,1.01,0); rb.add(pr); rb.userData.printer=pr;\n\t\tvar bot=new THREE.Group();\n\t\tvar botB=box(.34,.2,.28,SUN,{roughness:.4}); botB.position.y=.15; bot.add(botB);\n\t\tvar botH=box(.2,.15,.17,0xffdd88,{roughness:.4}); botH.position.y=.34; bot.add(botH);\n\t\tvar e1=new THREE.Mesh(new THREE.CircleGeometry(.028,10),new THREE.MeshBasicMaterial({color:0x141026})); e1.position.set(-.045,.35,.09); bot.add(e1);\n\t\tvar e2=e1.clone(); e2.position.x=.045; bot.add(e2);\n\t\tvar ant=cyl(.008,.008,.14,0x444466); ant.position.set(0,.48,0); bot.add(ant);\n\t\tvar antT=new THREE.Mesh(new THREE.SphereGeometry(.02,8,8),new THREE.MeshBasicMaterial({color:MINT})); antT.position.set(0,.56,0); bot.add(antT);\n\t\t[[-.13,-.11],[.13,-.11],[-.13,.11],[.13,.11]].forEach(function(w2){\n\t\t\tvar wh=cyl(.055,.055,.035,0x333344); wh.rotation.z=Math.PI\/2; wh.position.set(w2[0],.055,w2[1]); bot.add(wh);\n\t\t});\n\t\tbot.position.set(.75,1.01,.1); rb.add(bot); rb.userData.bot=bot;\n\t\tvar tools=box(.4,.14,.28,CORAL,{roughness:.6}); tools.position.set(.1,1.08,-.4); rb.add(tools);\n\t\trb.position.set(4.9,0,-4.1); rb.rotation.y=-.5; part(rb);\n\t\t\/* drone *\/\n\t\tvar drone=new THREE.Group();\n\t\tvar dBody=box(.22,.07,.22,DARK,{roughness:.35}); drone.add(dBody);\n\t\tvar dCam=sph(.04,0x8fb0ff,{emissive:0x8fb0ff,emissiveIntensity:.5},8,8); dCam.position.set(0,-.05,.1); drone.add(dCam);\n\t\tvar rotors=[];\n\t\t[[-.16,-.16],[.16,-.16],[-.16,.16],[.16,.16]].forEach(function(a){\n\t\t\tvar arm=box(.16,.02,.02,0x555577); arm.position.set(a[0]*.6,.02,a[1]*.6); arm.rotation.y=Math.atan2(a[1],a[0]); drone.add(arm);\n\t\t\tvar rot=cyl(.11,.11,.012,0xb9a7ff,{transparent:true,opacity:.55}); rot.position.set(a[0],.05,a[1]); drone.add(rot); rotors.push(rot);\n\t\t});\n\t\tdrone.userData.rotors=rotors; drone.position.set(4.9,2.7,-3.4); part(drone);\n\t\tworld.userData.drone=drone;\n\n\t\t\/* ---- 7 science lab ---- *\/\n\t\tvar sl=new THREE.Group();\n\t\tvar slTop=box(2.4,.09,1.1,0xf4f4fa,{roughness:.4}); slTop.position.y=.95; sl.add(slTop);\n\t\tvar slBody=box(2.2,.9,.95,0xdfe6f5,{roughness:.6}); slBody.position.y=.47; sl.add(slBody);\n\t\tfunction flask(x,c){\n\t\t\tvar g=new THREE.Group();\n\t\t\tvar body=new THREE.Mesh(new THREE.ConeGeometry(.11,.22,16),mat(0xe8f2ff,{transparent:true,opacity:.5,roughness:.15})); body.position.y=.11; g.add(body);\n\t\t\tvar liq=new THREE.Mesh(new THREE.ConeGeometry(.085,.12,16),mat(c,{emissive:c,emissiveIntensity:.5,transparent:true,opacity:.85})); liq.position.y=.065; g.add(liq);\n\t\t\tvar neck=cyl(.03,.03,.1,0xe8f2ff,{transparent:true,opacity:.5,roughness:.15}); neck.position.y=.26; g.add(neck);\n\t\t\tg.position.set(x,1,.15); return g;\n\t\t}\n\t\tsl.add(flask(-.75,MINT)); sl.add(flask(-.45,CORAL)); sl.add(flask(-.15,0x8fb0ff));\n\t\tvar micro=new THREE.Group();\n\t\tvar mBase=box(.22,.04,.16,DARK); micro.add(mBase);\n\t\tvar mArm=cyl(.025,.025,.3,DARK); mArm.rotation.z=.5; mArm.position.set(.05,.15,0); micro.add(mArm);\n\t\tvar mTube=cyl(.035,.035,.18,0x555577); mTube.rotation.z=.5; mTube.position.set(.12,.3,0); micro.add(mTube);\n\t\tmicro.position.set(.45,1,.1); sl.add(micro);\n\t\tvar slScr=new THREE.Mesh(new THREE.PlaneGeometry(.9,.56),\n\t\t\tscreenMat({map:labTex,emissive:0xffffff,emissiveMap:labTex,emissiveIntensity:.5,roughness:.3}));\n\t\tslScr.position.set(.85,1.45,0); slScr.rotation.y=-.5; sl.add(slScr);\n\t\tvar slStand=box(.05,.35,.05,DARK); slStand.position.set(.85,1.12,0); sl.add(slStand);\n\t\tsl.position.set(6.3,0,-3.8); sl.rotation.y=-.9; part(sl);\n\n\t\t\/* ---- 8 zero-client lab (right wall) ---- *\/\n\t\tvar zc=new THREE.Group();\n\t\tvar zcTop=box(3.6,.07,.8,0xf4f2fb,{roughness:.5}); zcTop.position.y=.92; zc.add(zcTop);\n\t\t[[-1.6],[0],[1.6]].forEach(function(px,idx){\n\t\t\tvar leg=boxM(.06,.92,.7,MTL.steel()); leg.position.set(px[0],.46,0); zc.add(leg);\n\t\t\tvar mon=new THREE.Group();\n\t\t\tvar scr=new THREE.Mesh(new THREE.PlaneGeometry(.72,.45),\n\t\t\t\tscreenMat({map:pcTex,emissive:0xffffff,emissiveMap:pcTex,emissiveIntensity:.5,roughness:.3}));\n\t\t\tscr.position.y=1.42; mon.add(scr);\n\t\t\tvar bez=box(.78,.5,.03,0x1c1533,{roughness:.35}); bez.position.set(0,1.42,-.02); mon.add(bez);\n\t\t\tvar stand=cyl(.03,.05,.22,0x8f89ad); stand.position.y=1.1; mon.add(stand);\n\t\t\tvar zbox=box(.16,.05,.2,DARK,{roughness:.4}); zbox.position.set(.28,.97,.12); mon.add(zbox);\n\t\t\tvar zled=new THREE.Mesh(new THREE.CircleGeometry(.012,8),new THREE.MeshBasicMaterial({color:0x39e29a})); zled.position.set(.34,.97,.23); zled.rotation.x=-.4; mon.add(zled);\n\t\t\tvar kb=box(.5,.02,.18,0xe4e4ec,{roughness:.6}); kb.position.set(0,.945,.22); mon.add(kb);\n\t\t\tmon.position.x=px[0]; zc.add(mon);\n\t\t\tvar stool=cylM(.2,.2,.06,MTL.fabric([0x00c2a8,0xff9500,0xff3b30,0x1f6feb][px[1]%4||0]));\n\t\t\tstool.position.set(px[0],.55,.75); zc.add(stool);\n\t\t\tvar spole=cyl(.03,.03,.5,0x8f89ad); spole.position.set(px[0],.27,.75); zc.add(spole);\n\t\t});\n\t\tzc.position.set(6.9,0,.8); zc.rotation.y=-Math.PI\/2; part(zc);\n\n\t\t\/* ---- 9 teacher desk ---- *\/\n\t\tvar td=new THREE.Group();\n\t\tvar tdTop=boxM(2.25,.08,.95,MTL.wood(WOOD)); tdTop.position.y=.9; td.add(tdTop);\n\t\tvar tdPanel=boxM(2.05,.72,.07,MTL.wood(WOOD_D)); tdPanel.position.set(0,.5,.4); td.add(tdPanel);\n\t\tvar tdL1=boxM(.07,.9,.85,MTL.steel(0xa9a5bb)); tdL1.position.set(-1.05,.45,0); td.add(tdL1);\n\t\tvar tdL2=tdL1.clone(); tdL2.position.x=1.05; td.add(tdL2);\n\t\tvar lp=new THREE.Group();\n\t\tvar lpB=box(.52,.025,.36,0xe4e4ec); lpB.position.y=.94; lp.add(lpB);\n\t\tvar lpS=new THREE.Mesh(new THREE.BoxGeometry(.52,.34,.02),\n\t\t\tnew THREE.MeshStandardMaterial({color:0x1b1140,emissive:GLOW,emissiveIntensity:.5,roughness:.3}));\n\t\tlpS.position.set(0,1.1,-.17); lpS.rotation.x=-.28; lp.add(lpS);\n\t\tlp.position.x=-.55; td.add(lp);\n\t\tvar dc=new THREE.Group();\n\t\tvar dcBase=box(.3,.03,.24,0x2b2150); dcBase.position.y=.94; dc.add(dcBase);\n\t\tvar dcArm=box(.045,.5,.045,0x2b2150); dcArm.position.set(.1,1.2,0); dc.add(dcArm);\n\t\tvar dcHead=box(.18,.05,.12,0x2b2150); dcHead.position.set(.02,1.46,0); dc.add(dcHead);\n\t\tvar dcL=new THREE.Mesh(new THREE.CircleGeometry(.028,10),new THREE.MeshBasicMaterial({color:GLOW})); dcL.rotation.x=-Math.PI\/2; dcL.position.set(.02,1.43,0); dc.add(dcL);\n\t\tdc.position.x=.55; td.add(dc);\n\t\tvar mug=cyl(.05,.05,.11,CORAL,{roughness:.6}); mug.position.set(.05,1,.28); td.add(mug);\n\t\ttd.position.set(-3.4,0,-4.3); td.rotation.y=.4; part(td);\n\n\t\t\/* ---- student desks ---- *\/\n\t\tvar deskIndex=0;\n\t\tfunction desk(x,z,ry){\n\t\t\tvar g=new THREE.Group();\n\t\t\tvar top=boxM(1.15,.055,.62,MTL.laminate(0xfbfaff)); top.position.y=.76; g.add(top);\n\t\t\tvar edge=box(1.15,.03,.62,[0xff3b30,0xff9500,0x00c2a8,0x1f6feb,0xaf52de,0xff2d78][deskIndex%6],{roughness:.5});\n\t\t\tedge.position.y=.725; g.add(edge);\n\t\t\t[[-.5,-.24],[.5,-.24],[-.5,.24],[.5,.24]].forEach(function(l){\n\t\t\t\tvar leg=cylM(.028,.028,.75,MTL.steel()); leg.position.set(l[0],.375,l[1]); g.add(leg);\n\t\t\t});\n\t\t\tvar tab=new THREE.Mesh(new THREE.BoxGeometry(.42,.29,.02),\n\t\t\t\tnew THREE.MeshStandardMaterial({color:0x161230,emissive:GLOW,emissiveIntensity:.4,roughness:.3}));\n\t\t\ttab.position.set(0,.9,-.1); tab.rotation.x=-1.05; g.add(tab);\n\t\t\t\/* every chair a different colour \u2014 a room where all the furniture\n\t\t\t   matches reads as a showroom, not a classroom *\/\n\t\t\tvar CHAIR=[0xff3b30,0xff9500,0x00c2a8,0x1f6feb,0xaf52de,0xff2d78];\n\t\t\tvar cc_=CHAIR[(deskIndex++)%CHAIR.length];\n\t\t\tvar seat=boxM(.46,.05,.44,MTL.fabric(cc_)); seat.position.set(0,.47,.62); g.add(seat);\n\t\t\tvar back=boxM(.46,.5,.05,MTL.fabric(cc_)); back.position.set(0,.74,.85); g.add(back);\n\t\t\t[[-.18,.47],[.18,.47],[-.18,.8],[.18,.8]].forEach(function(cl){\n\t\t\t\tvar cleg=cylM(.022,.022,.47,MTL.steel(0x9d99b2)); cleg.position.set(cl[0],.235,cl[1]); g.add(cleg);\n\t\t\t});\n\t\t\tg.position.set(x,0,z); g.rotation.y=ry||0; return g;\n\t\t}\n\t\t[[-2.4,-1.7,.06],[-.6,-1.9,0],[1.2,-1.7,-.06],[-1.9,-.2,.04],[-.1,-.35,0],[1.6,-.2,-.05]].forEach(function(d){ part(desk(d[0],d[1],d[2])); });\n\n\t\t\/* ---- 10 signage ---- *\/\n\t\tvar sg=new THREE.Group();\n\t\tvar sgB=box(.72,2.3,.18,0x1c1533,{roughness:.35}); sgB.position.y=1.15; sg.add(sgB);\n\t\tvar sgS=new THREE.Mesh(new THREE.PlaneGeometry(.6,2.05),\n\t\t\tscreenMat({map:signTex,emissive:0xffffff,emissiveMap:signTex,emissiveIntensity:.55,roughness:.3}));\n\t\tsgS.position.set(0,1.2,.1); sg.add(sgS);\n\t\tvar sgBase=box(.9,.08,.5,0x35295e); sgBase.position.y=.04; sg.add(sgBase);\n\t\tsg.position.set(6.9,0,4.3); sg.rotation.y=-.9; part(sg);\n\t\t\/* balloons tied to signage *\/\n\t\tvar balloons=new THREE.Group();\n\t\t[[0,.2,0,CORAL],[.28,.5,.1,SUN],[-.24,.42,-.08,MINT]].forEach(function(b4){\n\t\t\tvar bl=sph(.16,b4[3],{roughness:.35,emissive:b4[3],emissiveIntensity:.12},14,12);\n\t\t\tbl.scale.y=1.18; bl.position.set(b4[0],2.6+b4[1],b4[2]); balloons.add(bl);\n\t\t\tvar str=cyl(.004,.004,1.1,0xd9d2f5); str.position.set(b4[0],2+b4[1]-.35,b4[2]); balloons.add(str);\n\t\t});\n\t\tballoons.position.set(6.35,0,3.7); world.add(balloons); world.userData.balloons=balloons;\n\n\t\t\/* ---- 11 attendance ---- *\/\n\t\tvar att=new THREE.Group();\n\t\tvar attD=box(.26,.36,.06,0x1c1533,{roughness:.3}); attD.position.y=1.45; att.add(attD);\n\t\tvar attS=new THREE.Mesh(new THREE.PlaneGeometry(.2,.26),\n\t\t\tnew THREE.MeshStandardMaterial({color:0x0e2b22,emissive:MINT,emissiveIntensity:.7,roughness:.3}));\n\t\tattS.position.set(0,1.47,.04); att.add(attS);\n\t\tvar attScan=new THREE.Mesh(new THREE.PlaneGeometry(.2,.02),new THREE.MeshBasicMaterial({color:0xd2ffe9}));\n\t\tattScan.position.set(0,1.47,.045); att.userData.scan=attScan; att.add(attScan);\n\t\tatt.position.set(-RX+.07,0,3.3); att.rotation.y=Math.PI\/2; part(att);\n\n\t\t\/* ---- 12 charging cart ---- *\/\n\t\tvar cc=new THREE.Group();\n\t\tvar ccB=box(.95,1.15,.62,V,{roughness:.4}); ccB.position.y=.68; cc.add(ccB);\n\t\tvar ccT=box(1,.05,.66,0x35295e,{roughness:.4}); ccT.position.y=1.28; cc.add(ccT);\n\t\tfor(var r4=0;r4<3;r4++){ for(var c4=0;c4<4;c4++){\n\t\t\tvar slot=box(.16,.2,.02,0x2a1b52,{emissive:GLOW,emissiveIntensity:.45});\n\t\t\tslot.position.set(-.33+c4*.22,.4+r4*.3,.32); cc.add(slot);\n\t\t}}\n\t\t[[-.34,-.2],[.34,-.2],[-.34,.2],[.34,.2]].forEach(function(w3){\n\t\t\tvar wh=cyl(.07,.07,.05,0x222233); wh.rotation.z=Math.PI\/2; wh.position.set(w3[0],.07,w3[1]); cc.add(wh);\n\t\t});\n\t\tvar led=new THREE.Mesh(new THREE.CircleGeometry(.03,10),new THREE.MeshBasicMaterial({color:0x39e29a})); led.position.set(.32,1.18,.32); cc.add(led);\n\t\tcc.position.set(-7,0,2.1); cc.rotation.y=.6; part(cc);\n\n\t\t\/* ---- 13 uniform mannequin ---- *\/\n\t\tvar un=new THREE.Group();\n\t\tvar unBase=cyl(.3,.34,.05,0xd0cbe2); unBase.position.y=.03; un.add(unBase);\n\t\tvar unPole=cyl(.025,.025,.5,0x8f89ad); unPole.position.y=.3; un.add(unPole);\n\t\tvar torso=new THREE.Mesh(new THREE.CylinderGeometry(.16,.22,.62,16),mat(0xffffff,{roughness:.7})); torso.position.y=.86; un.add(torso);\n\t\tvar tie=box(.06,.3,.02,V,{roughness:.5}); tie.position.set(0,.92,.2); un.add(tie);\n\t\tvar collar=cyl(.09,.11,.06,0xf0eef8); collar.position.y=1.2; un.add(collar);\n\t\tvar head=sph(.13,0xe8dcc8,{roughness:.7}); head.position.y=1.38; un.add(head);\n\t\tvar skirt=new THREE.Mesh(new THREE.CylinderGeometry(.22,.3,.34,16),mat(0x50208F,{roughness:.7})); skirt.position.y=.42; un.add(skirt);\n\t\tvar badge=new THREE.Mesh(new THREE.CircleGeometry(.035,10),new THREE.MeshBasicMaterial({color:SUN})); badge.position.set(.1,1.05,.185); un.add(badge);\n\t\tun.position.set(-6.9,0,4.9); un.rotation.y=.9; part(un);\n\n\t\t\/* ---- aquarium (fun) ---- *\/\n\t\tvar aq=new THREE.Group();\n\t\tvar aqStand=box(1.5,.55,.6,0x8a6cc9,{roughness:.6}); aqStand.position.y=.28; aq.add(aqStand);\n\t\tvar water=box(1.4,.6,.5,0x3fa8c9,{transparent:true,opacity:.4,roughness:.15}); water.position.y=.88; aq.add(water);\n\t\tvar glass=box(1.46,.66,.56,0xcfe8ff,{transparent:true,opacity:.14,roughness:.05}); glass.position.y=.89; aq.add(glass);\n\t\tvar sand=box(1.38,.06,.48,0xe8d5a5,{roughness:.9}); sand.position.y=.62; aq.add(sand);\n\t\tvar weed1=new THREE.Mesh(new THREE.ConeGeometry(.05,.3,8),mat(0x3f9e63,{roughness:.8})); weed1.position.set(-.5,.78,.1); aq.add(weed1);\n\t\tvar weed2=weed1.clone(); weed2.position.set(.45,.78,-.12); weed2.scale.setScalar(.8); aq.add(weed2);\n\t\tvar fishes=[];\n\t\t[[CORAL,0],[SUN,1.2],[0x8fb0ff,2.3]].forEach(function(fz){\n\t\t\tvar fg=new THREE.Group();\n\t\t\tvar fb=sph(.06,fz[0],{roughness:.5},10,8); fb.scale.x=1.6; fg.add(fb);\n\t\t\tvar ft=new THREE.Mesh(new THREE.ConeGeometry(.045,.09,8),mat(fz[0],{roughness:.5})); ft.rotation.z=Math.PI\/2; ft.position.x=-.12; fg.add(ft);\n\t\t\tfg.position.y=.9; fg.userData.ph=fz[1]; aq.add(fg); fishes.push(fg);\n\t\t});\n\t\tvar bubbles=[];\n\t\tfor(var bb=0;bb<5;bb++){\n\t\t\tvar bub=new THREE.Mesh(new THREE.SphereGeometry(.016,8,8),new THREE.MeshBasicMaterial({color:0xeaf6ff,transparent:true,opacity:.7}));\n\t\t\tbub.position.set(.4,.7+bb*.09,.05); bub.userData.ph=bb*.7; aq.add(bub); bubbles.push(bub);\n\t\t}\n\t\taq.userData.fishes=fishes; aq.userData.bubbles=bubbles;\n\t\taq.position.set(7.2,0,-5.05); aq.rotation.y=0; part(aq);\n\n\t\t\/* ---- cat (fun) ---- *\/\n\t\tvar cat=new THREE.Group();\n\t\tvar catB=sph(.2,0x9a92b5,{roughness:.95},14,12); catB.scale.set(1.25,.8,.9); catB.position.y=.16; cat.add(catB);\n\t\tvar catH=sph(.12,0x9a92b5,{roughness:.95},12,10); catH.position.set(.24,.28,0); cat.add(catH);\n\t\tvar ear1=new THREE.Mesh(new THREE.ConeGeometry(.04,.07,8),mat(0x9a92b5)); ear1.position.set(.2,.4,.06); cat.add(ear1);\n\t\tvar ear2=ear1.clone(); ear2.position.z=-.06; cat.add(ear2);\n\t\tvar catNose=sph(.015,0xff8f9c,{},6,6); catNose.position.set(.36,.27,0); cat.add(catNose);\n\t\tvar tail=new THREE.Group();\n\t\tvar tailM=cyl(.025,.04,.35,0x9a92b5); tailM.position.y=.17; tail.add(tailM);\n\t\ttail.position.set(-.28,.12,0); tail.rotation.z=.8; cat.add(tail); cat.userData.tail=tail;\n\t\tcat.position.set(-4.6,.065,4.4); cat.rotation.y=-.6; part(cat);\n\n\t\t\/* ---- paper plane (fun) ---- *\/\n\t\tvar plane=new THREE.Group();\n\t\tvar pw1=new THREE.Mesh(new THREE.PlaneGeometry(.34,.16),mat(0xffffff,{side:THREE.DoubleSide,roughness:.6}));\n\t\tpw1.rotation.z=.45; pw1.position.set(0,.03,.09); plane.add(pw1);\n\t\tvar pw2=pw1.clone(); pw2.rotation.z=-.45; pw2.position.z=-.09; plane.add(pw2);\n\t\tvar pbody=new THREE.Mesh(new THREE.ConeGeometry(.03,.3,4),mat(0xf2f0fa,{roughness:.5}));\n\t\tpbody.rotation.z=-Math.PI\/2; plane.add(pbody);\n\t\tworld.add(plane); world.userData.plane=plane;\n\n\t\t\/* ---------------- markers ---------------- *\/\n\t\tfunction markerTex2(num){\n\t\t\tvar cv=document.createElement('canvas'); cv.width=cv.height=160; var g=cv.getContext('2d');\n\t\t\tg.beginPath(); g.arc(80,80,66,0,7); g.strokeStyle='rgba(159,140,255,.28)'; g.lineWidth=4; g.stroke();\n\t\t\tg.beginPath(); g.arc(80,80,50,0,7); g.strokeStyle='rgba(159,140,255,.6)'; g.lineWidth=5; g.stroke();\n\t\t\tvar grd=g.createLinearGradient(40,40,120,120); grd.addColorStop(0,'#6a3bd8'); grd.addColorStop(1,'#7090E0');\n\t\t\tg.beginPath(); g.arc(80,80,34,0,7); g.fillStyle=grd; g.fill();\n\t\t\tg.beginPath(); g.arc(80,80,34,0,7); g.strokeStyle='rgba(255,255,255,.95)'; g.lineWidth=6; g.stroke();\n\t\t\tg.fillStyle='#ffffff'; g.font='800 40px sans-serif'; g.textAlign='center'; g.textBaseline='middle';\n\t\t\tg.fillText(String(num),80,82);\n\t\t\treturn new THREE.CanvasTexture(cv);\n\t\t}\n\t\t\/* ---- keep the furniture out of itself -------------------------\n\t\t   Every station was authored by hand, and by the time there were\n\t\t   fourteen of them around one room several were sitting inside each\n\t\t   other \u2014 the science bench through the robotics bench, the teacher\n\t\t   desk through the sandbox. Hand-nudging coordinates fixes today's\n\t\t   collision and quietly creates tomorrow's, so this resolves them\n\t\t   by measurement instead.\n\n\t\t   It runs BEFORE measureFrames(), which reads live bounding boxes,\n\t\t   so each station camera re-aims itself at wherever its subject\n\t\t   ended up. Two rules keep it from making things worse: props that\n\t\t   were authored against a wall stay welded to that wall, and two\n\t\t   props that never share a height are left alone \u2014 a ceiling\n\t\t   projector overlapping a desk in plan view is not a collision. *\/\n\t\tfunction separateProps(){\n\t\t\tvar GAP=0.30;                       \/* clear air between neighbours *\/\n\t\t\tparts.forEach(function(p){\n\t\t\t\tp.updateMatrixWorld(true);\n\t\t\t\tp.userData.pinX = Math.abs(p.position.x) > RX-0.40;\n\t\t\t\tp.userData.pinZ = Math.abs(p.position.z) > RZ-0.40;\n\t\t\t});\n\t\t\tfor(var pass=0; pass<30; pass++){\n\t\t\t\tvar boxes=parts.map(function(p){\n\t\t\t\t\tp.updateMatrixWorld(true);\n\t\t\t\t\treturn new THREE.Box3().setFromObject(p);\n\t\t\t\t});\n\t\t\t\tvar moved=false;\n\t\t\t\tfor(var a=0;a<parts.length;a++){\n\t\t\t\t\tfor(var b=a+1;b<parts.length;b++){\n\t\t\t\t\t\tvar A=boxes[a], B=boxes[b];\n\t\t\t\t\t\tif(A.isEmpty()||B.isEmpty()) continue;\n\t\t\t\t\t\t\/* no shared height means no visible clash *\/\n\t\t\t\t\t\tif(Math.min(A.max.y,B.max.y)-Math.max(A.min.y,B.min.y) <= 0.02) continue;\n\t\t\t\t\t\tvar ix=Math.min(A.max.x,B.max.x)-Math.max(A.min.x,B.min.x)+GAP;\n\t\t\t\t\t\tvar iz=Math.min(A.max.z,B.max.z)-Math.max(A.min.z,B.min.z)+GAP;\n\t\t\t\t\t\tif(ix<=0||iz<=0) continue;\n\t\t\t\t\t\tvar pa=parts[a], pb=parts[b];\n\t\t\t\t\t\tvar ca=A.getCenter(new THREE.Vector3()), cb=B.getCenter(new THREE.Vector3());\n\t\t\t\t\t\t\/* separate along whichever axis needs the least travel,\n\t\t\t\t\t\t   unless that axis is pinned for both of them *\/\n\t\t\t\t\t\tvar useX = (ix<iz);\n\t\t\t\t\t\tif(useX && pa.userData.pinX && pb.userData.pinX) useX=false;\n\t\t\t\t\t\telse if(!useX && pa.userData.pinZ && pb.userData.pinZ) useX=true;\n\t\t\t\t\t\tvar freeA = useX ? !pa.userData.pinX : !pa.userData.pinZ;\n\t\t\t\t\t\tvar freeB = useX ? !pb.userData.pinX : !pb.userData.pinZ;\n\t\t\t\t\t\tif(!freeA && !freeB) continue;      \/* both welded: leave it *\/\n\t\t\t\t\t\tvar pen = useX ? ix : iz;\n\t\t\t\t\t\tvar dir = useX ? (ca.x<cb.x?-1:1) : (ca.z<cb.z?-1:1);\n\t\t\t\t\t\tvar share = (freeA&&freeB) ? 0.5 : 1.0;\n\t\t\t\t\t\tvar step = pen*share*0.85;\n\t\t\t\t\t\tif(useX){\n\t\t\t\t\t\t\tif(freeA) pa.position.x += dir*step;\n\t\t\t\t\t\t\tif(freeB) pb.position.x -= dir*step;\n\t\t\t\t\t\t}else{\n\t\t\t\t\t\t\tif(freeA) pa.position.z += dir*step;\n\t\t\t\t\t\t\tif(freeB) pb.position.z -= dir*step;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tmoved=true;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tparts.forEach(function(p){\n\t\t\t\t\tif(!p.userData.pinX) p.position.x=Math.max(-RX+0.55, Math.min(RX-0.55, p.position.x));\n\t\t\t\t\tif(!p.userData.pinZ) p.position.z=Math.max(-RZ+0.55, Math.min(RZ-0.55, p.position.z));\n\t\t\t\t});\n\t\t\t\tif(!moved) break;\n\t\t\t}\n\t\t\tparts.forEach(function(p){ p.updateMatrixWorld(true); });\n\t\t}\n\t\tseparateProps();\n\n\t\tvar markers=[];\n\t\tvar stops=[\n\t\t\t{pos:[0,6.2,13.5],  look:[0,1,0]},\n\t\t\t{pos:[0,1.9,-1.9],  look:[0,1.95,-5.4],  marker:[0,3.35,-5.3]},\n\t\t\t{pos:[-4.4,2,-2.8], look:[-7.9,1.9,-2.8],marker:[-7.5,3.2,-2.8]},\n\t\t\t{pos:[2.2,3.3,6],   look:[2.2,.1,2.6],   marker:[2.2,1.45,2.6]},\n\t\t\t{pos:[-2.2,2.1,-1.6],look:[-4.6,.85,-3.9],marker:[-4.6,2.4,-3.9]},\n\t\t\t{pos:[-2.7,1.8,5.3],look:[-5.6,.75,3.4], marker:[-5.6,1.6,3.4]},\n\t\t\t{pos:[2.2,2,-1.9],  look:[4.9,1.1,-4.1], marker:[4.9,2.05,-4.05]},\n\t\t\t{pos:[3.4,1.9,-1.5],look:[6.3,1.15,-3.8],marker:[6.15,2.2,-3.75]},\n\t\t\t{pos:[3.4,1.8,.8],  look:[6.9,1.15,.8],  marker:[6.6,2,.8]},\n\t\t\t{pos:[-.6,1.85,-1.9],look:[-3.4,1.1,-4.3],marker:[-3.3,2,-4.2]},\n\t\t\t{pos:[3.4,1.9,2.1], look:[6.9,1.4,4.3],  marker:[6.65,2.75,4.3]},\n\t\t\t{pos:[-4.7,1.7,3.3],look:[-7.9,1.45,3.3],marker:[-7.5,2.1,3.3]},\n\t\t\t{pos:[-4.3,1.7,1.3],look:[-7,.9,2.1],    marker:[-6.8,1.85,2.1]},\n\t\t\t{pos:[-4.3,1.6,4.6],look:[-6.9,.95,4.9], marker:[-6.7,2,4.9]}\n\t\t];\n\t\t\/* Phase 3: markers are now DOM elements (glow, connector line, label,\n\t\t   keyboard focus). We keep an invisible 3D anchor list for projection. *\/\n\t\tvar hotDefs=[];\n\t\tstops.forEach(function(s,i){\n\t\t\tif(!s.marker) return;\n\t\t\tvar anchor=new THREE.Object3D(); anchor.position.fromArray(s.marker); scene.add(anchor);\n\t\t\tmarkers.push({ position:anchor.position, userData:{stop:i}, material:{opacity:1}, scale:{set:function(){}} });\n\t\t\thotDefs.push({ i:i, pos:anchor.position, label:('0'+(i+1)).slice(-2) });\n\t\t});\n\n\t\t\/* moving spotlight that follows the active station *\/\n\t\tvar spot=new THREE.SpotLight(0xd6c9ff,0,16,Math.PI\/7,.45,1.4);\n\t\tspot.position.set(0,6,0); scene.add(spot); scene.add(spot.target);\n\n\t\tvar focusRing=new THREE.Mesh(new THREE.RingGeometry(.55,.62,44),\n\t\t\tnew THREE.MeshBasicMaterial({color:GLOW,transparent:true,opacity:0,side:THREE.DoubleSide}));\n\t\tfocusRing.rotation.x=-Math.PI\/2; focusRing.position.y=.09; scene.add(focusRing);\n\n\t\t\/* ---- Phase 3 wiring: focus targets keyed by station index ---- *\/\n\t\tvar FOCUS_TARGETS = {\n\t\t\t1:panel, 2:proj, 3:rm, 4:sb, 5:vr, 6:rb, 7:sl, 8:zc, 9:td, 10:sg, 11:att, 12:cc, 13:un\n\t\t};\n\t\tvar glowRing=new THREE.Mesh(new THREE.RingGeometry(.95,1.12,56),\n\t\t\tnew THREE.MeshBasicMaterial({color:0x9f8cff,transparent:true,opacity:0,side:THREE.DoubleSide}));\n\t\tglowRing.rotation.x=-Math.PI\/2; glowRing.visible=false; scene.add(glowRing);\n\t\tMQR.Focus.init(scene, ambL, FOCUS_TARGETS, glowRing, [ambL,hemiL,sun,fillL,rimL]);\n\n\t\t\/* optional GLB upgrade \u2014 silently ignored when no model file exists *\/\n\t\tif(MQR.Models.init('https:\/\/digitalmqr.com\/wp-content\/themes\/mqr-theme\/assets\/models\/')){\n\t\t\t\/* station index -> model file. A missing file is ignored silently,\n\t\t\t   so entries can be listed before the model exists. *\/\n\t\t\t\/* Explicit size beats derived scale. Some stand-ins are a whole\n\t\t\t   bench and some are a bare wall plate, so matching their bounding\n\t\t\t   box either shrinks a panel to a stamp or inflates a projector to\n\t\t\t   three metres. w = intended width in metres; anchor 'floor' sits\n\t\t\t   the model on the stand-in's base, 'center' keeps it where it is\n\t\t\t   (wall-mounted things).\n\t\t\t   Station 6 (robotics) is left out on purpose: its stand-in is a\n\t\t\t   whole bench of equipment rather than one device. *\/\n\t\t\tvar GLB={\n\t\t\t\t1: {f:'interactive-smart-panels',   w:2.60, anchor:'center'},\n\t\t\t\t2: {f:'interactive-projectors',     w:0.62, anchor:'center'},\n\t\t\t\t3: {f:'interactive-floor',          w:2.00, anchor:'floor'},\n\t\t\t\t4: {f:'interactive-sandbox',        w:1.45, anchor:'floor'},\n\t\t\t\t5: {f:'vr-ar-solutions',            w:0.62, anchor:'floor'},\n\t\t\t\t7: {f:'virtual-science-labs',       w:1.95, anchor:'floor'},\n\t\t\t\t8: {f:'computing-solutions',        w:1.45, anchor:'floor'},\n\t\t\t\t9: {f:'teacher-station',            w:1.85, anchor:'floor'},\n\t\t\t\t10:{f:'digital-signage',            w:0.78, anchor:'floor'},\n\t\t\t\t11:{f:'face-fingerprint-devices',   w:0.30, anchor:'center'},\n\t\t\t\t12:{f:'charging-carts',             w:0.72, anchor:'floor'},\n\t\t\t\t13:{f:'school-uniform',             w:0.50, anchor:'floor'}\n\t\t\t};\n\t\t\tObject.keys(GLB).forEach(function(k){ MQR.Models.swap(FOCUS_TARGETS[k], GLB[k], scene); });\n\t\t}\n\n\n\t\t\/* ============================================================\n\t\t   PHASE 4 \u2014 guided tour \u00b7 floor map \u00b7 AR overlays \u00b7 TRANSFORM\n\t\t   Every module is self-contained and fails silently if its DOM\n\t\t   is missing, so the core tour keeps working regardless.\n\t\t   ============================================================ *\/\n\t\tvar root=document.getElementById('arx');\n\n\t\t\/* ---------- AR-style overlay anchored to the active station ----------\n\t\t   One projected DOM node, three words cycled on a rAF clock. *\/\n\t\tMQR.AR=(function(){\n\t\t\tvar el,inner,word,anchor=null,seq=[],idx=0,last=0,on=false;\n\t\t\tvar WORDS={\n\t\t\t\t1:['TOUCH','TEACH','SAVE'], 2:['PROJECT','INTERACT','ENGAGE'],\n\t\t\t\t3:['MOVE','PLAY','LEARN'],  4:['SHAPE','SCAN','SIMULATE'],\n\t\t\t\t5:['IMMERSIVE LEARNING'],   6:['DESIGN','PRINT','CREATE','PROGRAM','TEST','AUTOMATE'],\n\t\t\t\t7:['SIMULATE','EXPERIMENT','ANALYZE'], 8:['ONE SERVER','MANY SEATS'],\n\t\t\t\t9:['CONTROL','MONITOR','GUIDE'], 10:['ANNOUNCE','SCHEDULE','CELEBRATE'],\n\t\t\t\t11:['SCAN','VERIFY','ENTER'], 12:['CHARGE','STORE','SECURE'],\n\t\t\t\t13:['MEASURE','TAILOR','DELIVER']\n\t\t\t};\n\t\t\treturn {\n\t\t\t\tinit:function(node){ el=node; if(!el) return;\n\t\t\t\t\tinner=el.querySelector('.arxARin'); word=el.querySelector('b'); },\n\t\t\t\tset:function(i){\n\t\t\t\t\tif(!el) return;\n\t\t\t\t\tvar w=WORDS[i];\n\t\t\t\t\tif(!w || !stops[i] || !stops[i].marker){ this.clear(); return; }\n\t\t\t\t\tseq=w; idx=0; last=0; on=true; anchor=stops[i].marker;\n\t\t\t\t\tword.textContent=seq[0];\n\t\t\t\t\tword.classList.remove('flip'); void word.offsetWidth; word.classList.add('flip');\n\t\t\t\t\tel.classList.add('on');\n\t\t\t\t},\n\t\t\t\tclear:function(){ if(!el) return; on=false; anchor=null; el.classList.remove('on'); },\n\t\t\t\t\/* called from the render loop *\/\n\t\t\t\ttick:function(now,project){\n\t\t\t\t\tif(!on||!el||!anchor) return;\n\t\t\t\t\tif(seq.length>1 && now-last>2000){\n\t\t\t\t\t\tlast=now; idx=(idx+1)%seq.length;\n\t\t\t\t\t\tword.textContent=seq[idx];\n\t\t\t\t\t\tword.classList.remove('flip'); void word.offsetWidth; word.classList.add('flip');\n\t\t\t\t\t}\n\t\t\t\t\tvar p=project(anchor);\n\t\t\t\t\tif(!p){ el.style.opacity=0; return; }\n\t\t\t\t\tel.style.opacity='';\n\t\t\t\t\t\/* keep the badge on screen: flip it under the marker when there\n\t\t\t\t\t   is no room above, and never let it run past the side edges. *\/\n\t\t\t\t\tvar W=el.parentNode.clientWidth, H=el.parentNode.clientHeight;\n\t\t\t\t\tvar down = (p.y-34) < 96;\n\t\t\t\t\tvar y = down ? Math.min(p.y+34, H-26) : (p.y-34);\n\t\t\t\t\tvar x = Math.max(84, Math.min(W-84, p.x));\n\t\t\t\t\tel.classList.toggle('down', down);\n\t\t\t\t\tel.style.transform='translate('+x+'px,'+y+'px)';\n\t\t\t\t}\n\t\t\t};\n\t\t})();\n\n\n\n\t\t\/* ---------- WebXR \"view in your room\" -------------------------------\n\t\t   Only ever offered when the device genuinely reports immersive-ar\n\t\t   support, so the button is absent rather than broken elsewhere.\n\t\t   In session the classroom becomes a tabletop model you can walk\n\t\t   around; the room shell is hidden so it sits in your real space. *\/\n\t\tMQR.XR=(function(){\n\t\t\tvar session=null, prevScale=1, prevPos=null, shell=[], note=null;\n\t\t\treturn {\n\t\t\t\t\/* resolves true only when a real AR session is possible *\/\n\t\t\t\tprobe:function(){\n\t\t\t\t\tif(!navigator.xr || !navigator.xr.isSessionSupported) return Promise.resolve(false);\n\t\t\t\t\treturn navigator.xr.isSessionSupported('immersive-ar').catch(function(){ return false; });\n\t\t\t\t},\n\t\t\t\tenter:function(ctx){\n\t\t\t\t\tif(session) return;\n\t\t\t\t\tvar opts={ requiredFeatures:['local-floor'],\n\t\t\t\t\t\toptionalFeatures:['dom-overlay','hit-test'],\n\t\t\t\t\t\tdomOverlay:{ root: ctx.stage } };\n\t\t\t\t\tnavigator.xr.requestSession('immersive-ar', opts).then(function(s){\n\t\t\t\t\t\tsession=s;\n\t\t\t\t\t\tctx.renderer.xr.enabled=true;\n\t\t\t\t\t\tctx.renderer.xr.setReferenceSpaceType('local-floor');\n\t\t\t\t\t\treturn ctx.renderer.xr.setSession(s);\n\t\t\t\t\t}).then(function(){\n\t\t\t\t\t\t\/* shrink the classroom to a model on the floor in front of you *\/\n\t\t\t\t\t\tprevScale=ctx.world.scale.x;\n\t\t\t\t\t\tprevPos=ctx.world.position.clone();\n\t\t\t\t\t\tctx.world.scale.setScalar(0.055);\n\t\t\t\t\t\tctx.world.position.set(0, 0, -1.15);\n\t\t\t\t\t\tshell=ctx.shell.filter(function(o){ return o && o.visible; });\n\t\t\t\t\t\tshell.forEach(function(o){ o.visible=false; });\n\t\t\t\t\t\tctx.onEnter();\n\t\t\t\t\t\tctx.renderer.setAnimationLoop(ctx.frame);\n\t\t\t\t\t\tsession.addEventListener('end', function(){\n\t\t\t\t\t\t\tctx.renderer.setAnimationLoop(null);\n\t\t\t\t\t\t\tctx.renderer.xr.enabled=false;\n\t\t\t\t\t\t\tctx.world.scale.setScalar(prevScale);\n\t\t\t\t\t\t\tif(prevPos) ctx.world.position.copy(prevPos);\n\t\t\t\t\t\t\tshell.forEach(function(o){ o.visible=true; });\n\t\t\t\t\t\t\tsession=null;\n\t\t\t\t\t\t\tctx.onExit();\n\t\t\t\t\t\t});\n\t\t\t\t\t}).catch(function(err){\n\t\t\t\t\t\tctx.onFail(err && err.message ? err.message : '');\n\t\t\t\t\t});\n\t\t\t\t},\n\t\t\t\texit:function(){ if(session) session.end(); },\n\t\t\t\tactive:function(){ return !!session; }\n\t\t\t};\n\t\t})();\n\n\t\t\/* ---------- sound ----------------------------------------------\n\t\t   Everything is synthesised in the browser: no audio files, no\n\t\t   network cost, no licensing. Silent until the visitor asks for it,\n\t\t   because nobody wants a website that starts making noise at them. *\/\n\t\tMQR.Sound=(function(){\n\t\t\tvar actx=null, master=null, roomGain=null, on=false, ready=false;\n\n\t\t\tfunction build(){\n\t\t\t\tvar AC = window.AudioContext || window.webkitAudioContext;\n\t\t\t\tif(!AC) return false;\n\t\t\t\ttry{ actx = new AC(); }catch(e){ return false; }\n\t\t\t\tmaster = actx.createGain(); master.gain.value = 0; master.connect(actx.destination);\n\n\t\t\t\t\/* room tone: brown noise through a low-pass, breathing slowly *\/\n\t\t\t\tvar len = Math.floor(actx.sampleRate*3);\n\t\t\t\tvar buf = actx.createBuffer(1, len, actx.sampleRate), d = buf.getChannelData(0), last = 0;\n\t\t\t\tfor(var i=0;i<len;i++){\n\t\t\t\t\tvar w = Math.random()*2-1;\n\t\t\t\t\tlast = (last + 0.02*w)\/1.02;\n\t\t\t\t\td[i] = last*3.2;\n\t\t\t\t}\n\t\t\t\tvar src = actx.createBufferSource(); src.buffer = buf; src.loop = true;\n\t\t\t\tvar lp = actx.createBiquadFilter(); lp.type='lowpass'; lp.frequency.value=380; lp.Q.value=.5;\n\t\t\t\troomGain = actx.createGain(); roomGain.gain.value = .085;\n\t\t\t\tsrc.connect(lp); lp.connect(roomGain); roomGain.connect(master); src.start();\n\n\t\t\t\tvar lfo = actx.createOscillator(); lfo.frequency.value = .055;\n\t\t\t\tvar lfoG = actx.createGain(); lfoG.gain.value = .03;\n\t\t\t\tlfo.connect(lfoG); lfoG.connect(roomGain.gain); lfo.start();\n\n\t\t\t\tready = true; return true;\n\t\t\t}\n\n\t\t\t\/* one short sine with a soft envelope *\/\n\t\t\tfunction note(freq, dur, vol, delay, type){\n\t\t\t\tif(!on||!ready) return;\n\t\t\t\tvar t0 = actx.currentTime + (delay||0);\n\t\t\t\tvar o = actx.createOscillator(), g = actx.createGain();\n\t\t\t\to.type = type || 'sine'; o.frequency.setValueAtTime(freq, t0);\n\t\t\t\tg.gain.setValueAtTime(0, t0);\n\t\t\t\tg.gain.linearRampToValueAtTime(vol, t0+0.012);\n\t\t\t\tg.gain.exponentialRampToValueAtTime(0.0001, t0+dur);\n\t\t\t\to.connect(g); g.connect(master); o.start(t0); o.stop(t0+dur+0.02);\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\tisOn:function(){ return on; },\n\t\t\t\t\/* must be called from a real user gesture *\/\n\t\t\t\ttoggle:function(){\n\t\t\t\t\tif(!ready && !build()) return false;\n\t\t\t\t\ton = !on;\n\t\t\t\t\tif(on && actx.state === 'suspended') actx.resume();\n\t\t\t\t\tmaster.gain.cancelScheduledValues(actx.currentTime);\n\t\t\t\t\tmaster.gain.linearRampToValueAtTime(on ? .5 : 0, actx.currentTime + .4);\n\t\t\t\t\tif(on) note(660, .18, .12, .05);\n\t\t\t\t\ttry{ sessionStorage.setItem('mqrSnd', on ? '1' : '0'); }catch(e){}\n\t\t\t\t\treturn on;\n\t\t\t\t},\n\t\t\t\ttap:function(){  note(880, .06, .05, 0, 'triangle'); },\n\t\t\t\tmove:function(){ note(392, .22, .07); note(587, .26, .05, .07); },\n\t\t\t\tland:function(){ note(784, .30, .07, 0); note(1046, .34, .045, .09); }\n\t\t\t};\n\t\t})();\n\n\t\t\/* ---------- interactive floor plan ----------\n\t\t   Real coordinates: every dot maps to the station's look-at point,\n\t\t   so clicking it flies the camera to exactly that spot. *\/\n\t\tMQR.Map=(function(){\n\t\t\tvar wrap,plan,me,pts=[],open=false;\n\t\t\tvar X0=-8.6,X1=8.6,Z0=-6.2,Z1=6.2;                 \/* room bounds (metres) *\/\n\t\t\tfunction px(x){ return ((x-X0)\/(X1-X0))*100; }      \/* \u2192 % across  *\/\n\t\t\tfunction pz(z){ return ((z-Z0)\/(Z1-Z0))*100; }      \/* \u2192 % down    *\/\n\t\t\treturn {\n\t\t\t\tinit:function(node,labels,pick){\n\t\t\t\t\twrap=node; if(!wrap) return;\n\t\t\t\t\tplan=wrap.querySelector('#arxMapPlan'); me=wrap.querySelector('#arxMapMe');\n\t\t\t\t\tstops.forEach(function(s,i){\n\t\t\t\t\t\tif(i===0) return;                        \/* 0 is the overview camera *\/\n\t\t\t\t\t\tvar b=document.createElement('button');\n\t\t\t\t\t\tb.type='button'; b.className='pt'; b.dataset.i=i; b.setAttribute('data-n',i);\n\t\t\t\t\t\tb.title=labels[i]||''; b.setAttribute('aria-label',labels[i]||('Station '+i));\n\t\t\t\t\t\tb.style.left=px(s.look[0])+'%'; b.style.top=pz(s.look[2])+'%';\n\t\t\t\t\t\tb.addEventListener('click',function(){ pick(i); });\n\t\t\t\t\t\tplan.appendChild(b); pts.push(b);\n\t\t\t\t\t});\n\t\t\t\t},\n\t\t\t\ttoggle:function(v){\n\t\t\t\t\tif(!wrap) return open;\n\t\t\t\t\topen=(v===undefined)?!open:!!v;\n\t\t\t\t\tif(open){ wrap.hidden=false; void wrap.offsetWidth; wrap.classList.add('on'); }\n\t\t\t\t\telse { wrap.classList.remove('on'); setTimeout(function(){ if(!open) wrap.hidden=true; },300); }\n\t\t\t\t\treturn open;\n\t\t\t\t},\n\t\t\t\tisOpen:function(){ return open; },\n\t\t\t\tsetActive:function(i){ pts.forEach(function(b){ b.classList.toggle('on', +b.dataset.i===i); }); },\n\t\t\t\t\/* live \"you are here\" dot *\/\n\t\t\t\tfollow:function(v){ if(!me||!open) return;\n\t\t\t\t\tme.style.left=px(v.x)+'%'; me.style.top=pz(v.z)+'%'; }\n\t\t\t};\n\t\t})();\n\n\t\t\/* ---------- guided tour ----------\n\t\t   Six narrated stops. Advances ONLY when the visitor presses Next. *\/\n\t\tMQR.Guide=(function(){\n\t\t\tvar box,txt,step,bar,bPrev,bNext,bExit,run=false,k=0,go=null;\n\t\t\tvar STOPS=[\n\t\t\t\t{i:1,  t:\"We start at the front of the room. The interactive panel replaces the whiteboard \\u2014 4K multi-touch, wireless casting, and every lesson saved and shareable.\"},\n\t\t\t\t{i:6,  t:\"The maker corner. Students design on screen, print in 3D, then program and test robots \\u2014 the full design-build-test loop in one bench.\"},\n\t\t\t\t{i:5,  t:\"The VR corner takes the class inside the subject \\u2014 walk through the human body, ancient Rome or a jet engine, then step back out and discuss it.\"},\n\t\t\t\t{i:7,  t:\"The science lab runs every experiment in simulation first: zero risk, unlimited repeats, and real glassware for the fundamentals.\"},\n\t\t\t\t{i:3,  t:\"The interactive floor turns movement into learning. Younger classes play their way through numeracy, language and coordination.\"},\n\t\t\t\t{i:9,  t:\"It all comes back to the teacher station \\u2014 one place to control the panel, the lab, attendance and the screens across the school.\"}\n\t\t\t];\n\t\t\tfunction paint(){\n\t\t\t\tstep.textContent=('0'+(k+1)).slice(-2)+' \/ '+('0'+STOPS.length).slice(-2);\n\t\t\t\ttxt.textContent=STOPS[k].t;\n\t\t\t\tbar.style.width=(((k+1)\/STOPS.length)*100)+'%';\n\t\t\t\tbPrev.disabled=(k===0);\n\t\t\t\tvar rtl = (document.documentElement.getAttribute('dir')||'') === 'rtl';\n\t\t\t\tbNext.textContent = (k===STOPS.length-1)\n\t\t\t\t\t? \"Finish\"\t\t\t\t\t: \"Next\"+' '+(rtl?'\\u2190':'\\u2192');\n\t\t\t}\n\t\t\treturn {\n\t\t\t\tinit:function(node,goFn){\n\t\t\t\t\tbox=node; go=goFn; if(!box) return;\n\t\t\t\t\ttxt=box.querySelector('#arxGText'); step=box.querySelector('#arxGStep');\n\t\t\t\t\tbar=box.querySelector('#arxGBar');\n\t\t\t\t\tbPrev=box.querySelector('#arxGPrev'); bNext=box.querySelector('#arxGNext');\n\t\t\t\t\tbExit=box.querySelector('#arxGExit');\n\t\t\t\t\tbPrev.addEventListener('click',function(){ if(k>0){ k--; paint(); go(STOPS[k].i); } });\n\t\t\t\t\tbNext.addEventListener('click',function(){\n\t\t\t\t\t\tif(k<STOPS.length-1){ k++; paint(); go(STOPS[k].i); } else MQR.Guide.stop();\n\t\t\t\t\t});\n\t\t\t\t\tbExit.addEventListener('click',function(){ MQR.Guide.stop(); });\n\t\t\t\t},\n\t\t\t\tactive:function(){ return run; },\n\t\t\t\tstart:function(){\n\t\t\t\t\tif(!box||run) return;\n\t\t\t\t\trun=true; k=0;\n\t\t\t\t\tstage.classList.add('arx-guiding');\n\t\t\t\t\tbox.hidden=false; void box.offsetWidth; box.classList.add('on');\n\t\t\t\t\tplayIc.innerHTML='&#10005;';\n\t\t\t\t\tdocument.getElementById('arxPlay').setAttribute('aria-label',\n\t\t\t\t\t\t\"Exit the guided tour\");\n\t\t\t\t\tpaint(); go(STOPS[0].i);\n\t\t\t\t},\n\t\t\t\tstop:function(){\n\t\t\t\t\tif(!box||!run) return;\n\t\t\t\t\trun=false;\n\t\t\t\t\tstage.classList.remove('arx-guiding');\n\t\t\t\t\tbox.classList.remove('on');\n\t\t\t\t\tsetTimeout(function(){ if(!run) box.hidden=true; },400);\n\t\t\t\t\tplayIc.innerHTML='&#9654;';\n\t\t\t\t\tdocument.getElementById('arxPlay').setAttribute('aria-label',\n\t\t\t\t\t\t\"Start the guided tour\");\n\t\t\t\t},\n\t\t\t\t\/* keeps the ribbon honest if the visitor jumps somewhere else *\/\n\t\t\t\tonArrive:function(i){\n\t\t\t\t\tif(!run) return;\n\t\t\t\t\tif(STOPS[k].i!==i) MQR.Guide.stop();\n\t\t\t\t}\n\t\t\t};\n\t\t})();\n\n\t\t\/* ---------- TRANSFORM CLASSROOM ----------\n\t\t   The set-piece: strip the room back to a plain classroom, then rebuild\n\t\t   it stage by stage. Driven by one cancellable rAF timeline. *\/\n\t\tMQR.Transform=(function(){\n\t\t\tvar busy=false,t0=0,raf=null,capB,capS,capBox,ctx=null,stepAt=-1,guard=null,base=null;\n\t\t\tvar SCRIPT=[];\n\t\t\tfunction caption(a,b){\n\t\t\t\tcapB.classList.remove('on'); capS.classList.remove('on');\n\t\t\t\tsetTimeout(function(){\n\t\t\t\t\tcapB.textContent=a; capS.textContent=b||'';\n\t\t\t\t\tcapB.classList.add('on'); if(b) capS.classList.add('on');\n\t\t\t\t},220);\n\t\t\t}\n\t\t\tfunction fade(){ capB.classList.remove('on'); capS.classList.remove('on'); }\n\t\t\tfunction lerp(a,b,t){ return a+(b-a)*t; }\n\t\t\tfunction frame(now){\n\t\t\t\tif(!busy) return;\n\t\t\t\tvar e=(now-t0)\/1000;                       \/* seconds since start *\/\n\t\t\t\tfor(var s=0;s<SCRIPT.length;s++){\n\t\t\t\t\tif(e>=SCRIPT[s].at && stepAt<s){ stepAt=s; SCRIPT[s].fn(); }\n\t\t\t\t}\n\t\t\t\t\/* continuous light ramp between 1.2s and 8.5s *\/\n\t\t\t\tvar p=Math.max(0,Math.min(1,(e-1.2)\/7.3));\n\t\t\t\tif(BAKED_MAT && BAKED_MAT.userData.all){\n\t\t\t\t\tvar k=lerp(.22,1,p);\n\t\t\t\t\tvar all=BAKED_MAT.userData.all;\n\t\t\t\t\tfor(var nm in all){ var mm=all[nm], b=mm.userData.base;\n\t\t\t\t\t\tmm.color.setRGB(b.r*k, b.g*k, b.b*k); }\n\t\t\t\t}\n\t\t\t\tctx.amb.intensity =lerp(base.amb*.24, base.amb, p);\n\t\t\t\tctx.sun.intensity =lerp(base.sun*.20, base.sun, p);\n\t\t\t\tctx.fill.intensity=lerp(base.fill*.10,base.fill,p);\n\t\t\t\t\/* staggered device build-up from 2.6s *\/\n\t\t\t\tfor(var i=0;i<ctx.parts.length;i++){\n\t\t\t\t\tvar g=ctx.parts[i], d=e-2.6-i*.16;\n\t\t\t\t\tvar v=d<=0?0:Math.min(1,d\/.55);\n\t\t\t\t\tg.scale.setScalar(Math.max(.001, v*v*(3-2*v)));   \/* smoothstep *\/\n\t\t\t\t}\n\t\t\t\t\/* the grounding shadows follow the objects back in *\/\n\t\t\t\tsetContactOpacity(0.9*Math.max(0,Math.min(1,(e-2.6)\/3.2)));\n\t\t\t\tif(e>10.4){ finish(); return; }\n\t\t\t\traf=requestAnimationFrame(frame);\n\t\t\t}\n\t\t\tfunction finish(){\n\t\t\t\tbusy=false; stepAt=-1;\n\t\t\t\tif(guard){ clearTimeout(guard); guard=null; }\n\t\t\t\tif(raf){ cancelAnimationFrame(raf); raf=null; }\n\t\t\t\tctx.parts.forEach(function(g){ g.scale.setScalar(1); });\n\t\t\t\tsetContactOpacity(0.9);\n\t\t\t\tctx.amb.intensity=base.amb; ctx.sun.intensity=base.sun; ctx.fill.intensity=base.fill;\n\t\t\t\tif(BAKED_MAT && BAKED_MAT.userData.all){ var A=BAKED_MAT.userData.all;\n\t\t\t\t\tfor(var n2 in A) A[n2].color.copy(A[n2].userData.base); }\n\t\t\t\tfade();\n\t\t\t\tsetTimeout(function(){\n\t\t\t\t\tcapBox.hidden=true;\n\t\t\t\t\tctx.stage.classList.remove('arx-cine');\n\t\t\t\t\tctx.done();\n\t\t\t\t},700);\n\t\t\t}\n\t\t\treturn {\n\t\t\t\tinit:function(c){\n\t\t\t\t\tctx=c;\n\t\t\t\t\t\/* remember the rig's real levels so the sequence always lands\n\t\t\t\t\t   back exactly where the room started *\/\n\t\t\t\t\tbase={ amb:c.amb.intensity, sun:c.sun.intensity, fill:c.fill.intensity };\n\t\t\t\t\tcapBox=document.getElementById('arxTrans');\n\t\t\t\t\tcapB=document.getElementById('arxTransT'); capS=document.getElementById('arxTransS');\n\t\t\t\t\tvar L=c.labels;\n\t\t\t\t\tSCRIPT=[\n\t\t\t\t\t\t{at:0.15,fn:function(){ caption(L.a,L.aSub); }},\n\t\t\t\t\t\t{at:2.30,fn:function(){ caption(L.b,L.bSub); }},\n\t\t\t\t\t\t{at:4.30,fn:function(){ caption(L.c,L.cSub); }},\n\t\t\t\t\t\t{at:6.30,fn:function(){ caption(L.d,L.dSub); }},\n\t\t\t\t\t\t{at:8.30,fn:function(){ caption(L.e,L.eSub); }}\n\t\t\t\t\t];\n\t\t\t\t},\n\t\t\t\tbusy:function(){ return busy; },\n\t\t\t\trun:function(){\n\t\t\t\t\tif(busy||!ctx) return;\n\t\t\t\t\tbusy=true; stepAt=-1;\n\t\t\t\t\tctx.stage.classList.add('arx-cine');\n\t\t\t\tif(typeof DOF!=='undefined') DOF.target(0);   \/* the set-piece is a wide shot *\/\n\t\t\t\t\tcapBox.hidden=false;\n\t\t\t\t\tctx.before();                                   \/* camera to overview, UI away *\/\n\t\t\t\t\t\/* strip the room back to \"traditional\" *\/\n\t\t\t\t\tctx.parts.forEach(function(g){ g.scale.setScalar(.001); });\n\t\t\t\t\tsetContactOpacity(0);\n\t\t\t\t\tctx.amb.intensity=base.amb*.24; ctx.sun.intensity=base.sun*.20; ctx.fill.intensity=base.fill*.10;\n\t\t\t\t\tif(BAKED_MAT && BAKED_MAT.userData.all){ var A0=BAKED_MAT.userData.all;\n\t\t\t\t\t\tfor(var n0 in A0){ var b0=A0[n0].userData.base;\n\t\t\t\t\t\t\tA0[n0].color.setRGB(b0.r*.22,b0.g*.22,b0.b*.22); } }\n\t\t\t\t\tt0=performance.now();\n\t\t\t\t\traf=requestAnimationFrame(frame);\n\t\t\t\t\t\/* wall-clock backstop: rAF is suspended in a hidden tab, so a\n\t\t\t\t\t   timer guarantees the sequence always ends and the UI returns. *\/\n\t\t\t\t\tguard=setTimeout(function(){ if(busy) finish(); }, 12000);\n\t\t\t\t},\n\t\t\t\tcancel:function(){ if(busy) finish(); }\n\t\t\t};\n\t\t})();\n\n\n\n\t\t\/* Is the straight line from `from` to `c` free of anything except the\n\t\t   product itself, the floor and see-through helpers? *\/\n\t\tvar _los=new THREE.Raycaster();\n\t\tfunction shotIsClear(from, c, group){\n\t\t\tvar v=c.clone().sub(from), dist=v.length();\n\t\t\tif(dist<0.05) return true;\n\t\t\t_los.set(from, v.normalize());\n\t\t\t_los.far=dist*0.94;                   \/* stop just short of the product *\/\n\t\t\tvar hits;\n\t\t\ttry{ hits=_los.intersectObject(world, true); }catch(e){ return true; }\n\t\t\tfor(var h=0;h<hits.length;h++){\n\t\t\t\tvar o=hits[h].object, p=o, mine=false;\n\t\t\t\twhile(p){ if(p===group){ mine=true; break; } p=p.parent; }\n\t\t\t\tif(mine) continue;                \/* the product itself is fine *\/\n\t\t\t\tif(o===floor) continue;           \/* so is the floor slab        *\/\n\t\t\t\tvar m=o.material;\n\t\t\t\tif(m && !Array.isArray(m) && m.transparent && (m.opacity===undefined || m.opacity<.65)) continue;\n\t\t\t\treturn false;                     \/* something solid is in the way *\/\n\t\t\t}\n\t\t\treturn true;\n\t\t}\n\n\t\t\/* Every station's bounding sphere, measured once so the scoring loop\n\t\t   below is arithmetic rather than scene traversal. *\/\n\t\tvar COMPETITORS=[];\n\t\t(function measureCompetitors(){\n\t\t\tvar keys=Object.keys(FOCUS_TARGETS);\n\t\t\tfor(var n=0;n<keys.length;n++){\n\t\t\t\tvar g=FOCUS_TARGETS[keys[n]]; if(!g) continue;\n\t\t\t\tvar b=new THREE.Box3().setFromObject(g); if(b.isEmpty()) continue;\n\t\t\t\tCOMPETITORS.push({ g:g,\n\t\t\t\t\tc:b.getCenter(new THREE.Vector3()),\n\t\t\t\t\tr:b.getSize(new THREE.Vector3()).length()*.5 });\n\t\t\t}\n\t\t})();\n\n\t\t\/* How much of this shot is stolen by other products? Each competitor\n\t\t   contributes its apparent size, weighted by how near the centre of\n\t\t   frame it sits \u2014 something big and central is far worse than\n\t\t   something big at the edge. *\/\n\t\tvar HALF_FOV=(52*Math.PI\/180)\/2;\n\t\tfunction frameCompetition(from, c, group){\n\t\t\tvar view=c.clone().sub(from).normalize();\n\t\t\tvar score=0, edge=HALF_FOV*1.9;\n\t\t\tfor(var k=0;k<COMPETITORS.length;k++){\n\t\t\t\tvar o=COMPETITORS[k]; if(o.g===group) continue;\n\t\t\t\tvar to=o.c.clone().sub(from), dist=to.length();\n\t\t\t\tif(dist<.05) continue;\n\t\t\t\tvar ang=to.normalize().angleTo(view);\n\t\t\t\tif(ang>edge) continue;                      \/* out of shot *\/\n\t\t\t\tvar apparent=Math.atan2(o.r, dist);         \/* angular radius *\/\n\t\t\t\tscore+=apparent*(1-ang\/edge);\n\t\t\t}\n\t\t\treturn score;\n\t\t}\n\n\t\t\/* Sweep the viewing angle around the product and keep the best shot:\n\t\t   unobstructed, inside the room, and with the least competition for\n\t\t   attention. Ties break toward the authored direction. *\/\n\t\tfunction clearDirection(c, r, dir, group){\n\t\t\tvar probe=Math.max(1.9, (r\/Math.tan(HALF_FOV))*1.75);\n\t\t\tvar axis=new THREE.Vector3(0,1,0);\n\t\t\tvar lifts=[0, .12, .26];\n\t\t\tvar turns=[0, .35, -.35, .7, -.7, 1.05, -1.05, 1.4, -1.4, 1.75, -1.75, 2.1, -2.1, 2.6, -2.6, 3.14];\n\t\t\tvar best=null, bestScore=Infinity;\n\t\t\tfor(var L=0;L<lifts.length;L++){\n\t\t\t\tfor(var t=0;t<turns.length;t++){\n\t\t\t\t\tvar d=dir.clone().applyAxisAngle(axis, turns[t]);\n\t\t\t\t\td.y=Math.min(.8, d.y+lifts[L]); d.normalize();\n\t\t\t\t\tvar from=c.clone().addScaledVector(d, probe);\n\t\t\t\t\tif(Math.abs(from.x)>RX-.5 || Math.abs(from.z)>RZ-.5) continue;\n\t\t\t\t\tif(from.y<1.2 || from.y>2.7) continue;\n\t\t\t\t\tif(!shotIsClear(from, c, group)) continue;\n\t\t\t\t\t\/* competition, plus a gentle pull back toward the authored angle *\/\n\t\t\t\t\tvar sc=frameCompetition(from, c, group) + dir.angleTo(d)*.06;\n\t\t\t\t\tif(sc<bestScore){ bestScore=sc; best=d; }\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn best || dir;                    \/* nothing clear \u2014 keep the original *\/\n\t\t}\n\n\t\t\/* ---- automatic station framing --------------------------------\n\t\t   Measure each product once, while everything is still at full\n\t\t   scale (the entry animation shrinks these groups later on). *\/\n\t\tvar BASE_FOV=52;\n\t\tvar FRAMES=[];\n\t\t(function measureFrames(){\n\t\t\tvar keys=Object.keys(FOCUS_TARGETS);\n\t\t\tfor(var n=0;n<keys.length;n++){\n\t\t\t\tvar i=+keys[n], g=FOCUS_TARGETS[keys[n]], s=stops[i];\n\t\t\t\tif(!g||!s) continue;\n\t\t\t\tvar box=new THREE.Box3().setFromObject(g);\n\t\t\t\tif(box.isEmpty()) continue;\n\t\t\t\tvar c=box.getCenter(new THREE.Vector3());\n\t\t\t\tvar sz=box.getSize(new THREE.Vector3());\n\t\t\t\t\/* the frame has to contain width and height; depth only\n\t\t\t\t   matters a little, so it is weighted down *\/\n\t\t\t\tvar r=Math.max(sz.x, sz.y, sz.z*.65)*.5;\n\t\t\t\tif(!(r>0)) continue;\n\t\t\t\t\/* width and height separately. Using one radius for both meant\n\t\t\t\t   a 2.8m-tall, 0.95m-wide signage totem was framed as if it\n\t\t\t\t   were 2.8m WIDE too, so the camera backed off far enough to\n\t\t\t\t   show three metres of bare wall either side of it. *\/\n\t\t\t\tvar rw=Math.max(sz.x, sz.z)*.5, rh=sz.y*.5;\n\t\t\t\t\/* authored viewing direction \u2014 this is the art direction and\n\t\t\t\t   we keep it; only the distance is recomputed *\/\n\t\t\t\tvar dir=new THREE.Vector3().fromArray(s.pos).sub(new THREE.Vector3().fromArray(s.look));\n\t\t\t\tif(dir.lengthSq()<1e-4) dir.set(0,.5,1);\n\t\t\t\tdir.normalize();\n\t\t\t\tif(dir.y<.30) dir.y=.30;          \/* look down over foreground desks *\/\n\t\t\t\tif(dir.y>.72) dir.y=.72;          \/* never hang from the ceiling *\/\n\t\t\t\tdir.normalize();\n\t\t\t\tdir = clearDirection(c, r, dir, g);\n\t\t\t\tFRAMES.push({ i:i, c:c, r:r, rw:rw, rh:rh, dir:dir });\n\t\t\t}\n\t\t})();\n\n\t\t\/* small read-only handle for diagnosing framing and camera issues *\/\n\t\tMQR.debug={ camera:camera, scene:scene, stops:stops, frames:FRAMES, targets:FOCUS_TARGETS, THREE:THREE,\n\t\t\t\/* the render loop pauses when the tab is hidden; this forces it back\n\t\t\t   on for automated screenshots and debugging *\/\n\t\t\tresume:function(){ visible=true; awake=true; kick(); },\n\t\t\t\/* render exactly one frame synchronously. requestAnimationFrame is\n\t\t\t   suspended entirely in a hidden tab, so this is the only way to\n\t\t\t   drive the scene for automated capture. *\/\n\t\t\tstep:function(now){ visible=true; awake=true; loop(now||performance.now()); },\n\t\t\trenderer:function(){ return renderer; } };\n\n\n\n\n\n\n\t\t\/* ---- inspect mode ---------------------------------------------------\n\t\t   Everything here is captured on entry and put back on exit: light\n\t\t   levels, the shell's base colour, the product's own rotation and the\n\t\t   orbit limits. Nothing is left changed. *\/\n\t\tvar INSPECT = (function(){\n\t\t\tvar on=false, station=-1, group=null;\n\t\t\tvar lights=[], levels=[], shellBase=null, baseRot=0, spin=true;\n\t\t\tvar orbSaved=null;\n\t\t\treturn {\n\t\t\t\tactive:function(){ return on; },\n\t\t\t\tspinning:function(){ return on && spin; },\n\t\t\t\tstopSpin:function(){ spin=false; },\n\t\t\t\tenter:function(i){\n\t\t\t\t\tif(on || i<=0) return false;\n\t\t\t\t\tgroup=FOCUS_TARGETS[i];\n\t\t\t\t\tif(!group) return false;\n\t\t\t\t\ton=true; station=i; spin=true;\n\n\t\t\t\t\tlights=[ambL,hemiL,sun,fillL,rimL];\n\t\t\t\t\tlevels=lights.map(function(l){ return l.intensity; });\n\t\t\t\t\tlights.forEach(function(l){ l.intensity*=0.16; });\n\t\t\t\t\tif(BAKED_MAT && BAKED_MAT.userData.all){\n\t\t\t\t\t\tshellBase={};\n\t\t\t\t\t\tvar AI=BAKED_MAT.userData.all;\n\t\t\t\t\t\tfor(var ni in AI){ shellBase[ni]=AI[ni].color.clone();\n\t\t\t\t\t\t\tAI[ni].color.setRGB(shellBase[ni].r*.14, shellBase[ni].g*.14, shellBase[ni].b*.14); }\n\t\t\t\t\t}\n\t\t\t\t\t\/* the follow-spot is the only thing still lighting the room *\/\n\t\t\t\t\tspot.intensity=2.4;\n\n\t\t\t\t\tbaseRot=group.rotation.y;\n\t\t\t\t\tvar b=new THREE.Box3().setFromObject(group);\n\t\t\t\t\tvar c=b.getCenter(new THREE.Vector3());\n\t\t\t\t\tvar r=b.getSize(new THREE.Vector3()).length()*0.5;\n\n\t\t\t\t\torbSaved={ r:orb.r, th:orb.th, ph:orb.ph, min:ORB_MIN, max:ORB_MAX };\n\t\t\t\t\tORB_MIN=Math.max(0.5, r*0.9); ORB_MAX=Math.max(ORB_MIN+0.5, r*4.5);\n\t\t\t\t\tcamLook.copy(c);\n\t\t\t\t\torb.r=Math.max(ORB_MIN, Math.min(ORB_MAX, r*2.3));\n\t\t\t\t\torb.ph=1.15;\n\t\t\t\t\tapplyOrb();\n\n\t\t\t\t\tMQR.Hotspots.show(false);\n\t\t\t\t\tMQR.AR.clear();\n\t\t\t\t\tDOF.target(1);\n\t\t\t\t\tstage.classList.add('arx-inspect');\n\t\t\t\t\treturn true;\n\t\t\t\t},\n\t\t\t\texit:function(){\n\t\t\t\t\tif(!on) return -1;\n\t\t\t\t\ton=false;\n\t\t\t\t\tlights.forEach(function(l,k){ l.intensity=levels[k]; });\n\t\t\t\t\tif(BAKED_MAT && BAKED_MAT.userData.all && shellBase){\n\t\t\t\t\t\tvar AR=BAKED_MAT.userData.all;\n\t\t\t\t\t\tfor(var nr in AR) if(shellBase[nr]) AR[nr].color.copy(shellBase[nr]);\n\t\t\t\t\t}\n\t\t\t\t\tif(group) group.rotation.y=baseRot;\n\t\t\t\t\tif(orbSaved){ ORB_MIN=orbSaved.min; ORB_MAX=orbSaved.max; orbSaved=null; }\n\t\t\t\t\tMQR.Hotspots.show(true);\n\t\t\t\t\tstage.classList.remove('arx-inspect');\n\t\t\t\t\tvar s=station; station=-1; group=null;\n\t\t\t\t\treturn s;\n\t\t\t\t},\n\t\t\t\t\/* a slow turn until the visitor takes over *\/\n\t\t\t\ttick:function(dt){\n\t\t\t\t\tif(!on || !spin || !group) return;\n\t\t\t\t\tgroup.rotation.y += dt*0.28;\n\t\t\t\t}\n\t\t\t};\n\t\t})();\n\n\t\t\/* ---- plan view ------------------------------------------------------\n\t\t   The ceiling is part of the single baked shell mesh, so it cannot be\n\t\t   faded on its own. A global clipping plane removes everything above\n\t\t   it instead, which is both cheaper and gives the nicer effect: the\n\t\t   roof lifts off rather than dissolving. *\/\n\t\tvar PLAN = (function(){\n\t\t\tvar on=false, from=-1, clip=null, t0=0, DUR=1.25;\n\t\t\tvar startPos=new THREE.Vector3(), startLook=new THREE.Vector3(), startFov=52;\n\t\t\tvar endPos=new THREE.Vector3(), endLook=new THREE.Vector3(), endFov=15;\n\t\t\tvar CLIP_OPEN=9.0, CLIP_SHUT=3.42;\n\n\t\t\t\/* how far back the camera must sit for the whole room to fit *\/\n\t\t\tfunction overhead(){\n\t\t\t\tvar aspect=Math.max(camera.aspect||1.6, .3);\n\t\t\t\t\/* generous margin: the pill rail and the control bar overlay the\n\t\t\t\t   stage, so the room has to sit inside the clear middle *\/\n\t\t\t\tvar needH=Math.max(RZ*2+3.6, (RX*2+5.2)\/aspect);\n\t\t\t\tvar d=needH\/(2*Math.tan((endFov*Math.PI\/180)\/2));\n\t\t\t\treturn Math.max(14, Math.min(d, 130));\n\t\t\t}\n\t\t\treturn {\n\t\t\t\tactive:function(){ return on; },\n\t\t\t\tinit:function(){\n\t\t\t\t\tclip=new THREE.Plane(new THREE.Vector3(0,-1,0), CLIP_OPEN);\n\t\t\t\t\trenderer.clippingPlanes=[clip];\n\t\t\t\t},\n\t\t\t\tenter:function(prevStation){\n\t\t\t\t\tif(on) return;\n\t\t\t\t\ton=true; from=prevStation;\n\t\t\t\t\tstartPos.copy(camPos); startLook.copy(camLook); startFov=camera.fov;\n\t\t\t\t\tendPos.set(0, overhead(), 0.001);   \/* the offset avoids a degenerate lookAt *\/\n\t\t\t\t\tendLook.set(0, 0, 0);\n\t\t\t\t\tt0=performance.now();\n\t\t\t\t},\n\t\t\t\texit:function(){\n\t\t\t\t\tif(!on) return -1;\n\t\t\t\t\ton=false;\n\t\t\t\t\tvar back=from; from=-1;\n\t\t\t\t\tif(clip) clip.constant=CLIP_OPEN;\n\t\t\t\t\tcamera.fov=BASE_FOV; camera.updateProjectionMatrix();\n\t\t\t\t\treturn back;\n\t\t\t\t},\n\t\t\t\t\/* drives the camera itself while active; returns true if it did *\/\n\t\t\t\ttick:function(now){\n\t\t\t\t\tif(!on) return false;\n\t\t\t\t\tvar p=Math.max(0,Math.min((now-t0)\/DUR,1));\n\t\t\t\t\tvar e=MQR.Ease.inOutCubic(p);\n\t\t\t\t\tcamPos.lerpVectors(startPos,endPos,e);\n\t\t\t\t\tcamLook.lerpVectors(startLook,endLook,e);\n\t\t\t\t\tcamera.fov=startFov+(endFov-startFov)*e;\n\t\t\t\t\tcamera.updateProjectionMatrix();\n\t\t\t\t\tif(clip) clip.constant=CLIP_OPEN+(CLIP_SHUT-CLIP_OPEN)*e;\n\t\t\t\t\treturn true;\n\t\t\t\t},\n\t\t\t\tresize:function(){ if(on) endPos.set(0, overhead(), 0.001); }\n\t\t\t};\n\t\t})();\n\n\t\t\/* ---- micro-animation ------------------------------------------------\n\t\t   When a station is selected the product gives a short mechanical\n\t\t   response: it lifts, settles and yaws back to rest on a damped\n\t\t   spring. Deliberately driven by the render loop rather than a tween\n\t\t   library, because the loop pauses when the tab is hidden and an\n\t\t   independent ticker would drift out of step with it.\n\n\t\t   It does not touch emissive values \u2014 the focus system owns those. *\/\n\t\tMQR.Micro=(function(){\n\t\t\tvar g=null, base=null, t0=0, DUR=1.35;\n\t\t\tfunction rest(){\n\t\t\t\tif(!g||!base) return;\n\t\t\t\tg.position.y=base.y; g.rotation.y=base.ry; g.scale.setScalar(base.s);\n\t\t\t}\n\t\t\treturn {\n\t\t\t\tplay:function(group){\n\t\t\t\t\tif(!group || group===g) { if(group){ t0=performance.now(); } return; }\n\t\t\t\t\trest();\n\t\t\t\t\tg=group; t0=performance.now();\n\t\t\t\t\tbase={ y:group.position.y, ry:group.rotation.y, s:group.scale.x||1 };\n\t\t\t\t},\n\t\t\t\tstop:function(){ rest(); g=null; base=null; },\n\t\t\t\ttick:function(now){\n\t\t\t\t\tif(!g||!base) return;\n\t\t\t\t\tvar e=(now-t0)\/1000;\n\t\t\t\t\tif(e>=DUR){ rest(); g=null; base=null; return; }\n\t\t\t\t\tvar damp=Math.exp(-e*3.1);\n\t\t\t\t\tg.position.y = base.y + Math.sin(e*10.5)*0.024*damp;\n\t\t\t\t\tg.rotation.y = base.ry + Math.sin(e*7.6)*0.05*damp;\n\t\t\t\t\tvar lift = Math.sin(Math.min(1,e\/0.5)*Math.PI)*0.03;\n\t\t\t\t\tg.scale.setScalar(base.s*(1+lift));\n\t\t\t\t}\n\t\t\t};\n\t\t})();\n\n\t\t\/* ---- depth of field ------------------------------------------------\n\t\t   Focus follows the camera's own look-at target, so whatever the tour\n\t\t   is pointing at is what stays sharp. Strength ramps with `want`, and\n\t\t   at zero the composer is bypassed entirely and we render straight to\n\t\t   the canvas \u2014 the overview costs exactly what it did before. *\/\n\t\tvar DOF = (function(){\n\t\t\tvar composer=null, bokeh=null, on=false, blur=0, want=0, ready=false;\n\t\t\tvar MAX_BLUR=0.0115;\n\t\t\treturn {\n\t\t\t\tinit:function(){\n\t\t\t\t\tif(LOW_END) return false;\n\t\t\t\t\tif(typeof THREE.EffectComposer!=='function' || typeof THREE.BokehPass!=='function') return false;\n\t\t\t\t\ttry{\n\t\t\t\t\t\tvar w=Math.max(1,stage.clientWidth), h=Math.max(1,stage.clientHeight);\n\t\t\t\t\t\t\/* an sRGB target, otherwise the composer hands back linear\n\t\t\t\t\t\t   colour and the whole room shifts dark *\/\n\t\t\t\t\t\tvar rt=new THREE.WebGLRenderTarget(w,h,{\n\t\t\t\t\t\t\tminFilter:THREE.LinearFilter, magFilter:THREE.LinearFilter,\n\t\t\t\t\t\t\tformat:THREE.RGBAFormat, encoding:THREE.sRGBEncoding });\n\t\t\t\t\t\tcomposer=new THREE.EffectComposer(renderer, rt);\n\t\t\t\t\t\tcomposer.addPass(new THREE.RenderPass(scene,camera));\n\t\t\t\t\t\tbokeh=new THREE.BokehPass(scene,camera,{ focus:6.0, aperture:0.00021, maxblur:0.0, width:w, height:h });\n\t\t\t\t\t\tcomposer.addPass(bokeh);\n\t\t\t\t\t\tready=true;\n\t\t\t\t\t}catch(e){ ready=false; }\n\t\t\t\t\treturn ready;\n\t\t\t\t},\n\t\t\t\t\/* 0 = crisp (overview), 1 = shallow depth of field (a station) *\/\n\t\t\t\ttarget:function(v){ want=Math.max(0,Math.min(1,v)); },\n\t\t\t\tresize:function(w,h){ if(ready){ composer.setSize(w,h); } },\n\t\t\t\t\/* returns true when it rendered the frame itself *\/\n\t\t\t\trender:function(dist){\n\t\t\t\t\tif(!ready) return false;\n\t\t\t\t\tblur += (want-blur)*0.06;\n\t\t\t\t\tif(blur<0.02 && want<0.02){ on=false; return false; }\n\t\t\t\t\ton=true;\n\t\t\t\t\tvar u=bokeh.materialBokeh.uniforms;\n\t\t\t\t\tu.focus.value += (dist-u.focus.value)*0.10;\n\t\t\t\t\tu.maxblur.value = MAX_BLUR*blur;\n\t\t\t\t\tcomposer.render();\n\t\t\t\t\treturn true;\n\t\t\t\t},\n\t\t\t\tactive:function(){ return ready && on; },\n\t\t\t\tenabled:function(){ return ready; }\n\t\t\t};\n\t\t})();\n\n\t\t\/* ---- baked shell ---------------------------------------------------\n\t\t   Loads the Blender bake. If either asset is missing the procedural\n\t\t   shell simply stays visible, so this can never break the room. *\/\n\t\tvar BAKED=null, BAKED_MAT=null;\n\t\t(function loadBakedShell(){\n\t\t\tif(typeof THREE.GLTFLoader!=='function') return;\n\t\t\tvar base='https:\/\/digitalmqr.com\/wp-content\/themes\/mqr-theme';\n\t\t\t\/* version-stamped so a re-bake actually reaches the browser *\/\n\t\t\tvar v='?v=3.13.0';\n\t\t\tvar lm=new THREE.TextureLoader().load(base+'\/assets\/lightmaps\/classroom-lm.webp'+v);\n\t\t\tlm.flipY=false;                       \/* glTF UV convention *\/\n\t\t\tlm.encoding=THREE.sRGBEncoding;\n\t\t\t\/* Mipmaps are essential here: without them a 1024px atlas minified\n\t\t\t   across a 16-metre wall shimmers as the camera moves. *\/\n\t\t\tlm.minFilter=THREE.LinearMipmapLinearFilter;\n\t\t\tlm.magFilter=THREE.LinearFilter;\n\t\t\tlm.generateMipmaps=true;\n\t\t\tlm.anisotropy=Math.min(8, renderer.capabilities.getMaxAnisotropy());\n\t\t\tnew THREE.GLTFLoader().load(base+'\/assets\/models\/classroom-shell.glb'+v, function(gltf){\n\t\t\t\t\/* unlit: the bake already holds every bit of light in this room *\/\n\t\t\t\t\/* Three slots, three tints.\n\n\t\t\t\t   These have to be SATURATED, not pastel. The shell is unlit\n\t\t\t\t   MeshBasicMaterial, so what you see is tint x lightmap, and\n\t\t\t\t   the lightmap averages about 0.7 grey. A pastel cream came\n\t\t\t\t   out of that multiply as plain grey \u2014 which is exactly why\n\t\t\t\t   the room kept reading as washed out no matter how much the\n\t\t\t\t   exposure was nudged. A mint wall against the lilac hardware\n\t\t\t\t   and honey-oak floor is what finally gave it colour. *\/\n\t\t\t\tvar SHELL_TINT={ ShellFloor:0xe0a55e, ShellWall:0xffe6c2, ShellCeiling:0xfff8ec };\n\t\t\t\tvar shellMats={};\n\t\t\t\tfunction makeShell(name){\n\t\t\t\t\tvar mm=new THREE.MeshBasicMaterial({\n\t\t\t\t\t\tcolor: SHELL_TINT[name] || 0xffe6c2,\n\t\t\t\t\t\tlightMap:lm, lightMapIntensity:1.42 });\n\t\t\t\t\tmm.userData.base=mm.color.clone();\n\t\t\t\t\tshellMats[name]=mm;\n\t\t\t\t\treturn mm;\n\t\t\t\t}\n\t\t\t\tBAKED_MAT=makeShell('ShellWall');\n\t\t\t\tBAKED_MAT.userData.all=shellMats;\n\t\t\t\tgltf.scene.traverse(function(o){\n\t\t\t\t\tif(!o.isMesh) return;\n\t\t\t\t\tvar g=o.geometry;\n\t\t\t\t\tif(!g.getAttribute('uv2')){\n\t\t\t\t\t\tvar second=g.getAttribute('uv1')||g.getAttribute('uv');\n\t\t\t\t\t\tif(second) g.setAttribute('uv2', second);\n\t\t\t\t\t}\n\t\t\t\t\tvar nm=(o.material && o.material.name) || 'ShellWall';\n\t\t\t\t\to.material = shellMats[nm] || makeShell(nm);\n\t\t\t\t\to.castShadow=false; o.receiveShadow=false;\n\t\t\t\t});\n\t\t\t\tworld.add(gltf.scene);\n\t\t\t\tBAKED=gltf.scene;\n\t\t\t\t\/* retire the runtime shell but keep the references alive *\/\n\t\t\t\t[floor,wb,wl,wr,wf,ceil].forEach(function(m){ if(m) m.visible=false; });\n\t\t\t}, undefined, function(){ \/* asset missing \u2014 procedural shell stands in *\/ });\n\t\t})();\n\n\t\t\/* ---- contact shadows ---------------------------------------------\n\t\t   The sun casts one hard shadow map; objects still read as floating\n\t\t   without the soft darkening directly beneath them. One shared\n\t\t   material and one shared quad, so this is a handful of draw calls. *\/\n\t\tvar contactMat=null, contactShadows=[];\n\t\t\/* One global dial for the intro fade, but each shadow keeps its own\n\t\t   dim factor, so a big faint pool and a small dark one respond to the\n\t\t   same fade without ending up equally heavy. *\/\n\t\tfunction setContactOpacity(v){\n\t\t\tfor(var i=0;i<contactShadows.length;i++){\n\t\t\t\tvar m=contactShadows[i];\n\t\t\t\tm.material.opacity = v * (m.userData.dim===undefined?1:m.userData.dim);\n\t\t\t}\n\t\t}\n\t\tfunction addContactShadows(){\n\t\t\tvar t=tex(128,128,function(g,w,h){\n\t\t\t\tvar gr=g.createRadialGradient(w\/2,h\/2,0,w\/2,h\/2,w\/2);\n\t\t\t\tgr.addColorStop(0,'rgba(0,0,0,.62)');\n\t\t\t\tgr.addColorStop(.5,'rgba(0,0,0,.26)');\n\t\t\t\tgr.addColorStop(1,'rgba(0,0,0,0)');\n\t\t\t\tg.fillStyle=gr; g.fillRect(0,0,w,h);\n\t\t\t});\n\t\t\tcontactMat=new THREE.MeshBasicMaterial({ map:t, transparent:true,\n\t\t\t\tdepthWrite:false, opacity:0.9 });\n\t\t\tvar quad=new THREE.PlaneGeometry(1,1);\n\t\t\tfor(var i=0;i<parts.length;i++){\n\t\t\t\tvar b=new THREE.Box3().setFromObject(parts[i]);\n\t\t\t\tif(b.isEmpty()) continue;\n\t\t\t\tif(b.min.y>1.15) continue;          \/* wall-mounted: nothing to ground *\/\n\t\t\t\tvar m=new THREE.Mesh(quad, contactMat.clone());\n\t\t\t\tm.rotation.x=-Math.PI\/2;\n\t\t\t\tm.renderOrder=-1;\n\t\t\t\tworld.add(m);\n\t\t\t\tcontactShadows.push(m);\n\t\t\t\t\/* remember which prop owns it: the moment a GLB replaces the\n\t\t\t\t   procedural stand-in the footprint changes, and a shadow\n\t\t\t\t   sized to the old stand-in is the \"tiny object, enormous\n\t\t\t\t   shadow\" bug. fitContactShadow re-reads the box instead. *\/\n\t\t\t\tparts[i].userData.contact=m;\n\t\t\t\tfitContactShadow(parts[i]);\n\t\t\t}\n\t\t}\n\n\t\t\/* Size one contact shadow to whatever its prop currently looks like.\n\t\t   1.15 rather than the old 1.8: a contact shadow is the darkening\n\t\t   where an object meets the floor, so it should barely exceed the\n\t\t   footprint. At 1.8 a two-metre bench threw a six-metre bruise. *\/\n\t\t\/* Box3.setFromObject walks hidden children too. Once a GLB is parked\n\t\t   next to the stand-in inside the same group, every measurement taken\n\t\t   that way still describes the stand-in \u2014 which is how a correctly\n\t\t   scaled device ended up with a stand-in-sized shadow and a camera\n\t\t   pulled back far enough to frame something nobody can see. *\/\n\t\tfunction shownBox(root, skip){\n\t\t\tvar b=new THREE.Box3();\n\t\t\t(function rec(o){\n\t\t\t\tif(!o.visible || o===skip) return;\n\t\t\t\tif(o.isMesh) b.expandByObject(o);\n\t\t\t\tfor(var i=0;i<o.children.length;i++) rec(o.children[i]);\n\t\t\t})(root);\n\t\t\treturn b;\n\t\t}\n\n\t\tfunction fitContactShadow(p){\n\t\t\tvar m=p&&p.userData&&p.userData.contact;\n\t\t\tif(!m) return;\n\t\t\tp.updateMatrixWorld(true);\n\t\t\tvar b=shownBox(p, m);\n\t\t\tif(b.isEmpty()) return;\n\t\t\tvar sz=b.getSize(new THREE.Vector3()), c=b.getCenter(new THREE.Vector3());\n\t\t\tvar w=Math.max(.30, Math.min(sz.x*1.15, 3.2));\n\t\t\tvar d=Math.max(.30, Math.min(sz.z*1.15, 3.2));\n\t\t\tm.scale.set(w, d, 1);\n\t\t\tm.position.set(c.x, .038, c.z);\n\t\t\t\/* a wide blob has to be fainter or it reads as a hole in the floor,\n\t\t\t   and something resting high above the floor barely shades it *\/\n\t\t\tvar area=w*d, lift=Math.max(0, b.min.y);\n\t\t\tm.userData.dim = Math.max(.30, Math.min(1, 2.4\/Math.max(1,area))) * Math.max(.25, 1-lift*0.9);\n\t\t\t\/* apply it straight away: the intro fade is not the only route in\n\t\t\t   (skip intro, deep link, reduced motion), and a shadow that only\n\t\t\t   gets dimmed by the fade path stays at full strength on the rest *\/\n\t\t\tm.material.opacity = 0.9 * m.userData.dim;\n\t\t}\n\n\t\tfunction applyFrames(){\n\t\t\tif(!FRAMES.length) return;\n\t\t\tvar half=Math.tan((BASE_FOV*Math.PI\/180)\/2);\n\t\t\tvar aspect=Math.max(camera.aspect||1.5, .5);\n\t\t\t\/* cached once per call: this runs again after every model swap *\/\n\t\t\tvar propBoxes=[];\n\t\t\tfor(var q0=0;q0<parts.length;q0++){\n\t\t\t\tvar pb=new THREE.Box3().setFromObject(parts[q0]);\n\t\t\t\tif(pb.isEmpty()) continue;\n\t\t\t\tpb.expandByScalar(0.10);\n\t\t\t\tpropBoxes.push(pb);\n\t\t\t}\n\t\t\tfor(var n=0;n<FRAMES.length;n++){\n\t\t\t\tvar f=FRAMES[n], s=stops[f.i];\n\t\t\t\tvar rh=(f.rh!==undefined?f.rh:f.r), rw=(f.rw!==undefined?f.rw:f.r);\n\t\t\t\tvar fitV=rh\/half;                  \/* distance to fit its height   *\/\n\t\t\t\tvar fitH=rw\/(half*aspect);         \/* distance to fit its width    *\/\n\t\t\t\t\/* tighter than before: the subject filling more of the frame is\n\t\t\t\t   itself a defence against neighbours crowding in *\/\n\t\t\t\tvar d=Math.max(fitV,fitH)*1.42;\n\t\t\t\td=Math.max(1.8, Math.min(d, 8.5));\n\t\t\t\tvar p=f.c.clone().addScaledVector(f.dir, d);\n\t\t\t\t\/* stay inside the room so the camera never punches a wall *\/\n\t\t\t\tp.x=Math.max(-RX+.55, Math.min(RX-.55, p.x));\n\t\t\t\tp.z=Math.max(-RZ+.55, Math.min(RZ-.55, p.z));\n\t\t\t\tp.y=Math.max(1.40, Math.min(2.55, p.y));   \/* standing eye height, not ceiling height *\/\n\t\t\t\t\/* If that viewpoint landed inside a desk, back off along the\n\t\t\t\t   view ray until it is in open air \u2014 otherwise the visitor\n\t\t\t\t   arrives looking at the inside of a cabinet. *\/\n\t\t\t\tfor(var tries=0; tries<16; tries++){\n\t\t\t\t\tvar hit=false;\n\t\t\t\t\tfor(var q=0;q<propBoxes.length;q++){\n\t\t\t\t\t\tif(propBoxes[q].containsPoint(p)){ hit=true; break; }\n\t\t\t\t\t}\n\t\t\t\t\tif(!hit) break;\n\t\t\t\t\tp.addScaledVector(f.dir, 0.30);\n\t\t\t\t\tp.x=Math.max(-RX+.55, Math.min(RX-.55, p.x));\n\t\t\t\t\tp.z=Math.max(-RZ+.55, Math.min(RZ-.55, p.z));\n\t\t\t\t\tp.y=Math.max(1.40, Math.min(2.60, p.y));\n\t\t\t\t}\n\t\t\t\ts.pos=[p.x,p.y,p.z];\n\t\t\t\t\/* aim a touch above centre so the product sits on the upper\n\t\t\t\t   two-thirds of the frame rather than behind the furniture *\/\n\t\t\t\ts.look=[f.c.x, f.c.y + f.r*.12, f.c.z];\n\t\t\t}\n\t\t}\n\t\tapplyFrames();\n\t\taddContactShadows();\n\n\t\t\/* The model loader calls this the moment a GLB lands in a group. *\/\n\t\tMQR.Models.onSwapped=function(group){\n\t\t\t\/* Measure nothing until the matrices are current. fitContactShadow\n\t\t\t   used to be what refreshed them, but it returns immediately for\n\t\t\t   wall-mounted props (they get no contact shadow), so the Smart\n\t\t\t   Access panel was re-framed from a stale box at the world origin\n\t\t\t   and its camera ended up parked in the middle of the desks. *\/\n\t\t\tgroup.updateMatrixWorld(true);\n\t\t\tfitContactShadow(group);\n\t\t\tfor(var n=0;n<FRAMES.length;n++){\n\t\t\t\tif(FOCUS_TARGETS[FRAMES[n].i]!==group) continue;\n\t\t\t\tvar b=shownBox(group, group.userData.contact);\n\t\t\t\tif(b.isEmpty()) return;\n\t\t\t\tvar sz=b.getSize(new THREE.Vector3());\n\t\t\t\tvar r=Math.max(sz.x, sz.y, sz.z*.65)*.5;\n\t\t\t\tif(!(r>0)) return;\n\t\t\t\tvar c=b.getCenter(new THREE.Vector3());\n\t\t\t\t\/* a subject should never travel far from where it was authored;\n\t\t\t\t   if it claims to have, the measurement is wrong, not the room *\/\n\t\t\t\tvar home=new THREE.Vector3(); group.getWorldPosition(home);\n\t\t\t\tif(c.distanceTo(home)>4) return;\n\t\t\t\tFRAMES[n].c=c;\n\t\t\t\tFRAMES[n].r=r;\n\t\t\t\tFRAMES[n].rw=Math.max(sz.x, sz.z)*.5;\n\t\t\t\tFRAMES[n].rh=sz.y*.5;\n\t\t\t\tapplyFrames();\n\t\t\t\treturn;\n\t\t\t}\n\t\t};\n\n\t\t\/* ---------------- tour engine ---------------- *\/\n\t\tvar data=[[\"Overview\",\"Welcome to the smart classroom\",\"One room where every surface teaches \\u2014 14 technologies working together. Fly the tour, or drag and explore freely.\",[\"14 stations\",\"Guided tour\",\"Drag & zoom\"],\"\",[]],[\"Interactive Panel\",\"4K Interactive Smart Panel\",\"The whiteboard, reinvented \\u2014 multi-touch 4K display with built-in whiteboarding, wireless casting and saved lessons. Dahua & Promethean.\",[\"20-point touch\",\"4K UHD\",\"Wireless casting\"],\"https:\\\/\\\/digitalmqr.com\\\/ar\\\/interactive-smart-panels\\\/\",[\"https:\\\/\\\/digitalmqr.com\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/3-1024x687.png\",\"https:\\\/\\\/digitalmqr.com\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/interactiveppanels-1024x576.jpg\",\"https:\\\/\\\/digitalmqr.com\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/dah1.webp\",\"https:\\\/\\\/digitalmqr.com\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/Okv-1024x336.webp\",\"https:\\\/\\\/digitalmqr.com\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/CDX-iProImage-1024x512.webp\",\"https:\\\/\\\/digitalmqr.com\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/Panel-1-1024x1024.webp\"]],[\"UST Projector\",\"Epson Ultra-Short-Throw Projection\",\"A 100-inch bright interactive image cast from centimetres off the wall \\u2014 no shadows over the teacher, no glare.\",[\"100\\\" image\",\"Finger-touch\",\"Epson laser\"],\"https:\\\/\\\/digitalmqr.com\\\/ar\\\/interactive-projectors\\\/\",[\"https:\\\/\\\/digitalmqr.com\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/epsonamerica_1632527228_2670154350964663210_38598550-1024x1024.png\",\"https:\\\/\\\/digitalmqr.com\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/EB-760Wi--1024x1024.png\",\"https:\\\/\\\/digitalmqr.com\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/EB-1481Fi-1024x1024.png\",\"https:\\\/\\\/digitalmqr.com\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/EB-1485Fi-1024x1024.png\",\"https:\\\/\\\/digitalmqr.com\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/jnghyjn-1024x1024.jpg\",\"https:\\\/\\\/digitalmqr.com\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/K12-Education-Projectors_fg_display-solution_tile02-1140x900-1-1024x808.jpg\"]],[\"Interactive Floor\",\"Roomie Interactive Floor\",\"An educational universe projected on the floor \\u2014 themed worlds that turn movement into play and play into skills.\",[\"11 worlds\",\"Motion sensing\",\"KG\\u2013primary\"],\"https:\\\/\\\/digitalmqr.com\\\/ar\\\/solutions\\\/\",[\"https:\\\/\\\/digitalmqr.com\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/Edpuzzle-1024x1024.webp\",\"https:\\\/\\\/digitalmqr.com\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/Edpuzzle-1-1024x1024.webp\",\"https:\\\/\\\/digitalmqr.com\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/Screenshot-1-1024x683.webp\",\"https:\\\/\\\/digitalmqr.com\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/maxresdefault-1-1024x576.webp\"]],[\"AR Sandbox\",\"Interactive AR Sandbox\",\"Shape real sand and watch topography, water flow and geography appear live \\u2014 abstract concepts become hands-on.\",[\"Live topography\",\"Geography & STEM\",\"IMSAND\"],\"https:\\\/\\\/digitalmqr.com\\\/ar\\\/interactive-sandbox\\\/\",[\"https:\\\/\\\/digitalmqr.com\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/3f930020-4d98-4e86-abdf-275bd34b60d7-1024x683.webp\",\"https:\\\/\\\/digitalmqr.com\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/IMSand-1024x1024.webp\",\"https:\\\/\\\/digitalmqr.com\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/25361f2d-f0a6-4719-8bf8-33d245b3f1af-1024x683.webp\",\"https:\\\/\\\/digitalmqr.com\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/sandbox_n4-2048x1366-2-1-1024x683.webp\",\"https:\\\/\\\/digitalmqr.com\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/sandbox_n-2048x1366-1-1-1024x683.webp\",\"https:\\\/\\\/digitalmqr.com\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/IMSAND-Edited-7-1024x576.webp\"]],[\"VR Corner\",\"VR Glasses & Curriculum\",\"Field trips without leaving the room \\u2014 the human body, ancient cities, the solar system \\u2014 mapped to the curriculum.\",[\"Meta Quest\",\"Curriculum-mapped\",\"Teacher console\"],\"https:\\\/\\\/digitalmqr.com\\\/ar\\\/vr-ar-solutions\\\/\",[\"https:\\\/\\\/digitalmqr.com\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/323-1024x576.jpg\",\"https:\\\/\\\/digitalmqr.com\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/Pages-quick-element.webp\",\"https:\\\/\\\/digitalmqr.com\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/Umety-1024x1024.webp\",\"https:\\\/\\\/digitalmqr.com\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/sddefault.webp\",\"https:\\\/\\\/digitalmqr.com\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/1720623623480.webp\",\"https:\\\/\\\/digitalmqr.com\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/Group-122135-1-1.webp\"]],[\"Robotics & 3D\",\"Robotics, Coding & 3D Printing\",\"VEX robotics kits and Prusa \\\/ Creality printers turn theory into building \\u2014 a full hands-on STEM bench, with a class drone overhead.\",[\"VEX kits\",\"Prusa & Creality\",\"STEM curriculum\"],\"https:\\\/\\\/digitalmqr.com\\\/ar\\\/ai-robots-programming\\\/\",[\"https:\\\/\\\/digitalmqr.com\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/Sandstorm2-scaled-1-1024x768.webp\",\"https:\\\/\\\/digitalmqr.com\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/vex-robotics-2c-1.webp\",\"https:\\\/\\\/digitalmqr.com\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/VEXThumbnail.webp\",\"https:\\\/\\\/digitalmqr.com\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/download-1024x683.webp\",\"https:\\\/\\\/digitalmqr.com\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/soen-1280x720-20240106-vex-robotics-001-1024x576.webp\",\"https:\\\/\\\/digitalmqr.com\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/VEX-Photo1-1024x576.webp\"]],[\"Science Lab\",\"Virtual Science Labs\",\"Every experiment, zero risk \\u2014 full chemistry, physics and biology benches in simulation, paired with real glassware for the basics.\",[\"PraxiLabs\",\"Safe & repeatable\",\"No consumables\"],\"https:\\\/\\\/digitalmqr.com\\\/ar\\\/virtual-science-labs\\\/\",[\"https:\\\/\\\/digitalmqr.com\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/3-3-1024x576.jpg\",\"https:\\\/\\\/digitalmqr.com\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/1-1.png\",\"https:\\\/\\\/digitalmqr.com\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/2-1.png\",\"https:\\\/\\\/digitalmqr.com\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/3-1.png\"]],[\"Zero-Client Lab\",\"Zero-Client Computing Lab\",\"A full computer lab with no bulky PCs \\u2014 silent zero-client stations connect to one central server. Less power, less maintenance, central control.\",[\"Zero clients\",\"Central server\",\"70% less power\"],\"https:\\\/\\\/digitalmqr.com\\\/ar\\\/computing-solutions\\\/\",[\"https:\\\/\\\/digitalmqr.com\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/321-1024x576.jpg\",\"https:\\\/\\\/digitalmqr.com\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/zero-client-computing-1024x355.png\",\"https:\\\/\\\/digitalmqr.com\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/ZCS.png\",\"https:\\\/\\\/digitalmqr.com\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/education.png\",\"https:\\\/\\\/digitalmqr.com\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/Office.png\",\"https:\\\/\\\/digitalmqr.com\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/Call-Center.png\"]],[\"Teacher Station\",\"Teacher Desk, Doc-Cam & LMS\",\"Show any page or experiment in crisp detail with the document camera, while the school platform runs attendance, grades and parents.\",[\"4K doc camera\",\"SMS + LMS\",\"One dashboard\"],\"https:\\\/\\\/digitalmqr.com\\\/ar\\\/meeting-document-cameras\\\/\",[\"https:\\\/\\\/digitalmqr.com\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/5pwv0v1u-1024x576.webp\",\"https:\\\/\\\/digitalmqr.com\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/Copy-1024x252.webp\",\"https:\\\/\\\/digitalmqr.com\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/asikfdmi-1024x576.webp\",\"https:\\\/\\\/digitalmqr.com\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/original-1-1024x420.webp\",\"https:\\\/\\\/digitalmqr.com\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/original-1024x420.webp\"]],[\"Signage\",\"Digital Signage Totem\",\"Announcements, timetables and celebrations on bright displays at the entrance \\u2014 managed centrally for the whole school.\",[\"24\\\/7 display\",\"Central CMS\",\"Any screen\"],\"https:\\\/\\\/digitalmqr.com\\\/ar\\\/solutions\\\/\",[\"https:\\\/\\\/digitalmqr.com\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/Edpuzzle-1024x1024.webp\",\"https:\\\/\\\/digitalmqr.com\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/Edpuzzle-1-1024x1024.webp\",\"https:\\\/\\\/digitalmqr.com\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/Screenshot-1-1024x683.webp\",\"https:\\\/\\\/digitalmqr.com\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/maxresdefault-1-1024x576.webp\"]],[\"Smart Access\",\"Face & Fingerprint Attendance\",\"Virdi biometric devices greet the class at the door \\u2014 contactless recognition, automatic attendance, secure access.\",[\"Face recognition\",\"Live fingerprint\",\"Auto attendance\"],\"https:\\\/\\\/digitalmqr.com\\\/ar\\\/face-fingerprint-devices\\\/\",[\"https:\\\/\\\/digitalmqr.com\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/dse-1024x576.png\",\"https:\\\/\\\/digitalmqr.com\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/4-2-1024x1024.webp\",\"https:\\\/\\\/digitalmqr.com\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/1-1-1024x1024.webp\",\"https:\\\/\\\/digitalmqr.com\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/2-1-1024x1024.webp\",\"https:\\\/\\\/digitalmqr.com\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/418HqU6iACL._UF10001000_QL80_.webp\",\"https:\\\/\\\/digitalmqr.com\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/3-3-1024x1024.webp\"]],[\"Charging Cart\",\"Indota Charging Cart\",\"The device fleet charged, updated, stored securely and rolled wherever the lesson goes next.\",[\"30+ devices\",\"Smart charging\",\"Secure & mobile\"],\"https:\\\/\\\/digitalmqr.com\\\/ar\\\/charging-carts\\\/\",[\"https:\\\/\\\/digitalmqr.com\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/Transformer-1024x1024.webp\",\"https:\\\/\\\/digitalmqr.com\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/8b8983d8787f9fd.webp\",\"https:\\\/\\\/digitalmqr.com\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/882ee1268e48db6.webp\",\"https:\\\/\\\/digitalmqr.com\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/Indota-1024x1024.webp\"]],[\"School Uniform\",\"School Uniform Program\",\"Quality, comfortable, well-designed uniforms that build identity and equality \\u2014 designed and supplied end-to-end.\",[\"Custom design\",\"Quality fabric\",\"Full supply\"],\"https:\\\/\\\/digitalmqr.com\\\/ar\\\/school-uniform\\\/\",[\"https:\\\/\\\/digitalmqr.com\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/3-5-1024x1024.webp\",\"https:\\\/\\\/digitalmqr.com\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/4-4-1024x1024.webp\",\"https:\\\/\\\/digitalmqr.com\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/2-3-1024x1024.webp\",\"https:\\\/\\\/digitalmqr.com\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/1-3-1024x1024.webp\"]]];\n\t\tvar card=document.getElementById('arxCard'), tEl=document.getElementById('arxTitle'), bEl=document.getElementById('arxBody'),\n\t\t\tsEl=document.getElementById('arxStep'), lEl=document.getElementById('arxLabel'), chEl=document.getElementById('arxChips'),\n\t\t\tmoreEl=document.getElementById('arxMore'), numEl=document.getElementById('arxNum'), shotsEl=document.getElementById('arxShots'),\n\t\t\tdots=[].slice.call(document.querySelectorAll('#arxDots button')),\n\t\t\thint=document.getElementById('arxHint'), exitBtn=document.getElementById('arxExit'),\n\t\t\tliveEl=document.getElementById('arxLive');\n\t\tvar SR_STOP=\"Stop %1$s of %2$s: %3$s\";\n\t\tvar cur=-1, tween=null, auto=false, autoT=null, camRoll=0;\n\n\t\t\/* hotspots initialise here so labels can use the real station names *\/\n\t\thotDefs.forEach(function(h){ if(data[h.i] && data[h.i][0]) h.label=data[h.i][0]; });\n\t\tMQR.Hotspots.init(document.getElementById('arxHot'), camera, THREE, hotDefs, function(i){\n\t\t\tstopAuto(); goTo(i);\n\t\t});\n\t\tvar camPos=new THREE.Vector3(0,18,26), camLook=new THREE.Vector3(0,1,0);\n\t\tvar orb={r:9,th:0,ph:1.1};\n\t\tvar ORB_MIN=2.2, ORB_MAX=14;      \/* inspect mode narrows these *\/\n\n\t\tfunction ease(t){ return t<.5 ? 4*t*t*t : 1-Math.pow(-2*t+2,3)\/2; }\n\t\tfunction easeBack(t){ var c1=1.70158,c3=c1+1; return 1+c3*Math.pow(t-1,3)+c1*Math.pow(t-1,2); }\n\t\tfunction pad2(x){ return ('0'+x).slice(-2); }\n\n\t\tfunction goTo(i){\n\t\t\ti=(i+stops.length)%stops.length;\n\t\t\tvar s=stops[i], from=camPos.clone(), fromL=camLook.clone(),\n\t\t\t\tto=new THREE.Vector3().fromArray(s.pos), toL=new THREE.Vector3().fromArray(s.look);\n\t\t\t\/* curved flight: raised bezier midpoint for a real \"fly\" feel *\/\n\t\t\tvar mid=from.clone().add(to).multiplyScalar(.5);\n\t\t\tmid.y+=Math.min(1.9,.4+from.distanceTo(to)*.16);\n\t\t\t\/* the arc must not bow through a wall either *\/\n\t\t\tif(i>0 && cur>0){\n\t\t\t\tmid.x=Math.max(-RX+0.6, Math.min(RX-0.6, mid.x));\n\t\t\t\tmid.z=Math.max(-RZ+0.6, Math.min(RZ-0.6, mid.z));\n\t\t\t}\n\t\t\t\/* people who ask for reduced motion get an almost instant cut instead\n\t\t\t   of a two-second camera flight *\/\n\t\t\tvar dur = REDUCED ? 260 : Math.max(1100,Math.min(2200,from.distanceTo(to)*140));\n\t\t\tvar t0=performance.now();\n\t\t\tcard.classList.remove('on');\n\t\t\ttween=function(now){\n\t\t\t\t\/* clamped: a timestamp before t0 would extrapolate the curve\n\t\t\t\t   backwards and throw the camera out of the room *\/\n\t\t\t\tvar p=Math.max(0,Math.min((now-t0)\/dur,1)), e=ease(p);\n\t\t\t\tvar a=from.clone().lerp(mid,e), b=mid.clone().lerp(to,e);\n\t\t\t\tcamPos.copy(a.lerp(b,e));\n\t\t\t\tcamLook.lerpVectors(fromL,toL,e);\n\t\t\t\tcamera.fov=BASE_FOV+Math.sin(p*Math.PI)*6.5; camera.updateProjectionMatrix();\n\t\t\t\tcamRoll = Math.sin(p*Math.PI)*0.035;\n\t\t\t\tif(p>=1){ camera.fov=BASE_FOV; camera.updateProjectionMatrix(); tween=null; arrive(i); }\n\t\t\t};\n\t\t\tcur=i;\n\t\t\tif(typeof setCardOpen==='function') setCardOpen(false);\n\t\t\tif(window.MQR && MQR.Sound) MQR.Sound.move();\n\t\t\tdots.forEach(function(d,di){ d.classList.toggle('on',di===i); });\n\t\t\tvar dEl=dots[i]; if(dEl&&dEl.scrollIntoView) dEl.scrollIntoView({block:'nearest',inline:'nearest',behavior:'smooth'});\n\t\t\tsEl.textContent=pad2(i+1); numEl.textContent=pad2(i+1);\n\t\t\t\/* screen readers get the same information the camera move conveys *\/\n\t\t\tif(liveEl){ liveEl.textContent = SR_STOP\n\t\t\t\t.replace('%1$s', String(i+1)).replace('%2$s', String(stops.length))\n\t\t\t\t.replace('%3$s', data[i][0]) + '. ' + data[i][2]; }\n\t\t\tlEl.textContent=data[i][0]; tEl.textContent=data[i][1]; bEl.textContent=data[i][2];\n\t\t\t\/* the spec tags are rendered once, as the animated checkmark list below *\/\n\t\t\tchEl.innerHTML=''; chEl.style.display='none';\n\t\t\tvar shots=data[i][5]||[];\n\t\t\tshotsEl.innerHTML = shots.length\n\t\t\t\t? shots.slice(0,6).map(function(u,si){ return '<a href=\"'+u+'\" target=\"_blank\" rel=\"noopener\" style=\"animation-delay:'+(si*60)+'ms\"><img decoding=\"async\" src=\"'+u+'\" alt=\"\" loading=\"lazy\"><\/a>'; }).join('')\n\t\t\t\t: '';\n\t\t\tshotsEl.style.display = shots.length ? 'flex' : 'none';\n\t\t\tif(data[i][4]){ moreEl.href=data[i][4]; moreEl.hidden=false; } else { moreEl.hidden=true; }\n\n\t\t\t\/* animated feature bullets (reuse the spec chips as features) *\/\n\t\t\tvar featEl=document.getElementById('arxFeat');\n\t\t\tif(featEl){\n\t\t\t\tfeatEl.innerHTML=(data[i][3]||[]).map(function(f){\n\t\t\t\t\treturn '<li><i><\/i><span>'+f+'<\/span><\/li>'; }).join('');\n\t\t\t}\n\t\t\t\/* real CTAs \u2014 every button performs an actual action *\/\n\t\t\tvar ctaEl=document.getElementById('arxCta');\n\t\t\tif(ctaEl){\n\t\t\t\tvar html='';\n\t\t\t\tif(data[i][4]) html+='<a class=\"primary\" href=\"'+data[i][4]+'\">Explore Solution <span class=\"arr\">&rarr;<\/span><\/a>';\n\t\t\t\thtml+='<a class=\"ghost\" href=\"https:\/\/digitalmqr.com\/ar\/contact\/\">Request a Demo<\/a>';\n\t\t\t\thtml+='<button type=\"button\" class=\"ghost inspect\" data-inspect=\"1\">Inspect<\/button>';\n\t\t\t\tctaEl.innerHTML = i===0 ? '' : html;\n\t\t\t}\n\t\t\t\/* make the selected product visually dominant *\/\n\t\t\tif(window.MQR && MQR.Focus){ i===0 ? MQR.Focus.clear() : MQR.Focus.set(i); }\n\t\t\tif(window.MQR && MQR.Hotspots){ MQR.Hotspots.setActive(i===0?-1:i); }\n\t\t\tfocusRing.position.set(s.look[0],.09,s.look[2]);\n\t\t\tfocusRing.material.opacity=i===0?0:.8;\n\t\t}\n\t\tfunction arrive(i){\n\t\t\tcard.classList.add('on');\n\t\t\tvar off=camPos.clone().sub(camLook);\n\t\t\torb.r=off.length();\n\t\t\torb.th=Math.atan2(off.x,off.z);\n\t\t\torb.ph=Math.acos(Math.max(-1,Math.min(1,off.y\/orb.r)));\n\t\t\t\/* nothing is queued here \u2014 arriving at a station is the end of the move *\/\n\t\t\tif(typeof DOF!=='undefined') DOF.target(i===0 ? 0 : 1);\n\t\t\tif(MQR.Micro){ if(i===0) MQR.Micro.stop(); else MQR.Micro.play(FOCUS_TARGETS[i]); }\n\t\t\tif(window.MQR && MQR.Sound) MQR.Sound.land();\n\t\t\tif(window.MQR && MQR.Guide) MQR.Guide.onArrive(i);\n\t\t\tif(window.MQR && MQR.AR) MQR.AR.set(i);\n\t\t\tif(window.MQR && MQR.Map) MQR.Map.setActive(i);\n\t\t}\n\t\tvar playIc=document.getElementById('arxPlayIc');\n\t\t\/* legacy hook: kept so every existing call-site stays valid. There is no\n\t\t   auto-advance timer any more, so this only guarantees the flag is clear. *\/\n\t\tfunction stopAuto(){ auto=false; if(autoT){ clearTimeout(autoT); autoT=null; } }\n\n\t\tdocument.getElementById('arxNext').addEventListener('click',function(){ stopAuto(); goTo(cur+1); });\n\t\tdocument.getElementById('arxPrev').addEventListener('click',function(){ stopAuto(); goTo(cur-1); });\n\t\tdocument.getElementById('arxPlay').addEventListener('click',function(){\n\t\t\tif(MQR.Guide.active()) MQR.Guide.stop(); else MQR.Guide.start();\n\t\t});\n\t\tdots.forEach(function(d){ d.addEventListener('click',function(){\n\t\t\tstopAuto();\n\t\t\tif(PLAN.active()){ PLAN.exit(); MQR.Hotspots.show(true);\n\t\t\t\tstage.classList.remove('arx-plan');\n\t\t\t\tdocument.getElementById('arxBtnMap').setAttribute('aria-pressed','false'); }\n\t\t\tgoTo(parseInt(d.dataset.i,10)||0);\n\t\t}); });\n\t\texitBtn.addEventListener('click',function(){\n\t\t\tstopAuto(); card.classList.remove('on');\n\t\t\tif(window.MQR){ MQR.Focus.clear(); MQR.Hotspots.setActive(-1); }\n\t\t\tdocument.getElementById('arxIntro').classList.remove('off');\n\t\t\texitBtn.hidden=true;\n\t\t});\n\n\t\tfunction applyOrb(){\n\t\t\tcamPos.set(\n\t\t\t\tcamLook.x+orb.r*Math.sin(orb.ph)*Math.sin(orb.th),\n\t\t\t\tcamLook.y+orb.r*Math.cos(orb.ph),\n\t\t\t\tcamLook.z+orb.r*Math.sin(orb.ph)*Math.cos(orb.th));\n\t\t}\n\t\tvar down=false,lx=0,ly=0,moved=false;\n\t\tcanvas.addEventListener('pointerdown',function(e){ down=true;moved=false;lx=e.clientX;ly=e.clientY;canvas.classList.add('drag'); });\n\t\twindow.addEventListener('pointerup',function(e){\n\t\t\tcanvas.classList.remove('drag');\n\t\t\tif(down&&!moved) pick(e);\n\t\t\tdown=false;\n\t\t});\n\t\twindow.addEventListener('pointermove',function(e){\n\t\t\tif(!down||tween||PLAN.active()) return;\n\t\t\tvar dx=e.clientX-lx, dy=e.clientY-ly; lx=e.clientX; ly=e.clientY;\n\t\t\tif(Math.abs(dx)+Math.abs(dy)>3){ moved=true; if(INSPECT.active()) INSPECT.stopSpin(); }\n\t\t\torb.th-=dx*.0042; orb.ph=Math.max(.3,Math.min(1.45,orb.ph-dy*.0032));\n\t\t\thint.classList.add('off');\n\t\t\tapplyOrb();\n\t\t});\n\t\tcanvas.addEventListener('wheel',function(e){\n\t\t\te.preventDefault(); if(tween||PLAN.active()) return;\n\t\t\torb.r=Math.max(ORB_MIN,Math.min(ORB_MAX,orb.r*(1+e.deltaY*.0011))); applyOrb();\n\t\t},{passive:false});\n\t\tvar pinchD=0;\n\t\tcanvas.addEventListener('touchstart',function(e){\n\t\t\tif(e.touches.length===2) pinchD=Math.hypot(e.touches[0].clientX-e.touches[1].clientX,e.touches[0].clientY-e.touches[1].clientY);\n\t\t},{passive:true});\n\t\tcanvas.addEventListener('touchmove',function(e){\n\t\t\tif(e.touches.length===2 && !tween && !PLAN.active()){\n\t\t\t\tvar d=Math.hypot(e.touches[0].clientX-e.touches[1].clientX,e.touches[0].clientY-e.touches[1].clientY);\n\t\t\t\tif(pinchD>0&&d>0){ orb.r=Math.max(ORB_MIN,Math.min(ORB_MAX,orb.r*pinchD\/d)); applyOrb(); }\n\t\t\t\tpinchD=d;\n\t\t\t}\n\t\t},{passive:true});\n\t\tcanvas.addEventListener('touchend',function(){ pinchD=0; },{passive:true});\n\n\t\tvar ray=new THREE.Raycaster(), ptr=new THREE.Vector2();\n\t\tfunction pick(e){\n\t\t\tvar r=canvas.getBoundingClientRect();\n\t\t\tptr.x=((e.clientX-r.left)\/r.width)*2-1; ptr.y=-((e.clientY-r.top)\/r.height)*2+1;\n\t\t\tray.setFromCamera(ptr,camera);\n\t\t\t\/* DOM hotspots handle their own clicks; picking kept for future object hits *\/\n\t\t}\n\n\t\t\/* no pop-in: the lightmap already contains these objects' shadows,\n\t\t   so they cannot arrive after their own shadows do *\/\n\t\tparts.forEach(function(g){ g.scale.setScalar(1); g.userData.entry=1; });\n\t\tvar entered=false, entryT0=0;\n\t\tdocument.getElementById('arxEnter').addEventListener('click',function(){\n\t\t\tfitStage(); snapStage();\n\t\t\tdocument.getElementById('arxIntro').classList.add('off');\n\t\t\texitBtn.hidden=false;\n\t\t\t\/* Scene 1\u20132 play over black; when Intro signals ready the room\n\t\t\t   materialises (Scene 3) and the camera glides in (Scene 4). *\/\n\t\t\tMQR.Intro.play(document.getElementById('arx'), function(){\n\t\t\t\tentered=true; entryT0=performance.now();\n\t\t\t\tsetTimeout(function(){ goTo(0); }, 420);\n\t\t\t\t\/* NO auto-advance. The camera only moves when the visitor asks it to:\n\t\t\t\t   Prev\/Next, a station pill, a hotspot, the floor map, the arrow keys,\n\t\t\t\t   or by explicitly starting the guided tour. *\/\n\t\t\t});\n\t\t});\n\n\t\t\/* keyboard navigation: arrows move stations, Esc exits *\/\n\t\tdocument.addEventListener('keydown', function(e){\n\t\t\tif(!entered) return;\n\t\t\tif(e.key==='ArrowRight'){ stopAuto(); goTo(cur+1); }\n\t\t\telse if(e.key==='ArrowLeft'){ stopAuto(); goTo(cur-1); }\n\t\t\telse if(e.key==='Escape'){\n\t\t\t\t\/* close whatever is open, innermost first *\/\n\t\t\t\tif(INSPECT.active()){ exitInspect(); }\n\t\t\t\telse if(MQR.Transform.busy()){ MQR.Transform.cancel(); }\n\t\t\t\telse if(MQR.Guide.active()){ MQR.Guide.stop(); }\n\t\t\t\telse if(PLAN.active() || MQR.Map.isOpen()){ closeMap(); }\n\t\t\t\telse if(document.fullscreenElement){ document.exitFullscreen(); }\n\t\t\t\telse { exitBtn.click(); }\n\t\t\t}\n\t\t});\n\n\t\t\/* The site header is position:sticky, so it permanently eats the top of the\n\t\t   viewport. Measure it (plus the WP admin bar) and hand the value to CSS so\n\t\t   the stage is exactly as tall as the space the visitor can actually see \u2014\n\t\t   no scrolling needed to reach the controls. *\/\n\t\tvar _lastTop=-1,_lastH=-1;\n\t\tfunction fitStage(){\n\t\t\tvar top=0, el, cs;\n\t\t\tel=document.querySelector('.header');\n\t\t\tif(el){ cs=getComputedStyle(el);\n\t\t\t\tif(cs.position==='sticky'||cs.position==='fixed') top+=el.offsetHeight; }\n\t\t\tel=document.getElementById('wpadminbar');\n\t\t\tif(el){ cs=getComputedStyle(el);\n\t\t\t\tif(cs.position==='fixed') top+=el.offsetHeight; }\n\t\t\tif(document.fullscreenElement) top=0;\n\n\t\t\t\/* `root` is the non-sticky wrapper, so its rect gives the stage's natural\n\t\t\t   top even after the stage has pinned. Anything above it that is still\n\t\t\t   on screen (the promo strip) is respected; once it scrolls away the\n\t\t\t   stage grows into the freed space. *\/\n\t\t\tvar natural = document.fullscreenElement ? 0 : root.getBoundingClientRect().top;\n\t\t\tvar eff = Math.max(top, Math.min(natural, Math.round(window.innerHeight*.5)));\n\t\t\tvar h = Math.max(420, window.innerHeight - eff);\n\t\t\tif(top!==_lastTop){ root.style.setProperty('--arx-top', top+'px'); _lastTop=top; }\n\t\t\tif(h!==_lastH){ root.style.setProperty('--arx-h', h+'px'); _lastH=h; }\n\t\t\tvar bar=document.getElementById('arxBar');\n\t\t\tif(bar) root.style.setProperty('--arx-barh', bar.offsetHeight+'px');\n\t\t\treturn h;\n\t\t}\n\t\t\/* rAF-throttled scroll listener: one measure + at most two style writes *\/\n\t\tvar _fitQ=false;\n\t\twindow.addEventListener('scroll',function(){\n\t\t\tif(_fitQ) return; _fitQ=true;\n\t\t\trequestAnimationFrame(function(){ _fitQ=false; var h=fitStage();\n\t\t\t\tif(Math.abs(h-stage.clientHeight)>1) resize(); });\n\t\t},{passive:true});\n\n\t\t\/* ============================================================\n\t\t   PHASE 4 WIRING \u2014 every control below performs a real action\n\t\t   ============================================================ *\/\n\t\tvar labelsAll=data.map(function(d){ return d[0]; });\n\n\t\tMQR.AR.init(document.getElementById('arxAR'));\n\t\tMQR.Guide.init(document.getElementById('arxGuide'), function(i){ goTo(i); });\n\t\tMQR.Map.init(document.getElementById('arxMap'), labelsAll, function(i){\n\t\t\t\/* picking a room lands you there and hands the view back to the card *\/\n\t\t\tMQR.Guide.stop(); closeMap(); goTo(i);\n\t\t});\n\t\tfunction closeMap(){\n\t\t\tvar b=document.getElementById('arxBtnMap');\n\t\t\tif(b) b.setAttribute('aria-pressed','false');\n\t\t\tstage.classList.remove('arx-mapping','arx-plan');\n\t\t\tMQR.Map.toggle(false);\n\t\t\tif(PLAN && PLAN.active()){\n\t\t\t\tvar back=PLAN.exit();\n\t\t\t\tMQR.Hotspots.show(true);\n\t\t\t\tgoTo(back>=0?back:0);\n\t\t\t}\n\t\t}\n\n\t\t\/* project a THREE-space point to stage pixels (shared by the AR overlay) *\/\n\t\tvar _pv=new THREE.Vector3();\n\t\tfunction projectToStage(arr){\n\t\t\t_pv.set(arr[0],arr[1],arr[2]).project(camera);\n\t\t\tif(_pv.z>1) return null;\n\t\t\treturn { x:(_pv.x*.5+.5)*stage.clientWidth, y:(-_pv.y*.5+.5)*stage.clientHeight };\n\t\t}\n\n\t\tvar btnMap=document.getElementById('arxBtnMap');\n\t\tbtnMap.addEventListener('click',function(){\n\t\t\tif(PLAN.active()){ closeMap(); return; }\n\t\t\t\/* everything that assumes an eye-level camera steps aside *\/\n\t\t\tMQR.Guide.stop(); MQR.AR.clear(); MQR.Micro.stop();\n\t\t\tMQR.Focus.clear(); MQR.Hotspots.setActive(-1); MQR.Hotspots.show(false);\n\t\t\tDOF.target(0);\n\t\t\tcard.classList.remove('on');\n\t\t\thint.classList.add('off');\n\t\t\tPLAN.enter(cur);\n\t\t\tbtnMap.setAttribute('aria-pressed','true');\n\t\t\tstage.classList.add('arx-plan');\n\t\t});\n\t\tdocument.getElementById('arxMapX').addEventListener('click',closeMap);\n\n\t\tdocument.getElementById('arxBtnReset').addEventListener('click',function(){\n\t\t\tMQR.Guide.stop(); MQR.Transform.cancel(); MQR.AR.clear(); closeMap();\n\t\t\tif(INSPECT.active()) INSPECT.exit();\n\t\t\tif(typeof DOF!=='undefined') DOF.target(0);\n\t\t\tMQR.Focus.clear(); MQR.Hotspots.setActive(-1);\n\t\t\tgoTo(0);\n\t\t});\n\n\t\t\/* --- sound: off until asked for, remembered for the session ---- *\/\n\t\tvar btnSound=document.getElementById('arxBtnSound');\n\t\tbtnSound.addEventListener('click',function(){\n\t\t\tvar s=MQR.Sound.toggle();\n\t\t\tbtnSound.setAttribute('aria-pressed', s?'true':'false');\n\t\t\tbtnSound.setAttribute('aria-label', s\n\t\t\t\t? \"Turn sound off\"\t\t\t\t: \"Turn sound on\");\n\t\t});\n\t\t\/* a soft cue on every control, only ever audible once sound is on *\/\n\t\tdocument.querySelectorAll('.arx-bar button, .arx-dots button, .arxGBtns button')\n\t\t\t.forEach(function(b){ b.addEventListener('click',function(){ MQR.Sound.tap(); }); });\n\n\t\t\/* --- AR: shown only when the device really supports it ---------- *\/\n\t\t\/* --- mobile card sheet ----------------------------------------- *\/\n\t\tvar cardToggle=document.getElementById('arxCardToggle');\n\t\tvar CARD_MORE=\"Show details\",\n\t\t\tCARD_LESS=\"Hide details\";\n\t\tfunction setCardOpen(v){\n\t\t\tcard.classList.toggle('open', !!v);\n\t\t\tcardToggle.setAttribute('aria-expanded', v?'true':'false');\n\t\t\tcardToggle.setAttribute('aria-label', v?CARD_LESS:CARD_MORE);\n\t\t}\n\t\tcardToggle.addEventListener('click',function(){\n\t\t\tsetCardOpen(!card.classList.contains('open'));\n\t\t\tif(card.classList.contains('open')) card.scrollTop=0;\n\t\t});\n\n\t\t\/* delegated: the CTA row is rebuilt on every station change *\/\n\t\tdocument.getElementById('arxCta').addEventListener('click',function(e){\n\t\t\tvar b=e.target.closest && e.target.closest('[data-inspect]');\n\t\t\tif(!b) return;\n\t\t\te.preventDefault();\n\t\t\tMQR.Sound.tap();\n\t\t\tif(INSPECT.active()) exitInspect(); else INSPECT.enter(cur);\n\t\t});\n\t\tfunction exitInspect(){\n\t\t\tvar back=INSPECT.exit();\n\t\t\tif(back>=0) goTo(back);\n\t\t}\n\n\t\tvar btnAR=document.getElementById('arxBtnAR');\n\t\tvar arNote=document.createElement('div');\n\t\tarNote.className='arx-ar-note'; arNote.hidden=true; stage.appendChild(arNote);\n\t\tfunction arSay(msg){\n\t\t\tarNote.textContent=msg; arNote.hidden=false;\n\t\t\tclearTimeout(arNote._t); arNote._t=setTimeout(function(){ arNote.hidden=true; }, 5000);\n\t\t}\n\t\t\/* ?ardebug=1 reveals the button whatever the device says and reports the\n\t\t   capability check, so a phone that shows nothing can be told apart from\n\t\t   a phone where something is broken. *\/\n\t\tvar AR_DEBUG=\/[?&]ardebug=1\/.test(location.search);\n\t\tMQR.XR.probe().then(function(ok){\n\t\t\tif(ok || AR_DEBUG) btnAR.hidden=false;\n\t\t\tif(AR_DEBUG){\n\t\t\t\tarSay('navigator.xr: '+(navigator.xr?'yes':'NO')\n\t\t\t\t\t+' \u00b7 immersive-ar: '+(ok?'supported':'not supported')\n\t\t\t\t\t+' \u00b7 '+(window.isSecureContext?'https ok':'INSECURE CONTEXT')\n\t\t\t\t\t+' \u00b7 '+navigator.userAgent.slice(0,42));\n\t\t\t}\n\t\t});\n\t\tbtnAR.addEventListener('click',function(){\n\t\t\tif(MQR.XR.active()){ MQR.XR.exit(); return; }\n\t\t\tMQR.Guide.stop(); MQR.Transform.cancel(); closeMap(); MQR.AR.clear();\n\t\t\tMQR.Focus.clear(); MQR.Hotspots.setActive(-1); card.classList.remove('on');\n\t\t\tMQR.XR.enter({\n\t\t\t\tstage:stage, renderer:renderer, world:world,\n\t\t\t\tshell:[wb,wl,wr,wf,ceil,floor].concat(BAKED?[BAKED]:[]),\n\t\t\t\tframe:function(){\n\t\t\t\t\tvar t=clk.getElapsedTime();\n\t\t\t\t\tif(rm.userData.shapes) rm.userData.shapes.rotation.y=t*.25;\n\t\t\t\t\trenderer.render(scene,camera);\n\t\t\t\t},\n\t\t\t\tonEnter:function(){\n\t\t\t\t\tstage.classList.add('arx-inxr');\n\t\t\t\t\tbtnAR.setAttribute('aria-pressed','true');\n\t\t\t\t\tarSay(\"Point your phone at the floor, then walk around the classroom.\");\n\t\t\t\t},\n\t\t\t\tonExit:function(){\n\t\t\t\t\tstage.classList.remove('arx-inxr');\n\t\t\t\t\tbtnAR.setAttribute('aria-pressed','false');\n\t\t\t\t\tkick(); resize(); goTo(0);\n\t\t\t\t},\n\t\t\t\tonFail:function(m){\n\t\t\t\t\tarSay(\"AR could not start on this device.\" + (m?' ('+m+')':''));\n\t\t\t\t}\n\t\t\t});\n\t\t});\n\n\t\tvar btnFull=document.getElementById('arxBtnFull');\n\t\tbtnFull.addEventListener('click',function(){\n\t\t\tif(document.fullscreenElement){ document.exitFullscreen && document.exitFullscreen(); }\n\t\t\telse if(stage.requestFullscreen){ stage.requestFullscreen().catch(function(){}); }\n\t\t});\n\t\tif(!stage.requestFullscreen) btnFull.style.display='none';\n\n\t\t\/* --- TRANSFORM CLASSROOM ------------------------------------ *\/\n\t\tMQR.Transform.init({\n\t\t\tstage:stage,\n\t\t\tparts:parts,\n\t\t\tamb:ambL,\n\t\t\tsun:sun, fill:fillL,\n\t\t\tlabels:{\n\t\t\t\ta:\"An ordinary classroom\",\n\t\t\t\taSub:\"Four walls and a board\",\n\t\t\t\tb:\"The board comes alive\",\n\t\t\t\tbSub:\"Interactive panel online\",\n\t\t\t\tc:\"The room fills with technology\",\n\t\t\t\tcSub:\"Devices connecting\",\n\t\t\t\td:\"Reality gets an upgrade\",\n\t\t\t\tdSub:\"AR and VR activated\",\n\t\t\t\te:\"This is a Digital MQR smart classroom\",\n\t\t\t\teSub:\"14 technologies, one room\"\t\t\t},\n\t\t\tbefore:function(){\n\t\t\t\tMQR.Guide.stop(); MQR.AR.clear(); MQR.Focus.clear();\n\t\t\t\tMQR.Hotspots.setActive(-1); closeMap();\n\t\t\t\tif(MQR.Micro) MQR.Micro.stop();\n\t\t\t\tcard.classList.remove('on');\n\t\t\t\tgoTo(0);\n\t\t\t},\n\t\t\tdone:function(){ goTo(0); }\n\t\t});\n\t\tdocument.addEventListener('visibilitychange',function(){\n\t\t\tif(document.hidden && MQR.Transform.busy()) MQR.Transform.cancel();\n\t\t});\n\t\tdocument.getElementById('arxBtnTransform').addEventListener('click',function(){\n\t\t\tif(!entered){ document.getElementById('arxEnter').click(); setTimeout(function(){ MQR.Transform.run(); },5200); return; }\n\t\t\tMQR.Transform.run();\n\t\t});\n\n\t\t\/* extra keyboard shortcuts \u2014 all documented in the hint line *\/\n\t\tdocument.addEventListener('keydown',function(e){\n\t\t\tif(!entered) return;\n\t\t\tif(e.metaKey||e.ctrlKey||e.altKey) return;\n\t\t\tvar tag=(e.target&&e.target.tagName||'').toLowerCase();\n\t\t\tif(tag==='input'||tag==='textarea'||tag==='select'||e.target.isContentEditable) return;\n\t\t\tvar k=(e.key||'').toLowerCase();\n\t\t\tif(k==='m'){ btnMap.click(); }\n\t\t\telse if(k==='t'){ document.getElementById('arxBtnTransform').click(); }\n\t\t\telse if(k==='g'){ document.getElementById('arxPlay').click(); }\n\t\t\telse if(k==='f'){ btnFull.click(); }\n\t\t\telse if(k==='i'){ if(INSPECT.active()) exitInspect(); else INSPECT.enter(cur); }\n\t\t});\n\n\t\t\/* The page has a promo strip above the sticky header, so at scroll-top the\n\t\t   stage starts 40px lower than the space we sized it for. Snap it flush so\n\t\t   the whole experience \u2014 including the control bar \u2014 is on screen at once. *\/\n\t\tfunction snapStage(){\n\t\t\tvar top=parseFloat(getComputedStyle(root).getPropertyValue('--arx-top'))||0;\n\t\t\tvar want=stage.getBoundingClientRect().top+window.pageYOffset-top;\n\t\t\tif(Math.abs(window.pageYOffset-want)<3) return;\n\t\t\ttry{ window.scrollTo({top:want,behavior:'smooth'}); }catch(err){ window.scrollTo(0,want); }\n\t\t}\n\t\tfunction resize(){\n\t\t\tfitStage();\n\t\t\tvar w=stage.clientWidth,h=stage.clientHeight;\n\t\t\trenderer.setSize(w,h,false);\n\t\t\tcamera.aspect=w\/Math.max(h,1); camera.updateProjectionMatrix();\n\t\t\tif(typeof DOF!=='undefined') DOF.resize(w,h);\n\t\t\tif(typeof PLAN!=='undefined') PLAN.resize();\n\t\t\t\/* a narrower frame needs the camera further back to hold the\n\t\t\t   same product \u2014 recompute rather than crop *\/\n\t\t\tif(typeof applyFrames==='function') applyFrames();\n\t\t}\n\t\twindow.addEventListener('resize',resize);\n\t\twindow.addEventListener('orientationchange',function(){ setTimeout(resize,180); });\n\t\tdocument.addEventListener('fullscreenchange',function(){\n\t\t\tsetTimeout(function(){ resize(); if(!document.fullscreenElement) snapStage(); },60);\n\t\t\tvar b=document.getElementById('arxBtnFull');\n\t\t\tif(b) b.setAttribute('aria-pressed', document.fullscreenElement?'true':'false');\n\t\t});\n\t\tDOF.init();\n\t\tPLAN.init();\n\t\tif(document.fonts && document.fonts.ready) document.fonts.ready.then(resize);\n\t\tresize();\n\t\t\/* the control bar's height depends on webfont metrics \u2014 re-measure once\n\t\t   they have landed so the card and pills sit at the right offsets *\/\n\t\tsetTimeout(resize, 400);\n\n\t\t\/* Stop rendering when nobody is looking: another tab, or the stage\n\t\t   scrolled out of view. Saves battery and keeps the fans quiet. *\/\n\t\tvar visible=true, awake=true, rafId=0;\n\t\tfunction kick(){ if(!rafId) rafId=requestAnimationFrame(loop); }\n\t\tdocument.addEventListener('visibilitychange',function(){\n\t\t\tawake=!document.hidden;\n\t\t\tif(awake) kick();\n\t\t});\n\t\tif(window.IntersectionObserver){\n\t\t\tnew IntersectionObserver(function(es){\n\t\t\t\tvisible = es[0].isIntersecting;\n\t\t\t\tif(visible && awake) kick();\n\t\t\t},{threshold:0.01}).observe(stage);\n\t\t}\n\n\t\tvar clk=new THREE.Clock();\n\t\tfunction loop(now){\n\t\t\trafId=0;\n\t\t\tif(!visible || !awake) return;          \/* pause; resumed by the hooks above *\/\n\t\t\trafId=requestAnimationFrame(loop);\n\t\t\tvar t=clk.getElapsedTime();\n\t\t\tnow=now||performance.now();\n\t\t\tif(tween) tween(now);\n\t\t\t\/* the staggered scale-in used to live here; see the note above *\/\n\t\t\t\/* living details *\/\n\t\t\tif(rm.userData.shapes){\n\t\t\t\trm.userData.shapes.children.forEach(function(sm){\n\t\t\t\t\tsm.position.y=.5+Math.sin(t*1.6+sm.userData.ph)*.12;\n\t\t\t\t\tsm.rotation.y+=.012; sm.rotation.x+=.006;\n\t\t\t\t});\n\t\t\t\trm.userData.shapes.rotation.y=t*.25;\n\t\t\t\trm.userData.game.rotation.z=t*.1;\n\t\t\t}\n\t\t\tif(rb.userData.bot){ rb.userData.bot.position.x=.75+Math.sin(t*.9)*.28; rb.userData.bot.rotation.y=Math.sin(t*.9+1.57)*.6; }\n\t\t\tif(rb.userData.printer){ rb.userData.printer.userData.head.position.x=Math.sin(t*2.2)*.12; }\n\t\t\tif(att.userData.scan){ att.userData.scan.position.y=1.47+Math.sin(t*2.6)*.1; }\n\t\t\tvar dr=world.userData.drone;\n\t\t\tif(dr){\n\t\t\t\tdr.position.x=4.9+Math.cos(t*.5)*.6; dr.position.z=-3.4+Math.sin(t*.5)*.6;\n\t\t\t\tdr.position.y=2.7+Math.sin(t*1.7)*.12; dr.rotation.y=-t*.5;\n\t\t\t\tdr.userData.rotors.forEach(function(ro){ ro.rotation.y+=.9; });\n\t\t\t}\n\t\t\tvar pp=world.userData.plane;\n\t\t\tif(pp){\n\t\t\t\tvar a=t*.42;\n\t\t\t\tpp.position.set(Math.cos(a)*5.6, 2.25+Math.sin(a*2)*.35, Math.sin(a)*3.4);\n\t\t\t\tpp.rotation.y=-a; pp.rotation.z=Math.sin(a*2)*.3;\n\t\t\t}\n\t\t\tif(aq.userData.fishes){\n\t\t\t\taq.userData.fishes.forEach(function(fs){\n\t\t\t\t\tvar ph=fs.userData.ph, fx=Math.sin(t*.7+ph)*.5;\n\t\t\t\t\tfs.position.x=fx; fs.position.y=.88+Math.sin(t*1.3+ph)*.06;\n\t\t\t\t\tfs.scale.x=(Math.cos(t*.7+ph)>=0)?1:-1;\n\t\t\t\t});\n\t\t\t\taq.userData.bubbles.forEach(function(bu){\n\t\t\t\t\tvar by=.7+((t*.25+bu.userData.ph)%1)*.35;\n\t\t\t\t\tbu.position.y=by; bu.material.opacity=.7*(1-((by-.7)\/.35));\n\t\t\t\t});\n\t\t\t}\n\t\t\tif(cat.userData.tail){ cat.userData.tail.rotation.z=.8+Math.sin(t*2.4)*.3; }\n\t\t\tvar bl2=world.userData.balloons;\n\t\t\tif(bl2){ bl2.rotation.z=Math.sin(t*.8)*.04; bl2.position.y=Math.sin(t*1.1)*.05; }\n\t\t\tvar dpArr=dust.geometry.attributes.position.array;\n\t\t\tfor(var du=0;du<dustN;du++){\n\t\t\t\tdpArr[du*3+1]+=.0022;\n\t\t\t\tif(dpArr[du*3+1]>3.2) dpArr[du*3+1]=0;\n\t\t\t}\n\t\t\tdust.geometry.attributes.position.needsUpdate=true;\n\t\t\tMQR.Hotspots.update(camPos);\n\t\t\tif(glowRing.visible){ glowRing.scale.setScalar(1+Math.sin(t*2.4)*.05); }\n\t\t\t\/* spotlight eases onto the current station *\/\n\t\t\tif(cur>0 && stops[cur]){\n\t\t\t\tvar L=stops[cur].look;\n\t\t\t\tspot.target.position.set(L[0],L[1]*.5,L[2]);\n\t\t\t\tspot.position.set(L[0], 5.2+Math.sin(t*1.3)*.15, L[2]+1.2);\n\t\t\t\tspot.intensity += ((tween?0.35:1.15) - spot.intensity)*0.05;\n\t\t\t} else { spot.intensity += (0 - spot.intensity)*0.06; }\n\t\t\tif(INSPECT.active()) INSPECT.tick(Math.min(0.05, clk.getDelta ? 0.0167 : 0.0167));\n\t\t\ttickScreens(now);\n\t\t\tif(MQR.Micro) MQR.Micro.tick(now);\n\n\t\t\tfocusRing.scale.setScalar(1+Math.sin(t*2.2)*.08);\n\t\t\t\/* Phase 4: keep the AR label pinned to its 3D anchor, and the\n\t\t\t   map's \"you are here\" dot on the real camera position. *\/\n\t\t\tif(MQR.AR) MQR.AR.tick(now, projectToStage);\n\t\t\tif(MQR.Map && MQR.Map.isOpen()) MQR.Map.follow(camPos);\n\t\t\tif(typeof PLAN!=='undefined') PLAN.tick(now);\n\t\t\t\/* Keep the camera inside the room. The walls face inward only, so\n\t\t\t   the moment the camera crosses one they are backface-culled and\n\t\t\t   the frame fills with empty background \u2014 which is what made the\n\t\t\t   wall flicker on the way into corner stations. The plan view is\n\t\t\t   exempt: it deliberately flies far overhead. *\/\n\t\t\t\/* Only clamp at product stations. Station 0 is the dollhouse\n\t\t\t   overview and is DELIBERATELY outside the room looking in \u2014\n\t\t\t   clamping it dragged the camera to the back wall and zoomed the\n\t\t\t   whole overview in. *\/\n\t\t\tif(cur>0 && !(typeof PLAN!=='undefined' && PLAN.active()) && !MQR.XR.active()){\n\t\t\t\tcamPos.x=Math.max(-RX+0.45, Math.min(RX-0.45, camPos.x));\n\t\t\t\tcamPos.z=Math.max(-RZ+0.45, Math.min(RZ-0.45, camPos.z));\n\t\t\t\tcamPos.y=Math.max(0.55, Math.min(3.30, camPos.y));\n\t\t\t}\n\t\t\tcamera.position.copy(camPos);\n\t\t\t\/* the breathing sway is an eye-level touch; a plan view should be still *\/\n\t\t\tif(!(typeof PLAN!=='undefined' && PLAN.active())) camera.position.y+=Math.sin(t*1.1)*.02;\n\t\t\tcamera.lookAt(camLook);\n\t\t\tcamRoll *= 0.94;\n\t\t\tcamera.rotation.z += camRoll;\n\t\t\t\/* hand the frame to the composer only while there is blur to draw *\/\n\t\t\tif(!DOF.render(camera.position.distanceTo(camLook))) renderer.render(scene,camera);\n\t\t}\n\t\tkick();\n\t};\n\t<\/script>\n\t<\/div>\n\t\t\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t<\/section>\n\t\t\t\t<\/div>\n\t\t","protected":false},"excerpt":{"rendered":"","protected":false},"author":0,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"elementor_header_footer","meta":{"footnotes":""},"class_list":["post-431","page","type-page","status-publish","hentry"],"_links":{"self":[{"href":"https:\/\/digitalmqr.com\/ar\/wp-json\/wp\/v2\/pages\/431","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/digitalmqr.com\/ar\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/digitalmqr.com\/ar\/wp-json\/wp\/v2\/types\/page"}],"replies":[{"embeddable":true,"href":"https:\/\/digitalmqr.com\/ar\/wp-json\/wp\/v2\/comments?post=431"}],"version-history":[{"count":1,"href":"https:\/\/digitalmqr.com\/ar\/wp-json\/wp\/v2\/pages\/431\/revisions"}],"predecessor-version":[{"id":433,"href":"https:\/\/digitalmqr.com\/ar\/wp-json\/wp\/v2\/pages\/431\/revisions\/433"}],"wp:attachment":[{"href":"https:\/\/digitalmqr.com\/ar\/wp-json\/wp\/v2\/media?parent=431"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}