Просмотр исходного кода

изменение структуры репозитория

Евгений 2 недель назад
Родитель
Сommit
d8802ef74c

+ 1025 - 0
ПМ.03.02/2.2.500 Ерошко/app.js

@@ -0,0 +1,1025 @@
+/* ===== js\theme.js ===== */
+/* Apply the palette before paint; keep the control and browser chrome in sync. */
+(() => {
+  const systemTheme = matchMedia('(prefers-color-scheme: dark)');
+  let selectedTheme;
+  try { selectedTheme = localStorage.getItem('zke-theme'); } catch { /* Use system preference. */ }
+  const isTheme = value => value === 'light' || value === 'dark';
+  function applyTheme(theme) {
+    document.documentElement.dataset.theme = theme;
+    document.querySelector('meta[name="theme-color"]').content = theme === 'dark' ? '#171a18' : '#f4f4ef';
+    const button = document.querySelector('#themeButton');
+    if (!button) return;
+    const label = theme === 'dark' ? 'Включить светлую тему' : 'Включить тёмную тему';
+    button.setAttribute('aria-label', label);
+    button.setAttribute('title', label);
+    button.setAttribute('aria-pressed', String(theme === 'dark'));
+  }
+  applyTheme(isTheme(selectedTheme) ? selectedTheme : systemTheme.matches ? 'dark' : 'light');
+  document.addEventListener('DOMContentLoaded', () => {
+    applyTheme(document.documentElement.dataset.theme);
+    document.querySelector('#themeButton').addEventListener('click', () => {
+      selectedTheme = document.documentElement.dataset.theme === 'dark' ? 'light' : 'dark';
+      applyTheme(selectedTheme);
+      try { localStorage.setItem('zke-theme', selectedTheme); } catch { /* Theme remains active for this session. */ }
+    });
+  });
+  systemTheme.addEventListener('change', event => {
+    if (!isTheme(selectedTheme)) applyTheme(event.matches ? 'dark' : 'light');
+  });
+})();
+
+/* ===== js\schematic.js ===== */
+/* Native vector reconstruction of 114.2.082.002 ПС.
+ * Component terminals and net membership are explicit so the drawing can be
+ * checked independently of its teaching animation. Coordinates follow the
+ * supplied drawing, with extra room around labels and the transformer.
+ */
+function buildLockSchematic() {
+  const pins = {}, elements = [], wires = [], nets = [], junctions = [];
+  const esc = value => String(value).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&apos;'}[c]));
+  const line = d => `<path d="${d}"/>`;
+  const text = (x,y,value,cls='',anchor='start') => `<text x="${x}" y="${y}" class="${cls}" text-anchor="${anchor}">${esc(value)}</text>`;
+  const register = (id, terminals, body, label, bounds, extra='') => {
+    pins[id] = terminals;
+    const [x,y,w,h] = bounds;
+    elements.push(`<g class="circuit-part ${extra}" data-part="${esc(id)}" tabindex="0" role="button" aria-label="${esc(id)}: показать описание"><title>${esc(id)} — назначение и состояние</title><rect class="part-hitbox" x="${x}" y="${y}" width="${w}" height="${h}" rx="5"/><g class="part-symbol">${body}</g>${label}</g>`);
+  };
+  function resistor(id,x,y,vertical=false,length=50,labelSide=1) {
+    const terminals = {A:[x,y],B:vertical?[x,y+length]:[x+length,y]};
+    const middle=length/2;
+    const body=vertical
+      ? line(`M${x} ${y}v${middle-14} M${x} ${y+middle+14}V${y+length}`)+`<rect x="${x-5}" y="${y+middle-14}" width="10" height="28"/>`
+      : line(`M${x} ${y}h${middle-14} M${x+middle+14} ${y}H${x+length}`)+`<rect x="${x+middle-14}" y="${y-5}" width="28" height="10"/>`;
+    const label=vertical ? text(x+labelSide*12,y+middle+4,id,'component-label',labelSide<0?'end':'start') : text(x+middle,y-13,id,'component-label','middle');
+    register(id,terminals,body,label,vertical?[x-10,y-4,20,length+8]:[x-4,y-10,length+8,20]);
+  }
+  function capacitor(id,x,y,vertical=false,length=40,polar=false,labelPosition=null) {
+    const middle=length/2, gap=4;
+    const body=vertical
+      ? line(`M${x} ${y}V${y+middle-gap} M${x-12} ${y+middle-gap}h24 M${x-12} ${y+middle+gap}h24 M${x} ${y+middle+gap}V${y+length}`)
+      : line(`M${x} ${y}H${x+middle-gap} M${x+middle-gap} ${y-12}v24 M${x+middle+gap} ${y-12}v24 M${x+middle+gap} ${y}H${x+length}`);
+    const [lx,ly] = labelPosition || (vertical?[x+18,y+middle-8]:[x+middle,y-19]);
+    const labels=text(lx,ly,id,'component-label',vertical?'start':'middle')+(polar?text(x-15,y+middle+19,'+','polarity','end'):'');
+    register(id,{A:[x,y],B:vertical?[x,y+length]:[x+length,y]},body,labels,vertical?[x-16,y-3,32,length+6]:[x-3,y-16,length+6,32]);
+  }
+  function diode(id,x,y,{vertical=false,up=false,length=45,labelPosition=null}={}) {
+    let body, terminals;
+    if (!vertical) {
+      const m=x+length/2;
+      body=line(`M${x} ${y}H${m-9} M${m-9} ${y-9}l18 9l-18 9Z M${m+9} ${y-11}v22 M${m+9} ${y}H${x+length}`);
+      terminals={A:[x,y],K:[x+length,y]};
+    } else {
+      const m=y+length/2;
+      body=up
+        ? line(`M${x} ${y}V${m-9} M${x-10} ${m-9}h20 M${x-9} ${m+9}l9 -18l9 18Z M${x} ${m+9}V${y+length}`)
+        : line(`M${x} ${y}V${m-9} M${x-9} ${m-9}l9 18l9 -18Z M${x-10} ${m+9}h20 M${x} ${m+9}V${y+length}`);
+      terminals=up?{K:[x,y],A:[x,y+length]}:{A:[x,y],K:[x,y+length]};
+    }
+    const [lx,ly]=labelPosition || (vertical?[x+16,y+length/2+4]:[x+length/2,y-16]);
+    register(id,terminals,body,text(lx,ly,id,'component-label',vertical?'start':'middle'),vertical?[x-14,y-3,28,length+6]:[x-3,y-14,length+6,28]);
+  }
+  function thyristor(id,y,stage) {
+    const x=660, m=y+17.5;
+    const body=line(`M${x} ${y}V${m-9} M${x-10} ${m-9}h20 M${x-9} ${m+9}l9 -18l9 18Z M${x} ${m+9}V${y+35} M638 ${y+5}h8l10 13`);
+    register(id,{K:[x,y],A:[x,y+35],G:[638,y+5]},body,text(678,m+5,id,'component-label'),[632,y-6,66,50],`memory-device" data-stage="${stage}`);
+  }
+  function transistor(id,cx,cy,{upperEmitter=false,pnp=true,labelPosition=null}={}) {
+    let body, terminals;
+    if (upperEmitter) {
+      // VT1 has its base on the right and its emitter at the top.
+      terminals={B:[cx+30,cy],E:[cx-10,cy-35],C:[cx-10,cy+35]};
+      body=`<circle cx="${cx}" cy="${cy}" r="20"/>`+line(`M${cx+10} ${cy-13}v26 M${cx+30} ${cy}H${cx+10} M${cx+10} ${cy-7}l-20 -13v-15 M${cx+10} ${cy+7}l-20 13v15`)
+        +`<path class="arrow-fill" d="M${cx-8} ${cy-19}l8 2l-4 5Z"/>`;
+    } else {
+      terminals={B:[cx-25,cy],C:[cx+10,cy-35],E:[cx+10,cy+35]};
+      body=`<circle cx="${cx}" cy="${cy}" r="20"/>`+line(`M${cx-10} ${cy-13}v26 M${cx-25} ${cy}H${cx-10} M${cx-10} ${cy-7}l20 -13v-15 M${cx-10} ${cy+7}l20 13v15`)
+        +`<path class="arrow-fill" d="${pnp?`M${cx-3} ${cy+11}l8 1l-4 5Z`:`M${cx+8} ${cy+19}l-8 -1l4 -5Z`}"/>`;
+    }
+    const [lx,ly]=labelPosition||[cx+23,cy-21];
+    register(id,terminals,body,text(lx,ly,id,'component-label'),[cx-29,cy-39,65,78],`transistor-device" data-device="${id}`);
+  }
+  function bridge(id,cx,cy) {
+    const r=28;
+    const body=line(`M${cx} ${cy-r}l${r} ${r}l-${r} ${r}l-${r} -${r}Z M${cx-9} ${cy-8}l9 16l9 -16Z M${cx-10} ${cy+9}h20`);
+    register(id,{MINUS:[cx,cy-r],PLUS:[cx,cy+r],AC1:[cx-r,cy],AC2:[cx+r,cy]},body,
+      text(cx-12,cy-r-13,id,'component-label')+text(cx+5,cy-r-2,'−','polarity')+text(cx+5,cy+r+13,'+','polarity')+text(cx-r-8,cy+4,'~','polarity','end')+text(cx+r+8,cy+4,'~','polarity'),[cx-38,cy-42,82,85]);
+  }
+  function net(id,connections,d,cls='',dots=[]) {
+    nets.push({id,connections,d});
+    wires.push(`<path class="net ${cls}" data-net="${id}" d="${d}"/>`);
+    dots.forEach(([x,y])=>junctions.push(`<circle class="junction ${cls}" cx="${x}" cy="${y}" r="2.5"/>`));
+  }
+
+  // S1 and plug field are drawn as actual contacts and connectors.
+  let keyboard='<rect class="device-outline" x="268" y="349" width="62" height="420" rx="3"/>'+text(280,370,'S1','component-label')+line('M245 385V790');
+  const keyPins={C:[245,790]}, plugPins={C:[376,830]};
+  let plugs='<rect class="device-outline" x="350" y="349" width="89" height="444" rx="3"/>';
+  for(let i=0;i<10;i++) {
+    const y=385+i*40, digit=(i+1)%10;
+    keyboard+=`<g data-key-contact="${digit}">${line(`M245 ${y}h33l13 -10 M297 ${y}H330`)}<circle cx="297" cy="${y}" r="1.8"/>${text(308,y-7,digit,'key-digit')}</g>`;
+    keyPins['K'+(i+1)]=[330,y]; plugPins['I'+(i+1)]=[400,y]; plugPins['O'+(i+1)]=[435,y];
+    plugs+=line(`M400 ${y}h7 M407 ${y-6}l6 6l-6 6 M417 ${y-6}l6 6l-6 6 M423 ${y}h12`)+text(393,y-12,`XP${i+1} / XS${i+1}`,'connector-label');
+  }
+  keyboard+=text(253,808,'13','pin-label');
+  plugs+=line('M376 810V830')+text(386,822,'13','pin-label');
+  register('S1',keyPins,keyboard,'',[235,344,100,467]);
+  register('XP/XS',plugPins,plugs,'',[343,344,99,490]);
+  [['R1',385],['R3',425],['R5',465],['R7',505]].forEach(([id,y])=>resistor(id,450,y));
+  [['R2',475],['R4',545],['R6',615]].forEach(([id,y])=>resistor(id,575,y));
+  resistor('R8',729,340,true);
+  resistor('R9',660,685,true,50,-1);
+  resistor('R11',660,765,true,50,-1);
+  resistor('R10',730,765,true,50,-1);
+  resistor('R12',835,685,true,50,-1);
+  resistor('R13',875,640,true,55,-1);
+  resistor('R14',875,765,true,50,-1);
+  resistor('R15',915,580,true,55);
+  resistor('R16',945,660,true,35,-1);
+  resistor('R17',955,765,true);
+  resistor('R18',1020,580,true);
+  resistor('R19',935,855);
+  resistor('R20',920,340,true);
+  capacitor('C1',835,745,true,60,true,[807,783]);
+  capacitor('C2',1070,710,true,80,true);
+  capacitor('C3',945,640,false,40,false,[968,616]);
+  capacitor('C4',620,493); capacitor('C5',620,565); capacitor('C6',620,637);
+  diode('VD1',555,610,{vertical:true,up:true,length:55,labelPosition:[548,647]});
+  diode('VD2',580,585,{labelPosition:[603,572]});
+  diode('VD3',565,680,{vertical:true,length:55,labelPosition:[582,717]});
+  diode('VD4',580,655,{labelPosition:[603,678]});
+  [440,510,580,650].forEach((y,i)=>thyristor('VD'+(5+i),y,i+1));
+  diode('VD9',885,375,{vertical:true,length:40,labelPosition:[901,418]});
+  transistor('VT1',670,390,{upperEmitter:true,pnp:false,labelPosition:[672,352]});
+  transistor('VT5',700,720,{labelPosition:[719,685]});
+  transistor('VT2',780,720,{labelPosition:[775,685]});
+  transistor('VT3',900,710,{labelPosition:[883,675]});
+  transistor('VT4',1010,710,{labelPosition:[1020,675]});
+  register('YA1',{A:[847,375],B:[847,415]},line('M847 375V385 M847 405V415')+'<rect x="831" y="385" width="32" height="20"/>',text(830,363,'YA1','component-label'),[825,371,44,48]);
+  register('HL1',{A:[920,435],B:[920,465]},'<circle class="lamp-bulb" cx="920" cy="450" r="15"/>'+line('M910 440l20 20 M910 460l20 -20'),text(945,454,'HL1','component-label'),[901,431,70,39]);
+  register('S2',{A:[980,535],B:[1025,535]},'<circle cx="980" cy="535" r="2.5"/><circle cx="1025" cy="535" r="2.5"/><path id="exitContact" d="M980 535L1015 519"/>'+line('M1005 508v11 M999 508h12'),text(1010,497,'S2','component-label'),[975,493,58,48]);
+  bridge('VD10',1160,560); bridge('VD11',1160,670);
+  const transformer=line('M1255 514V752 M1262 514V752 M1230 525h8q16 0 16 10t-16 10q16 0 16 10t-16 10q16 0 16 10t-16 10q16 0 16 10t-16 10h-8 M1230 650h8q16 0 16 10t-16 10q16 0 16 10t-16 10q16 0 16 10t-16 10q16 0 16 10t-16 10h-8 M1287 560h-4q-14 0 -14 10t14 10q-14 0 -14 10t14 10q-14 0 -14 10t14 10q-14 0 -14 10t14 10q-14 0 -14 10t14 10q-14 0 -14 10t14 10q-14 0 -14 10t14 10h4');
+  register('T1',{S5:[1230,525],S6:[1230,605],S4:[1230,650],S3:[1230,730],P1:[1287,560],P2:[1287,700]},transformer,text(1235,494,'T1','component-label')+text(1222,518,'5','pin-label')+text(1222,620,'6','pin-label')+text(1222,643,'4','pin-label')+text(1222,744,'3','pin-label')+text(1296,551,'1','pin-label')+text(1296,716,'2','pin-label'),[1216,489,96,273]);
+  register('FU1',{A:[1320,560],B:[1380,560]},line('M1320 560h14 M1366 560h14 M1334 560h32')+'<rect x="1334" y="555" width="32" height="10"/>',text(1350,542,'FU1','component-label','middle'),[1316,539,68,33]);
+
+  // All component terminals belong to exactly one named electrical net.
+  for(let i=0;i<10;i++)net('key-'+i,['S1.K'+(i+1),'XP/XS.I'+(i+1)],`M330 ${385+i*40}H400`,'key-net');
+  [['R1',385],['R3',425],['R5',465],['R7',505]].forEach(([id,y],i)=>nets.push({id:'plug-'+i,connections:['XP/XS.O'+(i+1),id+'.A'],d:`M435 ${y}H450`}));
+  net('common',['S1.C','XP/XS.C','R2.A','R4.A','R6.A','R11.B','R10.B','VT2.E','R14.B','C1.B','R17.B','C2.B','VD10.PLUS','VD11.PLUS','S2.B'],
+    'M245 790H360V830H1160 M376 830V810 M540 475V830 M540 475H575 M540 545H575 M540 615H575 M660 815V830 M730 815V830 M790 755V830 M835 805V830 M875 815V830 M955 815V830 M1070 790V830 M1160 588V610H1110V830 M1160 698V830 M1025 535H1090V830','common-net',[[376,830],[540,545],[540,615],[540,830],[660,830],[730,830],[790,830],[835,830],[875,830],[955,830],[1070,830],[1090,830],[1110,830]]);
+  net('stage-1-gate',['R1.B','VD5.G'],'M500 385H542V445H638','gate-net stage-route-1');
+  net('stage-2-gate',['R3.B','VD6.G','C4.A'],'M500 425H525V515H638 M620 493V515','gate-net stage-route-2',[[620,515]]);
+  net('stage-3-input',['R5.B','VD2.A','VD1.K'],'M500 465H508V585H580 M555 585V610','gate-net stage-route-3',[[555,585]]);
+  net('stage-3-gate',['VD2.K','VD7.G','C5.A'],'M625 585H638 M620 565V585H625','gate-net stage-route-3',[[625,585]]);
+  net('stage-4-input',['R7.B','VD4.A','VD3.A'],'M500 505H492V655H580 M565 655V680','gate-net stage-route-4',[[565,655]]);
+  net('stage-4-gate',['VD4.K','VD8.G','C6.A'],'M625 655H638 M620 637V655H625','gate-net stage-route-4',[[625,655]]);
+  net('memory-1',['VD5.A','VD6.K','R2.B','C4.B'],'M660 475V510 M625 475H660','memory-net',[[660,475],[660,493]]);
+  net('memory-2',['VD6.A','VD7.K','R4.B','C5.B'],'M660 545V580 M625 545H660','memory-net',[[660,545],[660,565]]);
+  net('memory-3',['VD7.A','VD8.K','R6.B','C6.B'],'M660 615V650 M625 615H660','memory-net',[[660,615],[660,637]]);
+  net('chain-supply',['VT1.C','VD5.K'],'M660 425V440','chain-supply');
+  net('full-code',['VD8.A','R9.A','R12.A'],'M660 685H675V665H835V685','full-code-net',[[660,685]]);
+  net('key-base',['R9.B','R11.A','VT5.B'],'M660 735V765 M660 750H675V720','driver-net',[[660,750]]);
+  net('darlington',['VT5.E','VT2.B','R10.A'],'M710 755H755V720 M730 755V765','driver-net',[[730,755]]);
+  net('load-return',['VT5.C','VT2.C','YA1.B','HL1.B','S2.A','VD9.K'],'M710 685V690H790V535H980 M790 535V685 M847 415V535 M920 465V535 M885 415H847','load-return-net',[[790,690],[847,535],[920,535],[847,415]]);
+  net('load-negative',['YA1.A','R20.A','VD9.A','VD10.MINUS'],'M847 375V310H1160V532 M920 310V340 M885 375H847','load-negative-net',[[920,310],[847,375]]);
+  net('lamp',['R20.B','HL1.A'],'M920 390V435','lamp-net');
+  net('logic-negative',['VT1.E','R8.A','R13.A','R15.A','C2.A','VD11.MINUS'],'M660 355V328H805V550H1070V710 M729 328V340 M875 550V640 M915 550V580 M1050 550V642H1160','logic-supply-net',[[729,328],[875,550],[915,550],[1050,550]]);
+  net('vt1-base',['VT1.B','R8.B','R18.A'],'M700 390H729V580H1020','bias-net',[[729,390]]);
+  net('timer-input',['R12.B','C1.A','R13.B','R14.A','VT3.B'],'M835 735V745H875 M875 695V765 M875 710H875','timer-net',[[835,745],[875,745],[875,710]]);
+  net('timer-emitter',['VT3.E','VT4.E','R17.A'],'M910 745V755H1020V745 M955 755V765','timer-net',[[955,755]]);
+  net('vt3-collector',['VT3.C','R15.B','R16.A','C3.A'],'M915 635V640H945V660 M910 675V640H915','timer-net',[[915,640],[945,640]]);
+  net('vt4-base',['VT4.B','R16.B'],'M945 695V710H985','timer-net');
+  net('vt4-collector',['VT4.C','R18.B'],'M1020 630V675','timer-net');
+  net('feedback',['R19.B','C3.B'],'M985 855H995V640H985','feedback-net');
+  net('error-bus',[...Array.from({length:6},(_,i)=>'XP/XS.O'+(i+5)),'VD1.A','VD3.K','R19.A'],'M435 545H450V855H935 M435 585H450 M435 625H450 M435 665H450 M435 705H450 M435 745H450 M555 665V855 M565 735V855','error-net',[[450,585],[450,625],[450,665],[450,705],[450,745],[555,855],[565,855]]);
+  net('ac-load-5',['VD10.AC1','T1.S5'],'M1230 525H1115V560H1132','ac-net');
+  net('ac-load-6',['VD10.AC2','T1.S6'],'M1230 605H1205V560H1188','ac-net');
+  net('ac-logic-4',['VD11.AC2','T1.S4'],'M1230 650H1205V670H1188','ac-net');
+  net('ac-logic-3',['VD11.AC1','T1.S3'],'M1230 730H1115V670H1132','ac-net');
+  net('mains-fuse',['T1.P1','FU1.A'],'M1287 560H1320','mains-net');
+  net('mains-19',['FU1.B'],'M1380 560H1410 M1404 555l6 5l-6 5','mains-net');
+  net('mains-18',['T1.P2'],'M1287 700H1410 M1404 695l6 5l-6 5','mains-net');
+
+  const styles=`
+    .vector-schematic{background:var(--panel-2,#f0f1ec);color:var(--text,#242823)}
+    .vector-schematic .net,.vector-schematic .part-symbol{fill:none;stroke:var(--wire,#7e897c);stroke-width:1.65;stroke-linecap:round;stroke-linejoin:round}
+    .vector-schematic text{font:12px monospace;fill:var(--text,#242823);stroke:var(--panel-2,#f0f1ec);stroke-width:3px;paint-order:stroke fill;stroke-linejoin:round}
+    .vector-schematic .component-label{font-weight:700}.vector-schematic .connector-label{font-size:9px;fill:var(--muted,#656e63)}
+    .vector-schematic .pin-label,.vector-schematic .polarity{font-size:10px}.vector-schematic .zone-label{font:10px sans-serif;letter-spacing:1px;fill:var(--muted,#656e63)}
+    .vector-schematic .part-symbol rect,.vector-schematic .part-symbol circle{fill:var(--panel-2,#f0f1ec)}
+    .vector-schematic .part-symbol .device-outline{fill:none;stroke:var(--line,#dddfd7)}
+    .vector-schematic .part-symbol .arrow-fill{fill:var(--wire,#7e897c);stroke:none}
+    .vector-schematic .part-hitbox{fill:transparent;stroke:none}.vector-schematic .junction{fill:var(--wire,#7e897c);stroke:none}
+    .vector-schematic .current-flow,.vector-schematic .signal-overlay,.vector-schematic .fault-flow,.vector-schematic .flyback-flow{fill:none;pointer-events:none}
+    .vector-schematic .current-flow{stroke:var(--accent,#3659d9);stroke-width:3;stroke-dasharray:3 12;opacity:0}
+    .vector-schematic .signal-overlay{stroke:var(--signal,#986016);stroke-width:3;opacity:0}
+    .vector-schematic .fault-flow{stroke:var(--danger,#b43f35);stroke-width:2.5;opacity:0}
+    .vector-schematic .flyback-flow{stroke:var(--signal,#986016);stroke-width:3;stroke-dasharray:2 8;opacity:0}
+  `;
+  const svg=`<svg xmlns="http://www.w3.org/2000/svg" id="lockCircuit" class="schematic vector-schematic" viewBox="210 260 1240 655" aria-labelledby="circuitTitle circuitDescription" data-state="idle"><title id="circuitTitle">Принципиальная схема ЗКЭ-4-10, 114.2.082.002 ПС</title><desc id="circuitDescription">Векторная перерисовка: S1, наборное поле XP/XS, четыре тиристора, пять транзисторов, все резисторы и конденсаторы, электромагнит, лампа, кнопка выхода, трансформатор и выпрямители. Выберите деталь для пояснения.</desc><style>${styles}</style><defs><pattern id="vectorCircuitGrid" width="16" height="16" patternUnits="userSpaceOnUse"><circle cx="1" cy="1" r=".55" fill="currentColor" opacity=".13"/></pattern></defs><rect x="210" y="260" width="1240" height="655" fill="url(#vectorCircuitGrid)"/>
+    ${text(268,285,'01 / КЛАВИАТУРА','zone-label')}${text(533,285,'02 / КОДОВАЯ ЦЕПОЧКА','zone-label')}${text(846,285,'03 / ЭЛЕКТРОМАГНИТ','zone-label')}${text(1160,285,'04 / ПИТАНИЕ','zone-label')}
+    <g class="circuit-nets">${wires.join('')}<g id="codeWires" aria-label="Перемычки набора кода"></g>${junctions.join('')}</g><g class="circuit-components">${elements.join('')}</g>
+    ${text(1380,614,'~220 В','component-label','middle')}${text(1380,635,'50 Гц','connector-label','middle')}${text(1410,547,'19','pin-label','middle')}${text(1410,720,'18','pin-label','middle')}
+    ${text(670,892,'VT5 + VT2 · СОСТАВНОЙ КЛЮЧ','zone-label')}${text(960,892,'VT3 / VT4 · ВЫДЕРЖКА','zone-label')}${text(370,878,'Общий провод (+)','pin-label')}
+    <g class="signal-overlay" aria-hidden="true"><path id="inputTrace" d="M330 385H400H435H450H500H542V445H638"/></g>
+    <g class="current-flow code-current" aria-hidden="true"><path d="M790 830V755 M790 685V535H847V415 M847 375V310H1160V532 M1160 588V610H1110V830H790"/></g>
+    <g class="current-flow exit-current" aria-hidden="true"><path d="M1090 830V535H847V415 M847 375V310H1160V532 M1160 588V610H1110V830H1090"/></g>
+    <g class="current-flow lamp-current" aria-hidden="true"><path d="M920 535V465 M920 435V390 M920 340V310"/></g>
+    <g class="fault-flow" aria-hidden="true"><path d="M450 545V855H935 M985 855H995V640H985 M729 580H1020V675"/></g>
+    <g class="flyback-flow" aria-hidden="true"><path d="M847 415V375H885V415H847"/></g>
+    </svg>`;
+  return {svg,pins,nets,partIds:Object.keys(pins),inputRoutes:[
+    'M435 385H500H542V445H638','M435 425H500H525V515H638 M620 493V515',
+    'M435 465H500H508V585H638 M620 565V585','M435 505H500H492V655H638 M620 637V655'
+  ]};
+}
+if(typeof module !== 'undefined' && module.exports) module.exports=buildLockSchematic;
+
+/* ===== js\circuit.js ===== */
+/* Native SVG reconstruction of the supplied 114.2.082.002 ПС photograph.
+ * Reference designators are retained; animation is a discrete teaching model,
+ * not an electrical solver. No component values are inferred from the photo. */
+(() => {
+  const host = document.querySelector('#diagramHost');
+  const schematic = buildLockSchematic();
+  host.innerHTML = schematic.svg;
+  let releaseTimer, previousState = 'idle', previousExit = false;
+
+  const groups = {
+    input: { title: 'S1 · Клавиатура и наборное поле', refs: 'S1, XP1–XP10 / XS1–XS10, R1, R3, R5, R7', text: 'Каждая цифра имеет отдельный провод. Наборное поле соединяет четыре выбранные клавиши с четырьмя ступенями кода. Резисторы ограничивают ток управляющих электродов тиристоров.', route: 'Клавиша S1 → соединитель XP/XS → R1 / R3 / R5 / R7 → управляющий электрод VD5 / VD6 / VD7 / VD8.', state: () => `В модели установлено соединение для кода ${currentCode.split('').join(' → ')}. Остальные клавиши относятся к цепи сброса.` },
+    memory: { title: 'VD5–VD8 · Четыре ступени памяти', refs: 'VD5, VD6, VD7, VD8; R2, R4, R6; C4, C5, C6', text: 'Тиристор открывается управляющим импульсом и сохраняет проводящее состояние, пока через него проходит достаточный ток. Последовательность ступеней задаёт порядок цифр. Межступенчатые RC-цепи участвуют в переходных процессах и переключении цепочки.', route: 'Первая цифра → VD5; вторая → VD6; третья → VD7; четвёртая → VD8. Полный набор меняет состояние управляющего каскада VT1.', state: () => `Проводящих ступеней в учебной модели: ${currentEntered.length} из 4. ${currentState === 'error' ? 'После ошибки вся цепочка сброшена.' : 'Синим выделены принятые ступени.'}` },
+    reset: { title: 'VD1–VD4, R19 · Цепи сброса и развязки', refs: 'VD1–VD4, R19, C3; управляющие каскады VT3 / VT4 / VT1', text: 'Некодовые клавиши объединены в отдельную шину. Через цепь обратной связи R19 и C3 воздействие поступает в управляющий каскад. Изменение режима VT1 прерывает ток удержания тиристоров, и память стирается. VD1–VD4 участвуют в развязке входных и сбросовых цепей.', route: 'Некодовая клавиша → общая шина ошибки → R19 / C3 → управляющий каскад → запирание VT1 → снятие тока удержания VD5–VD8.', state: () => currentState === 'error' ? 'Сработал сброс: ступени памяти выключены, электромагнит не включался.' : 'Ошибки нет. Цепь готова обнулить последовательность при неверном вводе.' },
+    timer: { title: 'C1, VT3, VT4 · Выдержка открытия', refs: 'C1, C3; VT1, VT3, VT4; R8, R12–R18', text: 'После полного кода через R12 начинает заряжаться C1. Напряжение на входе VT3 изменяется. После выдержки каскад VT3 / VT4 переключается и через R18 меняет смещение VT1. Ток цепочки тиристоров прекращается, составной ключ VT5 / VT2 выключается. C3 связан с цепью обратной связи через R19.', route: 'VD8 открыт → R12 → заряд C1 → VT3 / VT4 → R18 → VT1 закрыт → VD5–VD8 и силовой ключ выключены.', state: () => currentState === 'open' ? 'В стенде автоматическое закрытие отключено. Открытое состояние сохраняется до нажатия «Закрыть замок» или «Сбросить».' : 'Импульс открытия не запущен. Точные напряжения C1 по фотографии не определяются.' },
+    output: { title: 'VT5 + VT2, YA1 · Силовой ключ и защёлка', refs: 'VT5, VT2; R9–R11; YA1, VD9, HL1, R20, S2', text: 'VT5 и VT2 образуют составной транзисторный ключ: эмиттер VT5 соединён с базой VT2, а их коллекторы объединены. Он усиливает управляющий ток и замыкает цепь электромагнита. YA1 втягивает якорь; HL1 сигнализирует об открытии. VD9 ограничивает выброс самоиндукции. S2 напрямую замыкает цепь нагрузки при удержании.', route: 'Общий (+) → силовой ключ VT2 / VT5 → YA1 → отрицательный выход VD10. Ветвь R20 + HL1 включена параллельно YA1. S2 шунтирует ключ.', state: () => currentState === 'open' ? 'YA1 включён, HL1 горит, якорь втянут. Защёлка освобождена.' : 'YA1 обесточен, HL1 не горит. Пружина удерживает защёлку в закрытом положении.' },
+    power: { title: 'T1 · Два канала питания', refs: 'T1, FU1, VD10, VD11, C2', text: 'FU1 стоит во входной сетевой цепи. Трансформатор T1 понижает напряжение и отделяет низковольтные цепи от сети. Две вторичные обмотки и два моста питают нагрузку и электронику раздельно. C2 сглаживает пульсации питания электроники.', route: 'Сеть ~220 В / 50 Гц → FU1 → T1 → VD10 (электромагнит) и VD11 + C2 (схема управления).', state: () => 'Питание присутствует и при закрытом замке. Номиналы вторичных напряжений на исходном снимке не указаны.' }
+  };
+  const stageRouteAfterResistor = [
+    'M500 385H542V445H638',
+    'M500 425H525V515H638 M620 493V515',
+    'M500 465H508V585H580 M555 585V610 M580 585H625H638',
+    'M500 505H492V655H580 M565 655V680 M580 655H625H638'
+  ];
+  const codeRoute = (keyRow, stage) => {
+    const resistorRow = 385 + stage * 40;
+    const railX = 446 + stage * 8;
+    return `M330 ${keyRow}H400V${keyRow}H435H${railX}V${resistorRow}H450H500` + stageRouteAfterResistor[stage].slice(`M500 ${resistorRow}`.length);
+  };
+  const names = {
+    S1:'Десятиклавишный блок ввода', 'XP/XS':'Наборные соединители кода', S2:'Кнопка выхода изнутри', T1:'Трансформатор с двумя каналами', FU1:'Сетевой предохранитель', YA1:'Катушка электромагнита защёлки', HL1:'Сигнальная лампа открытия',
+    VT1:'Управляющий транзистор цепочки кода', VT2:'Выходной транзистор составного ключа', VT5:'Входной транзистор составного ключа', VT3:'Транзистор времязадающего каскада', VT4:'Транзистор выходного каскада',
+    VD9:'Диод подавления самоиндукции YA1', VD10:'Мост питания электромагнита', VD11:'Мост питания электроники', C1:'Времязадающий конденсатор', C2:'Сглаживающий конденсатор питания', C3:'Конденсатор связи каскадов', R8:'Резистор смещения VT1', R20:'Ограничение тока лампы HL1',
+    R1:'Резистор управляющей цепи VD5', R3:'Резистор управляющей цепи VD6', R5:'Резистор управляющей цепи VD7', R7:'Резистор управляющей цепи VD8',
+    R2:'Резистор межступенчатой цепи', R4:'Резистор межступенчатой цепи', R6:'Резистор межступенчатой цепи',
+    R9:'Резистор управления базой VT5', R10:'Резистор базы VT2', R11:'Резистор смещения базы VT5',
+    R12:'Резистор цепи C1', R13:'Резистор цепи управления VT3', R14:'Резистор цепи управления VT3',
+    R15:'Резистор каскада выдержки', R16:'Резистор каскада выдержки', R17:'Резистор общей цепи VT3 / VT4', R18:'Резистор цепи VT4', R19:'Резистор связи выходного и входного узлов',
+    C4:'Конденсатор связи первой и второй ступеней', C5:'Конденсатор связи второй и третьей ступеней', C6:'Конденсатор связи третьей и четвёртой ступеней',
+    VD1:'Диод связи цепи сброса', VD2:'Диод развязки сигналов', VD3:'Диод связи цепи сброса', VD4:'Диод развязки сигналов'
+  };
+  const groupFor = id => id === 'S1' || id === 'XP/XS' || /^R[1357]$/.test(id) ? 'input'
+    : /^VD[5-8]$/.test(id) || /^R[246]$/.test(id) || /^C[4-6]$/.test(id) ? 'memory'
+    : /^(VD[1-4]|R19)$/.test(id) ? 'reset'
+    : /^(T1|FU1|VD10|VD11|C2)$/.test(id) ? 'power'
+    : /^(VT[25]|R9|R10|R11|YA1|VD9|HL1|R20|S2)$/.test(id) ? 'output' : 'timer';
+  let selected = 'memory', selectedPart = '', currentState = 'idle', currentEntered = [], currentCode = '1234', currentExit = false;
+  function showComponentTooltip(group, id) {
+    if (!id) return;
+    const tooltip = document.querySelector('#componentTooltip');
+    const info = groups[group];
+    document.querySelector('#tooltipTitle').textContent = `${id} · ${names[id] || (/^VD[5-8]$/.test(id) ? `Тиристор ${Number(id.slice(2))-4}-й ступени` : /^R/.test(id) ? 'Резистор цепи' : /^C/.test(id) ? 'Конденсатор цепи' : info.title.split(' · ')[1])}`;
+    document.querySelector('#tooltipText').textContent = names[id] ? `${names[id]}. ${info.text}` : info.text;
+    tooltip.hidden = false;
+  }
+  function inspect(group, id = '', showHint = false) {
+    selected = group; selectedPart = id;
+    const info = groups[group];
+    document.querySelectorAll('[data-part]').forEach(p => p.classList.toggle('is-selected', id ? p.dataset.part === id : groupFor(p.dataset.part) === group));
+    if (showHint) showComponentTooltip(group, id);
+  }
+  host.addEventListener('click', e => { const p = e.target.closest('[data-part]'); if (p) inspect(groupFor(p.dataset.part), p.dataset.part, true); });
+  host.addEventListener('keydown', e => { if ((e.key === 'Enter' || e.key === ' ') && e.target.matches('[data-part]')) { e.preventDefault(); inspect(groupFor(e.target.dataset.part), e.target.dataset.part, true); } });
+  function renderNodeInfo(group) {
+    const info = groups[group];
+    document.querySelector('#detailTitle').textContent = info.title;
+    document.querySelector('#detailRefs').textContent = info.refs;
+    document.querySelector('#detailText').textContent = info.text;
+    document.querySelector('#detailRoute').textContent = info.route;
+    document.querySelector('#detailState').textContent = info.state();
+    document.querySelectorAll('[data-node]').forEach(b => b.setAttribute('aria-pressed', String(b.dataset.node === group)));
+    document.querySelector('.component-detail-disclosure').open = true;
+  }
+  document.querySelectorAll('[data-node]').forEach(b => b.onclick = () => renderNodeInfo(b.dataset.node));
+  const componentIds = ['S1','XP/XS',...Array.from({length:20},(_,i)=>'R'+(i+1)),...Array.from({length:6},(_,i)=>'C'+(i+1)),...Array.from({length:11},(_,i)=>'VD'+(i+1)),...Array.from({length:5},(_,i)=>'VT'+(i+1)),'YA1','HL1','S2','T1','FU1'];
+  document.querySelector('#componentList').innerHTML = componentIds.map(id => `<tr><th scope="row"><button type="button" data-inspect="${id}">${id}</button></th><td>${names[id] || (/^VD[5-8]$/.test(id) ? 'Тиристор памяти кода' : /^VD/.test(id) ? 'Диод развязки сигналов' : /^C/.test(id) ? 'Межступенчатый конденсатор' : groups[groupFor(id)].title.split(' · ')[1])}</td><td>${groupFor(id) === 'memory' ? 'Память' : groups[groupFor(id)].title.split(' · ')[1]}</td></tr>`).join('');
+  document.querySelector('#componentList').onclick = e => { const b = e.target.closest('[data-inspect]'); if (b) inspect(groupFor(b.dataset.inspect),b.dataset.inspect, true); };
+  document.querySelector('#componentTooltip .component-tooltip__close').onclick = () => { document.querySelector('#componentTooltip').hidden = true; };
+  document.addEventListener('keydown', e => { if (e.key === 'Escape') document.querySelector('#componentTooltip').hidden = true; });
+  let zoom = 100;
+  const setZoom = value => { zoom = Math.max(75,Math.min(200,value)); host.style.minWidth = '0'; host.style.width = `${Math.max(host.parentElement.clientWidth,1050)*zoom/100}px`; document.querySelector('#zoomValue').textContent = `${zoom}%`; document.querySelector('#zoomOut').disabled = zoom === 75; document.querySelector('#zoomIn').disabled = zoom === 200; };
+  const fitZoom = () => { const available = host.parentElement.clientWidth; const fitted = Math.floor((available / 1050) * 100); setZoom(Math.max(75, Math.min(100, fitted))); };
+  window.addEventListener('resize', fitZoom);
+  fitZoom();
+  document.querySelector('#downloadSvg').onclick = () => {
+    const svg = document.querySelector('#lockCircuit').cloneNode(true);
+    svg.querySelectorAll('#codeWires,.signal-overlay,.current-flow,.fault-flow,.flyback-flow').forEach(node => node.remove());
+    const blob = new Blob([new XMLSerializer().serializeToString(svg)], {type: 'image/svg+xml'});
+    const link = document.createElement('a'); link.href = URL.createObjectURL(blob); link.download = 'zke-4-10-circuit.svg'; link.click(); URL.revokeObjectURL(link.href);
+  };
+  renderNodeInfo('memory');
+  document.querySelector('#zoomIn').onclick = () => setZoom(zoom+25);
+  document.querySelector('#zoomOut').onclick = () => setZoom(zoom-25);
+  document.querySelector('#zoomFit').onclick = fitZoom;
+  const panel = document.querySelector('.circuit-panel');
+  const placeholder = document.createComment('circuit panel home'); panel.before(placeholder);
+  const modal = document.querySelector('#circuitModal');
+  document.querySelector('#expandCircuit').onclick = () => { modal.append(panel); modal.showModal(); setZoom(zoom); document.querySelector('#collapseCircuit').focus(); };
+  document.querySelector('#collapseCircuit').onclick = () => modal.close();
+  modal.addEventListener('close', () => { placeholder.after(panel); setZoom(zoom); document.querySelector('#expandCircuit').focus(); });
+  window.lockDiagram = {
+    render(state, entered, code, fromExit = false) {
+      currentState = state; currentEntered = [...entered]; currentCode = code; currentExit = fromExit;
+      const drawing = document.querySelector('#lockCircuit');
+      drawing.dataset.state = state;
+      drawing.dataset.exit = String(fromExit);
+      drawing.classList.toggle('is-held', state === 'open' && !fromExit);
+      drawing.querySelector('#exitContact').setAttribute('d',fromExit ? 'M980 535H1025' : 'M980 535L1015 519');
+      clearTimeout(releaseTimer);
+      drawing.classList.remove('is-releasing','is-flyback');
+      drawing.querySelectorAll('[data-device]').forEach(p => {
+        p.classList.toggle('is-biased', ['VT1','VT4'].includes(p.dataset.device) && state !== 'error');
+        p.classList.toggle('is-on', ['VT5','VT2'].includes(p.dataset.device) && state === 'open' && !fromExit);
+      });
+      if (previousState === 'open' && state === 'idle') {
+        drawing.classList.add('is-flyback');
+        if (!previousExit) drawing.classList.add('is-releasing');
+        releaseTimer = setTimeout(() => drawing.classList.remove('is-releasing','is-flyback'),600);
+      }
+      previousState = state; previousExit = fromExit;
+      drawing.querySelectorAll('[data-key-contact]').forEach(p => p.classList.toggle('key-active', false));
+      const keyRows = Object.fromEntries([...drawing.querySelectorAll('[data-key-contact]')].map((p, i) => [p.dataset.keyContact, 385 + i * 40]));
+      drawing.querySelector('#codeWires').innerHTML = [...code].map((digit, stage) => {
+        const resistorRow = 385 + stage * 40;
+        const railX = 446 + stage * 8;
+        return `<path class="net code-wire" data-code-stage="${stage + 1}" d="M435 ${keyRows[digit]}H${railX}V${resistorRow}H450"/>`;
+      }).join('');
+      drawing.querySelectorAll('[data-stage]').forEach(p => p.classList.toggle('is-conducting',Number(p.dataset.stage) <= entered.length));
+      drawing.querySelectorAll('.memory-net').forEach((p,i) => p.classList.toggle('is-conducting',i < entered.length));
+      document.querySelectorAll('[data-stage-card]').forEach(p => { const active = Number(p.dataset.stageCard) <= entered.length; p.classList.toggle('is-conducting',active); p.querySelector('small').textContent = active ? 'проводит' : 'закрыт'; });
+
+      inspect(selected, selectedPart);
+    },
+    pulse(key) {
+      const index = currentCode.indexOf(key);
+      document.querySelector('#keypadSignal').textContent = `S1 · нажата ${key}${index < 0 ? ' → сброс' : ` → ступень ${index+1}`}`;
+      document.querySelector('#lockCircuit').classList.add('is-receiving');
+      const keyRow = 385 + ((Number(key) + 9) % 10) * 40;
+      document.querySelector('#inputTrace').setAttribute('d', index < 0 ? `M330 ${keyRow}H400H435V855H935` : codeRoute(keyRow, index));
+      document.querySelectorAll('[data-key-contact]').forEach(p=>p.classList.toggle('key-active',p.dataset.keyContact === key));
+    },
+    clearPulse() { document.querySelector('#lockCircuit').classList.remove('is-receiving'); document.querySelectorAll('.key-active').forEach(p=>p.classList.remove('key-active')); document.querySelector('#keypadSignal').textContent = 'S1 · ожидание клавиши'; }
+  };
+})();
+
+/* ===== js\app.js ===== */
+/* app.js — interaction layer: code validation, visual state, sound, dialogs and local persistence. */
+const $ = (selector) => document.querySelector(selector);
+let activeCode = localStorage.getItem('zke-code') || '1234';
+if (!/^\d{4}$/.test(activeCode) || new Set(activeCode).size !== 4) activeCode = '1234';
+let exitOverride = false;
+let entered = [];
+let faultId;
+let inputPulseId;
+let circuitState = 'idle';
+let soundEnabled = localStorage.getItem('zke-sound') === 'true';
+const quiz = [
+  ['Что происходит при неверной цифре?', ['Цепь продолжает работу', 'Цепь сбрасывается', 'Дверь открывается'], 1],
+  ['Какой элемент физически убирает защёлку?', ['Электромагнит', 'Тиристор', 'Клавиша'], 0],
+  ['Что подаёт питание на электромагнит?', ['Транзисторный выходной каскад', 'Микроконтроллер DD1', 'Резистор R1'], 0],
+  ['Какую функцию выполняют VD5–VD8?', ['Хранят последовательность цифр', 'Выпрямляют сетевое напряжение', 'Подсвечивают лампу'], 0],
+  ['Какой элемент формирует выдержку после открытия?', ['C1', 'FU1', 'S1'], 0],
+  ['Для чего нужен VD9?', ['Для подавления выброса самоиндукции YA1', 'Для выбора кода', 'Для усиления звука'], 0],
+  ['Что происходит при нажатии S2?', ['Нагрузка включается напрямую', 'Код стирается навсегда', 'Схема переходит в режим настройки'], 0],
+  ['Какую роль играет HL1?', ['Сигнализирует об открытии замка', 'Запоминает первую цифру', 'Понижает напряжение'], 0],
+  ['Что соединяет наборное поле XP/XS?', ['Выбранные клавиши с каскадами кода', 'Сеть с трансформатором', 'Лампу с корпусом'], 0],
+  ['Как закрывается замок в учебном стенде?', ['Кнопкой «Закрыть замок» или сбросом', 'Автоматически через 1 секунду', 'Только после выключения браузера'], 0]
+];
+
+function tone(type = 'click') { if (!soundEnabled || !window.AudioContext) return; const ctx = new AudioContext(); const osc = ctx.createOscillator(); const gain = ctx.createGain(); osc.connect(gain); gain.connect(ctx.destination); const now = ctx.currentTime; osc.type = type === 'error' ? 'sawtooth' : 'sine'; osc.frequency.setValueAtTime(type === 'success' ? 780 : type === 'error' ? 140 : 350, now); if (type === 'success') osc.frequency.exponentialRampToValueAtTime(1180, now + .12); gain.gain.setValueAtTime(.06, now); gain.gain.exponentialRampToValueAtTime(.001, now + (type === 'error' ? .3 : .12)); osc.start(now); osc.stop(now + .35); }
+function setLog(text) { $('#eventLog').textContent = text; }
+const attemptHistory = [];
+const attemptLogList = $('#attemptLogList');
+function recordAttempt(code, result) {
+  attemptHistory.unshift({ code, result, time: new Date() });
+  attemptHistory.length = Math.min(attemptHistory.length, 5);
+  renderAttemptHistory();
+}
+function renderAttemptHistory() {
+  $('#clearAttemptLog').disabled = attemptHistory.length === 0;
+  if (!attemptHistory.length) {
+    attemptLogList.innerHTML = '<li class="attempt-log__empty">Попыток пока нет.</li>';
+    return;
+  }
+  attemptLogList.innerHTML = attemptHistory.map(({code, result, time}) => `<li class="attempt-log__item attempt-log__item--${result === 'Успех' ? 'success' : 'error'}"><span class="attempt-log__code">${code.split('').join(' ')}</span><span class="attempt-log__result">${result}</span><time datetime="${time.toISOString()}">${time.toLocaleTimeString('ru-RU', {hour:'2-digit', minute:'2-digit', second:'2-digit'})}</time></li>`).join('');
+}
+$('#clearAttemptLog').onclick = () => { attemptHistory.length = 0; renderAttemptHistory(); };
+function updateCircuit(state = circuitState) {
+  circuitState = state;
+  const powered = state === 'open';
+  window.lockDiagram.render(state, entered, activeCode, exitOverride);
+  $('#closeLockButton').disabled = !powered;
+  $('#finishPulse').disabled = !powered;
+  $('#lockReadout').textContent = powered ? (exitOverride ? 'S2 УДЕРЖИВАЕТСЯ' : 'ДО ЗАКРЫТИЯ КНОПКОЙ') : 'ОЖИДАНИЕ КОДА';
+  ['#backspaceButton', '#codeInput', '#applyCode', '#nextStep'].forEach(selector => {
+    $(selector).disabled = powered || state === 'error';
+  });
+  $('#resetButton').disabled = false;
+  $('#keypadReset').disabled = false;
+  $('#exitButton').disabled = state === 'error' || (powered && !exitOverride);
+  $('#exitButton').textContent = exitOverride ? 'S2 · Отпустить кнопку' : 'S2 · Выход изнутри';
+  document.querySelectorAll('[data-key]').forEach(b => b.disabled = powered || state === 'error');
+  document.querySelectorAll('[data-stage-digit]').forEach(p => p.textContent = activeCode[Number(p.dataset.stageDigit)]);
+  $('#driverReading').textContent = `${entered.length} / 4`;
+  $('#relayReading').textContent = powered ? (exitOverride ? 'S2 нажата' : 'Вручную') : 'Нет';
+  $('#coilReading').textContent = powered ? 'Включён' : 'Выключен';
+  $('#circuitStatus').textContent = powered
+    ? (exitOverride ? 'S2 удерживается · YA1 включён напрямую' : 'Код принят · замок открыт до закрытия кнопкой')
+    : state === 'error' ? 'Неверная цифра · тиристорная память сброшена'
+    : entered.length ? `Проводят ${entered.length} из 4 тиристоров · ожидание следующей цифры`
+    : 'Ожидание кода · VD5–VD8 закрыты';
+  $('#logicText').textContent = powered
+    ? (exitOverride ? 'S2 замкнула цепь нагрузки в обход составного ключа VT5 / VT2. YA1 включён, HL1 горит. Нажмите S2 повторно, чтобы отпустить кнопку и закрыть замок.' : 'VD5–VD8 проводят. Составной ключ VT5 / VT2 включил YA1; HL1 горит. Замок и схема остаются в открытом состоянии до нажатия «Закрыть замок» или «Сбросить».')
+    : state === 'error' ? 'Сработала цепь сброса R19 / C3 и управляющий каскад: ток удержания цепочки прерван, VD5–VD8 закрыты. Начните набор сначала.'
+    : entered.length ? `Цифра ${entered[entered.length-1]} принята: VD${entered.length+4} проводит. Ток удержания сохраняет состояние. ${entered.length < 4 ? `Следующая ступень — VD${entered.length+5}.` : ''} Электромагнит пока выключен.`
+    : 'VD5–VD8 закрыты. Выпрямители питают схему, но импульс открытия отсутствует. YA1 обесточен; пружина удерживает защёлку.';
+}
+function clearInputPulse() {
+  clearTimeout(inputPulseId);
+  inputPulseId = undefined;
+  window.lockDiagram.clearPulse();
+}
+function pulseInput(key) {
+  clearInputPulse();
+  window.lockDiagram.pulse(key);
+  inputPulseId = setTimeout(clearInputPulse, 650);
+}
+function updateScreen() {
+  $('#codeDisplay').textContent = [...entered, ...Array(4 - entered.length).fill('—')].join(' ');
+  $('#progressNumber').textContent = `${entered.length}/4`;
+  updateCircuit();
+}
+function reset(message = 'Система сброшена. Ожидание нового ввода.') {
+  exitOverride = false;
+  clearTimeout(faultId);
+  faultId = undefined;
+  clearInputPulse();
+  entered = [];
+  circuitState = 'idle';
+  $('#lockStage').classList.remove('is-open');
+  $('#lockPill').classList.remove('is-open');
+  $('#lockPill').textContent = 'ЗАКРЫТ';
+  $('#lamp').className = 'lamp';
+  $('#powerDot').classList.remove('is-on');
+  updateScreen();
+  setLog(message);
+}
+function openLock(fromExit = false) {
+  exitOverride = fromExit;
+  updateCircuit('open');
+  $('#lockStage').classList.add('is-open');
+  $('#lockPill').classList.add('is-open');
+  $('#lockPill').textContent = 'ОТКРЫТ';
+  $('#lamp').className = 'lamp is-success';
+  $('#powerDot').classList.add('is-on');
+  setLog(fromExit ? 'S2 удерживается. Прямое питание YA1; нажмите S2 повторно, чтобы отпустить.' : 'Код принят. VT5 / VT2 включили YA1. Замок открыт до нажатия «Закрыть замок» или «Сбросить».');
+  if (!fromExit) recordAttempt(entered.join(''), 'Успех');
+  tone('success');
+}
+function incorrect(key) {
+  recordAttempt(entered.join('') + key, 'Ошибка');
+  entered = [];
+  circuitState = 'error';
+  updateScreen();
+  $('#lamp').className = 'lamp is-error';
+  setLog('Неверная цифра — ввод сброшен. Тиристорная память сброшена, YA1 выключен.');
+  tone('error');
+  faultId = setTimeout(() => reset(), 700);
+}
+function input(key) {
+  if (circuitState === 'open' || circuitState === 'error' || entered.length >= 4) return;
+  tone();
+  pulseInput(key);
+  const candidate = entered.join('') + key;
+  if (!activeCode.startsWith(candidate)) return incorrect(key);
+  entered.push(key);
+  updateScreen();
+  if (candidate === activeCode) return openLock();
+  setLog(`Введено ${entered.length} из 4 цифр. Очередной тиристор проводит; набор ещё не завершён.`);
+}
+function applyCode() {
+  const value = $('#codeInput').value.trim();
+  if (!/^\d{4}$/.test(value) || new Set(value).size !== 4) {
+    $('#codeHint').textContent = 'Нужны четыре разные цифры: у каждой клавиши оригинала один провод к наборному полю.';
+    $('#codeHint').style.color = 'var(--danger)'; return;
+  }
+  activeCode = value; localStorage.setItem('zke-code', value);
+  $('#codeInput').value = value;
+  $('#codeHint').textContent = 'Соединения XP/XS обновлены в модели. Код сохранён в этом браузере.';
+  $('#codeHint').style.color = ''; $('#saveNote').textContent = 'сохранено сейчас';
+  reset('Наборное поле перенастроено. Можно вводить новый код.');
+}
+function renderQuiz(index = 0, score = 0) { const host = $('#quizContent'); if (index === quiz.length) { host.innerHTML = `<h2>Готово</h2><p class="quiz-result">${score} из ${quiz.length} — ${score === quiz.length ? 'идеально!' : 'ещё один заход?'}</p><button class="button button--primary" id="restartQuiz">Пройти ещё раз</button>`; $('#restartQuiz').onclick = () => renderQuiz(); return; } const [question, options, answer] = quiz[index]; host.innerHTML = `<p class="quiz-progress">ВОПРОС ${index + 1} / ${quiz.length}</p><h2>${question}</h2><div class="quiz-options">${options.map((option, i) => `<button data-answer="${i}">${option}</button>`).join('')}</div>`; host.querySelectorAll('button').forEach(button => button.onclick = () => renderQuiz(index + 1, score + (+button.dataset.answer === answer))); }
+
+$('#codeInput').value = activeCode; updateScreen();
+$('#keypad').addEventListener('click', e => { const key = e.target.dataset.key; if (key) input(key); });
+$('#closeLockButton').onclick = () => reset('Замок закрыт кнопкой. YA1 выключен; пружина вернула защёлку.');
+$('#finishPulse').onclick = $('#closeLockButton').onclick;
+$('#nextStep').onclick = () => input(activeCode[entered.length]);
+$('#exitButton').onclick = () => { if (exitOverride) return reset('S2 отпущена. YA1 выключен; пружина вернула защёлку.'); clearInputPulse(); entered = []; updateScreen(); openLock(true); };
+$('#keypadReset').onclick = () => reset(); $('#resetButton').onclick = () => reset(); $('#backspaceButton').onclick = () => reset('Тиристорная память сброшена целиком. Начните с первой цифры.');
+$('#applyCode').onclick = applyCode; $('#codeInput').addEventListener('keydown', e => { if (e.key === 'Enter') applyCode(); });
+document.addEventListener('keydown', e => {
+  if (document.querySelector('dialog[open]:not(#circuitModal)') || e.target.closest('input, select, textarea, [contenteditable="true"]') || e.ctrlKey || e.altKey || e.metaKey) return;
+  if (/^[0-9]$/.test(e.key)) { e.preventDefault(); input(e.key); }
+});
+const extrasModal = $('#extrasModal');
+extrasModal.addEventListener('close', () => $('#extrasButton').setAttribute('aria-expanded', 'false'));
+extrasModal.addEventListener('click', event => {
+  const bounds = extrasModal.getBoundingClientRect();
+  if (event.target === extrasModal && (event.clientX < bounds.left || event.clientX > bounds.right || event.clientY < bounds.top || event.clientY > bounds.bottom)) extrasModal.close();
+});
+
+function updateSoundButton() {
+  $('#soundButton').classList.toggle('is-active', soundEnabled);
+  $('#soundButton').setAttribute('aria-pressed', String(soundEnabled));
+  $('#soundButton').setAttribute('aria-label', soundEnabled ? 'Выключить звук' : 'Включить звук');
+}
+updateSoundButton();
+$('#soundButton').onclick = () => {
+  soundEnabled = !soundEnabled;
+  localStorage.setItem('zke-sound', soundEnabled);
+  updateSoundButton();
+  tone();
+};
+document.querySelectorAll('[data-open-modal]').forEach(button => button.onclick = () => { const modal = $(`#${button.dataset.openModal}`); if (modal.id === 'quizModal') renderQuiz(); modal.showModal(); if (modal.id === 'extrasModal') $('#extrasButton').setAttribute('aria-expanded', 'true'); }); document.querySelectorAll('.modal-close').forEach(button => button.onclick = () => button.closest('dialog').close());
+
+// Compact minesweeper: mines are placed after the first move, so it is always safe.
+const mineBoard = $('#minesweeperBoard');
+const mineMessage = $('#minesweeperMessage');
+const mineCounter = $('#minesweeperMines');
+const mineFlagToggle = $('#minesweeperFlagToggle');
+const mineSize = 9;
+const mineTotal = 10;
+let mineCells = [];
+let mineGameOver = false;
+let mineFlagMode = false;
+const mineIndex = (row, column) => row * mineSize + column;
+function mineNeighbours(index) { const row = Math.floor(index / mineSize); const column = index % mineSize; const result = []; for (let r = row - 1; r <= row + 1; r++) for (let c = column - 1; c <= column + 1; c++) if (r >= 0 && r < mineSize && c >= 0 && c < mineSize && (r !== row || c !== column)) result.push(mineIndex(r, c)); return result; }
+function renderMineCell(index) { const cell = mineCells[index]; const button = mineBoard.children[index]; button.className = 'mine-cell'; button.textContent = ''; if (cell.open) { button.classList.add('is-open'); if (cell.mine) { button.classList.add('is-mine'); button.textContent = '✹'; } else if (cell.count) { button.classList.add(`number-${Math.min(cell.count, 4)}`); button.textContent = cell.count; } } else if (cell.flagged) { button.classList.add('is-flagged'); button.textContent = '⚑'; } }
+function updateMineCounter() { mineCounter.textContent = `МИНЫ: ${mineTotal - mineCells.filter(cell => cell.flagged).length}`; }
+function prepareMines(safeIndex) { const forbidden = new Set([safeIndex, ...mineNeighbours(safeIndex)]); const candidates = Array.from({ length: mineSize * mineSize }, (_, i) => i).filter(i => !forbidden.has(i)); for (let i = candidates.length - 1; i > 0; i--) { const swap = Math.floor(Math.random() * (i + 1)); [candidates[i], candidates[swap]] = [candidates[swap], candidates[i]]; } candidates.slice(0, mineTotal).forEach(index => mineCells[index].mine = true); mineCells.forEach((cell, index) => cell.count = mineNeighbours(index).filter(neighbour => mineCells[neighbour].mine).length); }
+function openMineCell(index) { const cell = mineCells[index]; if (mineGameOver || cell.open || cell.flagged) return; if (!mineCells.some(item => item.mine)) prepareMines(index); cell.open = true; if (cell.mine) { mineGameOver = true; mineMessage.textContent = 'Мина. Попробуйте ещё раз.'; mineCells.forEach((item, i) => { if (item.mine) { item.open = true; renderMineCell(i); } }); tone('error'); return; } renderMineCell(index); if (!cell.count) mineNeighbours(index).forEach(openMineCell); if (!mineCells.some(item => !item.mine && !item.open)) { mineGameOver = true; mineMessage.textContent = 'Поле очищено — отлично!'; tone('success'); } }
+function flagMineCell(index) { const cell = mineCells[index]; if (mineGameOver || cell.open) return; cell.flagged = !cell.flagged; renderMineCell(index); updateMineCounter(); }
+function newMinesweeper() { mineGameOver = false; mineFlagMode = false; mineCells = Array.from({ length: mineSize * mineSize }, () => ({ mine: false, open: false, flagged: false, count: 0 })); mineBoard.innerHTML = ''; mineCells.forEach((_, index) => { const button = document.createElement('button'); button.type = 'button'; button.className = 'mine-cell'; button.setAttribute('role', 'gridcell'); button.setAttribute('aria-label', `Клетка ${index + 1}`); button.onclick = () => mineFlagMode ? flagMineCell(index) : openMineCell(index); button.oncontextmenu = event => { event.preventDefault(); flagMineCell(index); }; mineBoard.append(button); }); mineMessage.textContent = 'Поле готово'; mineFlagToggle.textContent = 'Режим: открыть клетки'; mineFlagToggle.setAttribute('aria-pressed', 'false'); updateMineCounter(); }
+$('#minesweeperReset').onclick = newMinesweeper;
+mineFlagToggle.onclick = () => { mineFlagMode = !mineFlagMode; mineFlagToggle.textContent = mineFlagMode ? 'Режим: ставить флажки' : 'Режим: открыть клетки'; mineFlagToggle.setAttribute('aria-pressed', String(mineFlagMode)); };
+newMinesweeper();
+
+// Radio uses the selected public stream directly, so the volume slider is a native draggable control.
+const radioAudio = $('#radioAudio');
+const radioStation = $('#radioStation');
+const radioPlay = $('#radioPlay');
+const radioVolume = $('#radioVolume');
+const radioVolumeValue = $('#radioVolumeValue');
+const radioStations = $('#radioStations');
+const radioOptions = [...radioStation.options];
+function setRadioVolume() { radioAudio.volume = Number(radioVolume.value) / 100; radioVolumeValue.textContent = `${radioVolume.value}%`; }
+function updateRadioButton() { const isPlaying = !radioAudio.paused; radioAudio.dataset.playing = String(isPlaying); radioPlay.textContent = isPlaying ? '❚❚' : '▶'; radioPlay.setAttribute('aria-label', isPlaying ? 'Поставить радио на паузу' : 'Включить радио'); }
+function updateRadioCards() { [...radioStations.children].forEach(card => card.classList.toggle('is-selected', card.dataset.value === radioStation.value)); }
+function selectRadioStation(value) { radioStation.value = value; radioStation.dispatchEvent(new Event('change')); }
+radioOptions.forEach(option => { const card = document.createElement('button'); card.type = 'button'; card.className = 'radio-station-card'; card.dataset.value = option.value; card.title = option.textContent; card.setAttribute('aria-label', `Включить ${option.textContent}`); card.innerHTML = `<img src="https://radiobells.com/stations/${option.dataset.logo}_70.webp" alt="" loading="lazy"><span>${option.textContent}</span>`; card.onclick = () => selectRadioStation(option.value); radioStations.append(card); });
+radioAudio.src = radioStation.value;
+setRadioVolume();
+radioVolume.addEventListener('input', setRadioVolume);
+radioPlay.onclick = async () => { if (radioAudio.paused) { try { await radioAudio.play(); } catch { radioPlay.title = 'Не удалось включить этот поток. Выберите другую станцию.'; } } else radioAudio.pause(); updateRadioButton(); };
+radioStation.onchange = async () => { const shouldResume = !radioAudio.paused; radioAudio.src = radioStation.value; if (shouldResume) { try { await radioAudio.play(); } catch { /* The play button keeps a retry available. */ } } updateRadioButton(); updateRadioCards(); };
+radioAudio.onplay = updateRadioButton;
+radioAudio.onpause = updateRadioButton;
+updateRadioCards();
+
+// Hidden story sequence: code 1984 opens a short archive and reaction test.
+const archiveModal = $('#archiveModal');
+const archiveReader = $('#archiveReader');
+let archiveInterval;
+let archiveTimeout;
+function clearArchiveTest() { clearInterval(archiveInterval); clearTimeout(archiveTimeout); archiveInterval = undefined; archiveTimeout = undefined; }
+function openArchive() { reset('Обнаружен архив №1984. Доступ предоставлен на один сеанс.'); archiveModal.showModal(); }
+function showArchiveFile(index) { document.querySelectorAll('[data-archive-file]').forEach(file => file.classList.toggle('is-open', +file.dataset.archiveFile === index)); clearArchiveTest(); if (index === 0) archiveReader.innerHTML = '<p><b>ШИФР СМЕНЫ</b><br>«Если лампа горит слишком ровно — проверьте не лампу, а того, кто на неё смотрит».</p>'; if (index === 1) archiveReader.innerHTML = '<p><b>ЗАПИСКА ОПЕРАТОРА</b><br>03:14. После четвёртого сигнала дверь открылась сама. В журнале этого нет. Не обсуждать по внутренней связи.</p>'; if (index === 2) archiveReader.innerHTML = '<p><b>ПРОВЕРКА ЛИНИИ</b><br>Найдите светящуюся лампу 6 раз за 10 секунд.</p><button class="button button--primary" id="startSignalTest">Запустить проверку</button>'; const startButton = $('#startSignalTest'); if (startButton) startButton.onclick = startSignalTest; }
+function startSignalTest() { clearArchiveTest(); let score = 0; let current = -1; let finished = false; archiveReader.innerHTML = '<div class="signal-test"><div class="signal-test__top"><span id="signalTestScore">СИГНАЛЫ: 0 / 6</span><span>10.0 c</span></div><div class="signal-lamps">' + Array.from({ length: 5 }, (_, index) => `<button class="signal-lamp" data-signal-lamp="${index}" aria-label="Сигнальная лампа ${index + 1}"></button>`).join('') + '</div></div>'; const scoreLabel = $('#signalTestScore'); const lamps = [...archiveReader.querySelectorAll('[data-signal-lamp]')]; const nextLamp = () => { lamps[current]?.classList.remove('is-target'); current = Math.floor(Math.random() * lamps.length); lamps[current].classList.add('is-target'); }; lamps.forEach((lamp, index) => lamp.onclick = () => { if (finished || index !== current) return; score++; scoreLabel.textContent = `СИГНАЛЫ: ${score} / 6`; if (score === 6) { finished = true; clearArchiveTest(); archiveReader.innerHTML = '<p><b>ЛИНИЯ ЧИСТА.</b><br>Наблюдатель отключён. Запись автоматически удалится после закрытия архива.</p>'; tone('success'); } else nextLamp(); }); nextLamp(); archiveInterval = setInterval(nextLamp, 850); archiveTimeout = setTimeout(() => { if (finished) return; finished = true; clearArchiveTest(); archiveReader.innerHTML = `<p><b>ПРОВЕРКА ЗАВЕРШЕНА.</b><br>Зафиксировано сигналов: ${score} из 6. Архив остаётся доступен.</p>`; tone('error'); }, 10000); }
+document.querySelectorAll('[data-archive-file]').forEach(file => file.onclick = () => showArchiveFile(+file.dataset.archiveFile));
+archiveModal.addEventListener('close', clearArchiveTest);
+
+/* ===== js\arcade.js ===== */
+(() => {
+  'use strict';
+  const modal = document.querySelector('#arcadeModal');
+  const canvas = document.querySelector('#arcadeCanvas');
+  const ctx = canvas.getContext('2d');
+  const ui = name => document.querySelector('#arcade' + name);
+  const W = 360, H = 480;
+  const held = new Set(), pointers = new Map();
+  let mode, state, paused = false, over = false, raf = 0, last = 0, accumulator = 0, best = 0;
+  const random = (a,b) => a + Math.random() * (b-a);
+  const clamp = (v,a,b) => Math.max(a,Math.min(b,v));
+  function record() {
+    if (state.score > best) {
+      best = state.score;
+      try { localStorage.setItem('zke-arcade-' + mode, String(best)); } catch {}
+    }
+    ui('Score').textContent = 'Счёт: ' + state.score;
+    if (mode === 'jump') ui('Score').textContent += ' · Уровень ' + (1 + Math.floor(difficulty(state.score) * 5));
+    ui('Best').textContent = 'Рекорд: ' + best;
+  }
+  function end(message) {
+    over = true; record(); held.clear(); pointers.clear();
+    ui('Status').textContent = message + ' Счёт: ' + state.score;
+    ui('Pause').disabled = true;
+  }
+  function pause(value = !paused) {
+    if (over || !modal.open) return;
+    paused = value; held.clear(); pointers.clear();
+    ui('Pause').textContent = paused ? 'Продолжить' : 'Пауза';
+    ui('Status').textContent = paused ? 'Пауза' : 'Игра идёт';
+    last = 0; accumulator = 0; draw();
+  }
+  function restart() {
+    cancelAnimationFrame(raf); paused = false; over = false; held.clear(); pointers.clear();
+    last = 0; accumulator = 0;
+    try { best = Number(localStorage.getItem('zke-arcade-' + mode)) || 0; } catch { best = 0; }
+    ui('Pause').textContent = 'Пауза'; ui('Pause').disabled = false;
+    ui('Status').textContent = 'Игра идёт';
+    state = mode === 'snake' ? newSnake() : newJump();
+    record(); draw(); raf = requestAnimationFrame(frame); canvas.focus();
+  }
+  function open(type) {
+    mode = type;
+    ui('Title').textContent = type === 'jump' ? 'Прыжок оператора' : 'Змейка';
+    ui('Help').textContent = type === 'jump'
+      ? '← → или A/D — движение · W / Пробел — огонь · P — пауза. Края соединены. Зелёные площадки прочные, голубые движутся, рыжие ломаются, белые исчезают. Пружина ↑, ранец J, щит S.'
+      : 'Стрелки или WASD — поворот · P — пауза. Собирайте еду, избегайте стен и своего хвоста.';
+    document.querySelectorAll('[data-control]').forEach(button => {
+      button.hidden = type === 'jump' ? ['up','down'].includes(button.dataset.control) : button.dataset.control === 'shoot';
+    });
+    if (!modal.open) modal.showModal();
+    restart();
+  }
+  // All input and animation belong to the open game only.
+  document.querySelectorAll('[data-arcade]').forEach(button => button.addEventListener('click', () => open(button.dataset.arcade)));
+  ui('Restart').onclick = restart; ui('Pause').onclick = () => pause();
+  ui('Close').onclick = () => modal.close();
+  modal.addEventListener('close', () => { cancelAnimationFrame(raf); held.clear(); pointers.clear(); last = 0; });
+  window.addEventListener('blur', () => pause(true));
+  document.addEventListener('visibilitychange', () => { if (document.hidden) pause(true); });
+  const keys = { ArrowLeft:'left', ArrowRight:'right', ArrowUp:'up', ArrowDown:'down', KeyA:'left', KeyD:'right', KeyW:'up', KeyS:'down', Space:'shoot' };
+  document.addEventListener('keydown', event => {
+    if (!modal.open) return;
+    if (event.code === 'Escape') return;
+    event.stopImmediatePropagation();
+    if (event.code === 'KeyP') { event.preventDefault(); if (!event.repeat) pause(); return; }
+    const control = mode === 'jump' && event.code === 'KeyW' ? 'shoot' : keys[event.code];
+    if (!control) return;
+    event.preventDefault();
+    if (paused || over) return;
+    held.add(event.code);
+    if (mode === 'snake') turn(control);
+  }, true);
+  document.addEventListener('keyup', event => { if (modal.open) held.delete(event.code); });
+  document.querySelectorAll('[data-control]').forEach(button => {
+    button.addEventListener('pointerdown', event => {
+      if (paused || over) return;
+      event.preventDefault(); button.setPointerCapture(event.pointerId);
+      pointers.set(event.pointerId, button.dataset.control);
+      if (mode === 'snake') turn(button.dataset.control);
+    });
+    const release = event => pointers.delete(event.pointerId);
+    button.addEventListener('pointerup', release);
+    button.addEventListener('pointercancel', release);
+    button.addEventListener('lostpointercapture', release);
+  });
+  const pressed = key => [...held].some(code => (mode === 'jump' && code === 'KeyW' ? 'shoot' : keys[code]) === key) || [...pointers.values()].includes(key);
+  function frame(time) {
+    if (!modal.open) return;
+    const dt = last ? Math.min((time-last)/1000, .05) : 0; last = time;
+    if (!paused && !over) {
+      accumulator += dt;
+      while (accumulator >= 1/120 && !over) {
+        mode === 'jump' ? stepJump(1/120) : stepSnake(1/120);
+        accumulator -= 1/120;
+      }
+      record();
+    }
+    draw();
+    raf = requestAnimationFrame(frame);
+  }
+  function newSnake() {
+    const s = { body:[{x:9,y:12},{x:8,y:12},{x:7,y:12}], dir:{x:1,y:0}, queue:[], elapsed:0, score:0 };
+    food(s); return s;
+  }
+  function food(s) {
+    const free = [];
+    for(let y=0;y<24;y++) for(let x=0;x<18;x++)
+      if(!s.body.some(p => p.x===x && p.y===y)) free.push({x,y});
+    s.food = free[Math.floor(Math.random()*free.length)] || null;
+  }
+  function turn(control) {
+    const dirs = {left:{x:-1,y:0},right:{x:1,y:0},up:{x:0,y:-1},down:{x:0,y:1}};
+    const next = dirs[control], previous = state.queue.at(-1) || state.dir;
+    if (!next || state.queue.length >= 2 || (next.x===previous.x && next.y===previous.y) || (next.x===-previous.x && next.y===-previous.y)) return;
+    state.queue.push(next);
+  }
+  function stepSnake(dt) {
+    const s=state; s.elapsed+=dt;
+    const interval=Math.max(.065,.17-s.score*.003);
+    if(s.elapsed<interval) return;
+    s.elapsed-=interval;
+    if(s.queue.length) s.dir=s.queue.shift();
+    const head={x:s.body[0].x+s.dir.x,y:s.body[0].y+s.dir.y};
+    const eating=s.food && head.x===s.food.x && head.y===s.food.y;
+    const occupied=eating?s.body:s.body.slice(0,-1);
+    if(head.x<0||head.x>=18||head.y<0||head.y>=24||occupied.some(p=>p.x===head.x&&p.y===head.y)) return end('Игра окончена.');
+    s.body.unshift(head);
+    if(eating) { s.score++; food(s); if(!s.food) end('Всё поле заполнено. Победа!'); }
+    else s.body.pop();
+  }
+  // World Y grows downward; upward progress moves the camera, never the physics.
+  function newJump() {
+    const s={player:{x:170,y:390,vy:-520}, platforms:[{x:135,y:430,w:90,type:'solid',item:null}], enemies:[], shots:[], debris:[], time:0, camera:0, top:430, anchor:135, score:0, boost:0, shield:0, cooldown:0, generated:0};
+    generate(s); return s;
+  }
+  // Difficulty rises over 600 points, then stays within normal jump reach.
+  function difficulty(score) { return clamp(score / 600, 0, 1); }
+  function generate(s) {
+    const d = difficulty(s.score);
+    while(s.top>s.camera-110) {
+      s.top-=random(50+24*d,68+18*d); s.generated++;
+      s.anchor=clamp(s.anchor+random(-60-10*d,60+10*d),12,W-88);
+      const roll=Math.random();
+      // Permanent landing in every row keeps the main route reachable.
+      const platform={x:s.anchor,y:s.top,w:86-30*d,type:'solid',item:null};
+      if(s.generated>5 && roll<.12+.48*d) { platform.type='moving'; platform.base=platform.x; platform.phase=random(0,6); platform.speed=1+1.3*d; }
+      if(s.generated%8===0) platform.item=['spring','jet','shield'][Math.floor(Math.random()*3)];
+      s.platforms.push(platform);
+      if(s.generated>5 && Math.random()<.25+.4*d) s.platforms.push({x:clamp(s.anchor+(s.anchor<180?130:-130),0,300),y:s.top+25,w:56-10*d,type:Math.random()<.5?'break':'vanish',item:null});
+      const enemyInterval = Math.max(3,8-Math.floor(d*5));
+      if(s.generated>14 && s.generated%enemyInterval===0) s.enemies.push({x:s.anchor<180?280:55,y:s.top-30,base:s.anchor<180?280:55,phase:random(0,6),r:16,speed:1.5+2*d,range:18+20*d});
+    }
+  }
+  function stepJump(dt) {
+    const s=state,p=s.player;
+    s.time += dt;
+    s.debris.forEach(d => { d.life -= dt; d.x += d.vx*dt; d.y += d.vy*dt; d.vy += d.gravity*dt; d.angle += d.spin*dt; });
+    s.debris = s.debris.filter(d => d.life > 0);
+    s.boost=Math.max(0,s.boost-dt); s.shield=Math.max(0,s.shield-dt); s.cooldown-=dt;
+    const speed=240;
+    p.x+=((pressed('right')?1:0)-(pressed('left')?1:0))*speed*dt;
+    if(p.x>W) p.x=-24; if(p.x<-24) p.x=W;
+    const oldBottom=p.y+30;
+    p.vy=s.boost>0?-640:p.vy+1450*dt;
+    p.y+=p.vy*dt;
+    s.platforms.forEach(a => {
+      if(a.type==='moving') { a.phase+=dt*(a.speed||1); a.x=clamp(a.base+Math.sin(a.phase)*22,0,W-a.w); }
+    });
+    if(p.vy>0) {
+      const landing=s.platforms.filter(a=>!a.dead&&oldBottom<=a.y&&p.y+30>=a.y&&p.x+24>a.x&&p.x<a.x+a.w).sort((a,b)=>a.y-b.y)[0];
+      if(landing) {
+        if(landing.type==='break') shatter(landing);
+        else {
+          p.y=landing.y-30; p.vy=-520;
+          if(landing.type==='vanish') shatter(landing);
+          if(landing.item==='spring') p.vy=-850;
+          if(landing.item==='jet') s.boost=2.5;
+          if(landing.item==='shield') s.shield=7;
+          if(landing.item!=='spring') landing.item=null;
+        }
+      }
+    }
+    if(pressed('shoot')&&s.cooldown<=0) { s.shots.push({x:p.x+12,y:p.y}); s.cooldown=.2; }
+    s.shots.forEach(b=>b.y-=750*dt);
+    s.enemies.forEach(e=>{
+      e.phase+=dt*(e.speed||2); e.x=e.base+Math.sin(e.phase)*(e.range||22);
+      s.shots.forEach(b=>{ if(!b.dead&&!e.dead&&Math.hypot(b.x-e.x,b.y-e.y)<e.r+6) {e.dead=true;b.dead=true;} });
+      if(!e.dead&&p.x+24>e.x-e.r&&p.x<e.x+e.r&&p.y+30>e.y-e.r&&p.y<e.y+e.r) {
+        if(s.shield>0||s.boost>0) e.dead=true;
+        else if(p.vy>0&&oldBottom<=e.y) {e.dead=true;p.vy=-520;}
+        else end('Столкновение с охранным дроном.');
+      }
+    });
+    s.camera=Math.min(s.camera,p.y-H*.42);
+    s.score=Math.max(s.score,Math.floor(-s.camera/10));
+    s.platforms=s.platforms.filter(a=>!a.dead&&a.y<s.camera+H+70);
+    s.enemies=s.enemies.filter(e=>!e.dead&&e.y<s.camera+H+60);
+    s.shots=s.shots.filter(b=>!b.dead&&b.y>s.camera-30);
+    generate(s);
+    if(p.y>s.camera+H+30) end('Падение. Попробуйте ещё раз!');
+  }
+  function shatter(platform) {
+    if(platform.dead) return;
+    platform.dead=true;
+    const ghost=platform.type==='vanish';
+    for(let i=0;i<8;i++) {
+      state.debris.push({
+        x:platform.x+(i+.5)*platform.w/8,y:platform.y+5,
+        w:platform.w/8-1,h:ghost?5:10,
+        vx:(i-3.5)*random(12,24),vy:ghost?random(-65,-15):random(-120,-45),
+        gravity:ghost?30:550,angle:0,spin:random(-5,5),
+        life:ghost?.65:1.1,color:ghost?'#b2e9ea':'#bd8151',
+        highlight:ghost?'#ffffff':'#f5c68a'
+      });
+    }
+  }
+  function rounded(x,y,w,h,r,color) {
+    ctx.fillStyle=color; ctx.beginPath(); ctx.roundRect(x,y,w,h,r); ctx.fill();
+  }
+  function drawOperator(x,y,vy,cooldown) {
+    // Outlined suit, helmet visor and boots retain the 24 × 30 collision silhouette.
+    ctx.save(); ctx.translate(x+12,y+15);
+    ctx.rotate(clamp(((pressed('right')?1:0)-(pressed('left')?1:0))*.09,-.09,.09));
+    rounded(-12,-3,24,18,5,'#15262a');
+    rounded(-10,-2,20,15,4,'#e6d5a5');
+    rounded(-14,0,5,10,2,'#789a83'); rounded(9,0,5,10,2,'#789a83');
+    rounded(-9,10,7,5,2,'#405a5f'); rounded(2,10,7,5,2,'#405a5f');
+    rounded(-12,-16,24,20,8,'#15262a');
+    rounded(-10,-15,20,18,7,'#f2e7c9');
+    rounded(-8,-12,16,11,4,'#255769');
+    rounded(-6,-11,12,4,2,'#67bbcd');
+    rect(-5,-10,4,2,'#d9faff');
+    rounded(-6,4,12,6,2,'#526c64');
+    circle(-3,7,1.3,'#c9ff6b'); circle(2,7,1.3,'#ffa967');
+    rect(-1,-18,3,5,'#b8cbd1');
+    if(cooldown>.14) { circle(.5,-20,4,'#ffe98a'); circle(.5,-20,2,'#fffbea'); }
+    ctx.restore();
+  }
+  function drawDrone(x,y,time) {
+    ctx.save(); ctx.translate(x,y);
+    // Wide rotor arms, armoured shell and one red sensor distinguish enemies.
+    rect(-25,-6,50,4,'#738395');
+    [-23,23].forEach(dx=>{
+      rect(dx-2,-12,4,10,'#a0b0b9');
+      const span=8+Math.sin(time*50)*4;
+      rounded(dx-span,-13,span*2,3,1,'#c1d6df');
+    });
+    rounded(-17,-12,34,26,9,'#18222f');
+    rounded(-15,-10,30,22,8,'#718496');
+    rounded(-12,-9,24,6,3,'#afbec8');
+    rounded(-12,-3,24,11,5,'#293345');
+    circle(0,2,7,'#752a46'); circle(0,2,4.5,'#ff526e'); circle(-1,0,1.7,'#ffe4d1');
+    rect(-10,11,5,4,'#344959'); rect(5,11,5,4,'#344959');
+    rect(-11,-5,3,2,'#ffcb77'); rect(8,-5,3,2,'#ffcb77');
+    ctx.restore();
+  }
+  function rect(x,y,w,h,color) {ctx.fillStyle=color;ctx.fillRect(x,y,w,h);}
+  function circle(x,y,r,color) {ctx.fillStyle=color;ctx.beginPath();ctx.arc(x,y,r,0,Math.PI*2);ctx.fill();}
+  function draw() {
+    rect(0,0,W,H,'#0d191c');
+    ctx.strokeStyle='#1a3033';ctx.lineWidth=1;
+    const grid = mode === 'snake' ? 20 : 24;
+    const offset=mode==='jump'?((-state.camera)%grid):0;
+    for(let x=0;x<W;x+=grid){ctx.beginPath();ctx.moveTo(x,0);ctx.lineTo(x,H);ctx.stroke();}
+    for(let y=offset;y<H;y+=grid){ctx.beginPath();ctx.moveTo(0,y);ctx.lineTo(W,y);ctx.stroke();}
+    if(mode==='snake') drawSnake(); else drawJump();
+    if(paused||over) {
+      rect(0,0,W,H,'rgba(4,10,12,.72)');
+      ctx.fillStyle='#dfffbd';ctx.font='bold 24px monospace';ctx.textAlign='center';
+      ctx.fillText(paused?'ПАУЗА':'КОНЕЦ ИГРЫ',W/2,H/2-10);
+      ctx.font='14px monospace';ctx.fillText('Счёт: '+state.score,W/2,H/2+22);ctx.textAlign='left';
+    }
+  }
+  function drawSnake() {
+    const s=state;
+    if(s.food) {circle(s.food.x*20+10,s.food.y*20+10,7,'#ff965e');}
+    s.body.forEach((p,i)=>rect(p.x*20+1,p.y*20+1,18,18,i===0?'#dfff91':'#77b86c'));
+    const h=s.body[0];rect(h.x*20+5,h.y*20+5,3,3,'#13201c');rect(h.x*20+12,h.y*20+5,3,3,'#13201c');
+  }
+  function drawJump() {
+    const s=state,p=s.player,cy=s.camera;
+    s.platforms.forEach(a=>{
+      const y=a.y-cy;
+      rect(a.x,y,a.w,9,({solid:'#9ce578',moving:'#65c5e6',break:'#c68d62',vanish:'#e0e9df'})[a.type]);
+      rect(a.x+3,y+9,a.w-6,3,'#38524c');
+      if(a.type==='break') {ctx.strokeStyle='#302522';ctx.beginPath();ctx.moveTo(a.x+20,y);ctx.lineTo(a.x+29,y+5);ctx.lineTo(a.x+22,y+9);ctx.stroke();}
+      if(a.item) {rect(a.x+a.w/2-9,y-20,18,18,a.item==='spring'?'#ffe38a':a.item==='jet'?'#ff986e':'#8cdeed');ctx.fillStyle='#152322';ctx.font='bold 14px monospace';ctx.fillText(({spring:'↑',jet:'J',shield:'S'})[a.item],a.x+a.w/2-4,y-6);}
+    });
+    s.debris.forEach(d => {
+      ctx.save(); ctx.globalAlpha = Math.min(1,d.life/.35);
+      ctx.translate(d.x,d.y-cy); ctx.rotate(d.angle);
+      rect(-d.w/2,-d.h/2,d.w,d.h,d.color);
+      rect(-d.w/2,-d.h/2,d.w,2,d.highlight);
+      ctx.restore();
+    });
+    s.enemies.forEach(e=>drawDrone(e.x,e.y-cy,s.time));
+    s.shots.forEach(b=>rect(b.x-2,b.y-cy,4,12,'#fff2a1'));
+    const y=p.y-cy;
+    if(s.shield>0){ctx.strokeStyle='#8cdeed';ctx.lineWidth=2;ctx.beginPath();ctx.arc(p.x+12,y+15,25,0,Math.PI*2);ctx.stroke();}
+    if(s.boost>0){rect(p.x+5,y+28,14,18+Math.sin(performance.now()/50)*5,'#ff965e');}
+    drawOperator(p.x,y,p.vy,s.cooldown);
+    ctx.fillStyle='#95b5aa';ctx.font='11px monospace';
+    if(s.boost>0)ctx.fillText('РАНЕЦ '+s.boost.toFixed(1),10,20);
+    else if(s.shield>0)ctx.fillText('ЩИТ '+Math.ceil(s.shield),10,20);
+  }
+})();
+
+/* ===== js\weather.js ===== */
+(() => {
+  const modal = document.querySelector('#weatherModal');
+  const status = document.querySelector('#weatherStatus');
+  const content = document.querySelector('#weatherContent');
+  const refresh = document.querySelector('#weatherRefresh');
+  let loadedAt = 0;
+  let pending = false;
+  const url = 'https://api.open-meteo.com/v1/forecast?' + new URLSearchParams({
+    latitude: '54.2093', longitude: '79.7477',
+    current: 'temperature_2m,apparent_temperature,is_day,precipitation,weather_code',
+    daily: 'temperature_2m_max,temperature_2m_min,weather_code,sunrise,sunset',
+    forecast_days: '3', timezone: 'Europe/Moscow'
+  });
+  const degrees = value => Number.isFinite(value) ? Math.round(value) + '°' : '—';
+  function sky(code, day = 1) {
+    if (code === 0) return [day ? '☀' : '☾', 'Ясно'];
+    if ([1, 2].includes(code)) return ['⛅', 'Переменная облачность'];
+    if (code === 3) return ['☁', 'Пасмурно'];
+    if ([45, 48].includes(code)) return ['≋', 'Туман'];
+    if ([51, 53, 55, 56, 57].includes(code)) return ['☂', 'Морось'];
+    if ([61, 63, 65, 66, 67, 80, 81, 82].includes(code)) return ['☂', 'Дождь'];
+    if ([71, 73, 75, 77, 85, 86].includes(code)) return ['❄', 'Снег'];
+    if ([95, 96, 99].includes(code)) return ['ϟ', 'Гроза'];
+    return ['◇', 'Нет данных'];
+  }
+  function clock(value) { return typeof value === 'string' && value.includes('T') ? value.split('T')[1].slice(0, 5) : '—'; }
+  function render(data) {
+    const current = data.current;
+    const daily = data.daily;
+    if (!current || !Number.isFinite(current.temperature_2m) || !daily || !Array.isArray(daily.time)) throw new Error('Incomplete forecast');
+    const [icon, label] = sky(current.weather_code, current.is_day);
+    const card = document.createElement('div');
+    card.className = 'weather-current';
+    const symbol = document.createElement('span');
+    symbol.className = 'weather-icon';
+    symbol.textContent = icon;
+    symbol.setAttribute('aria-hidden', 'true');
+    const summary = document.createElement('div');
+    const temperature = document.createElement('strong');
+    temperature.textContent = degrees(current.temperature_2m);
+    const description = document.createElement('p');
+    description.textContent = label + ' · ощущается как ' + degrees(current.apparent_temperature);
+    summary.append(temperature, description);
+    card.append(symbol, summary);
+    const details = document.createElement('p');
+    details.className = 'weather-details';
+    details.textContent = 'Осадки: ' + (Number.isFinite(current.precipitation) ? current.precipitation + ' мм' : '—') + ' · Восход ' + clock(daily.sunrise?.[0]) + ' · Закат ' + clock(daily.sunset?.[0]);
+    const forecast = document.createElement('div');
+    forecast.className = 'weather-forecast';
+    daily.time.slice(0, 3).forEach((date, index) => {
+      const day = document.createElement('div');
+      const title = document.createElement('span');
+      title.textContent = index === 0 ? 'Сегодня' : index === 1 ? 'Завтра' : new Date(date + 'T12:00:00Z').toLocaleDateString('ru-RU', { day: 'numeric', month: 'short', timeZone: 'UTC' });
+      const weather = document.createElement('span');
+      const [symbol, label] = sky(daily.weather_code?.[index]);
+      weather.textContent = symbol;
+      weather.setAttribute('aria-label', label);
+      weather.title = label;
+      const range = document.createElement('strong');
+      range.textContent = degrees(daily.temperature_2m_max?.[index]) + ' / ' + degrees(daily.temperature_2m_min?.[index]);
+      day.append(title, weather, range);
+      forecast.append(day);
+    });
+    content.replaceChildren(card, details, forecast);
+    content.hidden = false;
+    status.textContent = 'Данные на ' + clock(current.time) + ' МСК';
+  }
+  async function load(force = false) {
+    if (pending || (!force && Date.now() - loadedAt < 600000)) return;
+    pending = true;
+    refresh.disabled = true;
+    status.textContent = 'Загрузка прогноза…';
+    const controller = new AbortController();
+    const timeout = setTimeout(() => controller.abort(), 12000);
+    try {
+      const response = await fetch(url, { signal: controller.signal });
+      if (!response.ok) throw new Error('Weather unavailable');
+      render(await response.json());
+      loadedAt = Date.now();
+    } catch {
+      status.textContent = loadedAt ? 'Не удалось обновить. Показан предыдущий прогноз — попробуйте ещё раз.' : 'Погода сейчас недоступна. Проверьте интернет и нажмите «Обновить».';
+    } finally {
+      clearTimeout(timeout);
+      pending = false;
+      refresh.disabled = false;
+    }
+  }
+  document.querySelector('#weatherButton').addEventListener('click', () => {
+    modal.showModal();
+    load();
+  });
+  refresh.addEventListener('click', () => load(true));
+})();
+

BIN
ПМ.03.02/2.2.500 Ерошко/assets/circuit-original.png


BIN
ПМ.03.02/2.2.500 Ерошко/assets/posters/kadyrov.jfif


BIN
ПМ.03.02/2.2.500 Ерошко/assets/posters/max.jpeg


BIN
ПМ.03.02/2.2.500 Ерошко/assets/posters/svo.jpg


Разница между файлами не показана из-за своего большого размера
+ 0 - 17
ПМ.03.02/2.2.500 Ерошко/assets/zke-4-10-circuit.svg


+ 0 - 10
ПМ.03.02/2.2.500 Ерошко/css/arcade.css

@@ -1,10 +0,0 @@
-.arcade-modal { width:min(430px,calc(100% - 20px)); padding:18px; max-height:calc(100dvh - 20px); overflow-y:auto; }
-.arcade-heading,.arcade-hud,.arcade-actions { display:flex; justify-content:space-between; align-items:center; gap:10px; }
-.arcade-heading h2 { margin:0 0 12px; font-size:22px; }
-.arcade-hud { font:11px var(--mono); color:var(--accent); margin-bottom:10px; }
-#arcadeCanvas { display:block; width:100%; max-height:52dvh; aspect-ratio:3/4; object-fit:contain; background:#0d191c; border-radius:12px; border:1px solid var(--line); touch-action:none; }
-#arcadeStatus { min-height:20px; margin:9px 0; font-size:12px; color:var(--text); }
-.arcade-touch { display:flex; justify-content:center; gap:8px; margin:12px 0; }
-.arcade-touch button { min-width:42px; min-height:42px; padding:8px; border:1px solid var(--line); border-radius:8px; background:var(--panel-2); color:var(--text); touch-action:none; user-select:none; }
-.arcade-touch button:active { background:var(--accent-soft); }
-.arcade-modal .arcade-help { font-size:11px; color:var(--muted); margin-bottom:0; }

+ 0 - 59
ПМ.03.02/2.2.500 Ерошко/css/base.css

@@ -1,59 +0,0 @@
-/* Shared palette, typography and page composition. */
-:root {
-  color-scheme: light;
-  --bg: #f4f4ef; --panel: #fdfdfb; --panel-2: #f0f1ec; --line: #dddfd7;
-  --text: #242823; --muted: #656e63; --accent: #3659d9; --accent-soft: #e9edfd;
-  --on-accent: #fff; --danger: #b43f35; --danger-soft: #fbeeea;
-  --supply: #4e777d; --signal: #986016; --wire: #7e897c; --metal: #d2d6ce;
-  --mono: "Space Mono", monospace; --sans: "Manrope", sans-serif;
-}
-:root[data-theme="dark"] {
-  color-scheme: dark;
-  --bg: #171a18; --panel: #1f2320; --panel-2: #272d28; --line: #3b433c;
-  --text: #eceee7; --muted: #a5aea2; --accent: #a4b6ff; --accent-soft: #2d374e;
-  --on-accent: #17213d; --danger: #f69c8e; --danger-soft: #432d29;
-  --supply: #92b6bb; --signal: #e9bb78; --wire: #8c998d; --metal: #737e72;
-}
-:root {
-  --lab-glow: rgba(54,89,217,.16);
-  --lab-grid: rgba(126,137,124,.16);
-  --lab-grid-major: rgba(126,137,124,.22);
-}
-:root[data-theme="dark"] {
-  --lab-glow: rgba(164,182,255,.12);
-  --lab-grid: rgba(140,153,141,.18);
-  --lab-grid-major: rgba(140,153,141,.26);
-}
-* { box-sizing: border-box; }
-html { zoom: 1.15; scroll-behavior: smooth; scrollbar-gutter: stable; }
-body { margin: 0; min-width: 0; background: radial-gradient(circle at 12% 0%, var(--lab-glow), transparent 24%), radial-gradient(circle at 92% 18%, rgba(177,118,28,.08), transparent 26%), linear-gradient(var(--lab-grid-major) 1px, transparent 1px), linear-gradient(90deg, var(--lab-grid-major) 1px, transparent 1px), linear-gradient(var(--lab-grid) 1px, transparent 1px), linear-gradient(90deg, var(--lab-grid) 1px, transparent 1px), var(--bg); background-size: auto, auto, 128px 128px, 128px 128px, 32px 32px, 32px 32px; color: var(--text); font-family: var(--sans); -webkit-font-smoothing: antialiased; }
-body, .workspace, .modal, .lock-stage, .schematic-scroll { transition: background-color .25s, color .25s, border-color .25s; }
-button, input, select { font: inherit; }
-button { color: inherit; }
-button, a, input, select { -webkit-tap-highlight-color: transparent; }
-:focus-visible { outline: 2px solid var(--accent); outline-offset: 4px; }
-::selection { background: var(--accent); color: var(--on-accent); }
-h1, h2, p { margin-top: 0; }
-.app-shell { width: min(1280px, calc(100% - 96px)); margin-inline: auto; }
-.topbar { min-height: 94px; display: flex; align-items: center; justify-content: space-between; gap: 20px; border-bottom: 1px solid var(--line); position: relative; }
-.topbar::after { content: "LAB / INSTRUMENT CONTROL"; position: absolute; right: 0; bottom: 7px; color: var(--muted); font: 7px var(--mono); letter-spacing: .16em; opacity: .65; }
-.brand { display: flex; align-items: center; gap: 12px; color: var(--text); text-decoration: none; font-size: 13px; font-weight: 800; letter-spacing: -.02em; }
-.brand-mark { display: grid; place-items: center; width: 32px; height: 36px; color: var(--accent); }
-.brand-mark svg { width: 28px; height: 32px; }
-.brand i { font: 10px var(--mono); color: var(--muted); padding-left: 12px; border-left: 1px solid var(--line); }
-.brand-mark { filter: drop-shadow(0 0 8px var(--lab-glow)); }
-.topbar-actions { display: flex; align-items: center; gap: 8px; }
-.hero { display: grid; grid-template-columns: 1fr 310px; gap: 32px; align-items: end; padding: 64px 0 46px; }
-.eyebrow, .panel-kicker { margin: 0; font: 10px/1.6 var(--mono); letter-spacing: .09em; text-transform: uppercase; color: var(--muted); }
-.hero h1 { margin: 16px 0 0; font-size: clamp(30px, 4.4vw, 64px); font-weight: 500; line-height: 1.08; letter-spacing: -.065em; text-shadow: 0 12px 34px var(--lab-glow); }
-.hero-title__suffix { color: var(--accent); font-size: .72em; font-weight: 600; letter-spacing: -.04em; white-space: nowrap; }
-.hero h1 span { color: var(--accent); }
-.hero-copy { margin: 0 0 4px; color: var(--muted); font-size: 13px; line-height: 1.85; }
-.hero-meta { display: flex; align-items: center; gap: 8px; margin-top: 18px; font: 9px var(--mono); color: var(--muted); text-transform: uppercase; letter-spacing: .05em; }
-.hero-meta i { width: 6px; height: 6px; background: var(--accent); border-radius: 50%; }
-.hero-meta i { box-shadow: 0 0 0 4px var(--accent-soft), 0 0 12px var(--accent); }
-footer { display: flex; justify-content: space-between; gap: 16px; padding: 26px 0 36px; color: var(--muted); font: 9px/1.8 var(--mono); }
-@media (max-width: 1000px) { .app-shell { width: calc(100% - 48px); } .hero { grid-template-columns: 1fr 260px; } }
-@media (max-width: 800px) { .hero { grid-template-columns: 1fr; gap: 22px; padding-block: 38px 30px; } .hero-copy { max-width: 440px; } .hero-meta { margin-top: 12px; } .topbar-actions > .button--ghost { display: none; } }
-@media (max-width: 480px) { .app-shell { width: calc(100% - 32px); } .topbar { min-height: 76px; gap: 8px; } .brand i { display: none; } .brand { gap: 6px; font-size: 12px; } .topbar-actions { gap: 2px; } .hero h1 { font-size: clamp(28px, 8vw, 38px); } footer { flex-direction: column; gap: 4px; } }
-@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } *, *::before, *::after { animation: none !important; transition: none !important; } }

+ 0 - 232
ПМ.03.02/2.2.500 Ерошко/css/circuit.css

@@ -1,232 +0,0 @@
-/* Electronic lock: supply, driver, relay and switched electromagnet. */
-.circuit-panel .panel-heading { margin-bottom: 17px; }
-.circuit-panel { position: relative; }
-.circuit-status { display: flex; align-items: center; gap: 8px; color: var(--muted); font-size: 10px; margin-bottom: 12px; padding: 10px 12px; border: 1px solid var(--line); border-radius: 7px; background: color-mix(in srgb, var(--panel-2) 76%, transparent); box-shadow: inset 0 0 18px rgba(22,127,138,.06); }
-.circuit-status__dot { width: 6px; height: 6px; flex: 0 0 auto; border-radius: 50%; background: var(--muted); }
-.circuit-status__dot { box-shadow: 0 0 0 3px color-mix(in srgb, var(--muted) 20%, transparent); }
-.supply-label { margin-left: auto; white-space: nowrap; color: var(--supply); font: 10px var(--mono); }
-.schematic-scroll { overflow-x: auto; border: 1px solid var(--line); border-radius: 12px; background: radial-gradient(circle at 50% 15%, var(--lab-glow), transparent 42%), var(--panel-2); scrollbar-color: var(--wire) var(--panel-2); box-shadow: inset 0 0 28px rgba(20,28,22,.08); }
-.diagram-toolbar { padding: 6px 8px; border: 1px solid var(--line); border-radius: 7px; background: color-mix(in srgb, var(--panel-2) 70%, transparent); }
-.schematic-scroll:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
-.component-tooltip { position: relative; width: 100%; margin-top: 12px; padding: 16px 44px 15px 16px; border: 1px solid var(--accent); border-radius: 8px; background: var(--panel); box-shadow: 0 8px 20px rgba(0,0,0,.12); animation: tooltip-in .16s ease-out; }
-.component-tooltip[hidden] { display: none; }
-.component-tooltip__label { display: block; margin-bottom: 5px; color: var(--accent); font: 8px var(--mono); letter-spacing: .09em; }
-.component-tooltip strong { display: block; margin-bottom: 6px; color: var(--text); font: 700 13px/1.35 var(--mono); }
-.component-tooltip p { margin: 0; color: var(--muted); font-size: 11px; line-height: 1.6; }
-.component-tooltip__close { position: absolute; top: 8px; right: 9px; border: 0; background: transparent; color: var(--muted); cursor: pointer; font-size: 18px; line-height: 1; }
-.component-tooltip__close:hover { color: var(--text); }
-@keyframes tooltip-in { from { opacity: 0; transform: translateY(5px); } to { opacity: 1; transform: translateY(0); } }
-.schematic { display: block; width: 100%; min-width: 460px; color: var(--wire); }
-.schematic-grid { opacity: .4; }
-.schematic text { fill: var(--text); font: 11px var(--mono); }
-.schematic .component-label { fill: var(--text); font-weight: 700; }
-.schematic .schematic-muted { fill: var(--muted); font-size: 10px; }
-.schematic .supply-text { fill: var(--supply); font-weight: 700; }
-.wire { stroke: var(--wire); stroke-width: 1.7; fill: none; stroke-linejoin: round; stroke-linecap: round; transition: stroke .15s; }
-.wire circle:not(.transistor circle) { fill: var(--panel-2); }
-.supply { stroke: var(--supply); }
-.ground { stroke: var(--wire); }
-.controller-body, .keypad-input rect { fill: var(--panel); stroke: var(--wire); stroke-width: 1.5; }
-.controller [data-code-cell] rect { fill: var(--panel-2); stroke: var(--wire); }
-.controller [data-code-cell] text { font-size: 10px; fill: var(--muted); }
-.controller [data-code-cell].is-filled rect { fill: var(--accent); stroke: var(--accent); }
-.controller [data-code-cell].is-filled text { fill: var(--on-accent); font-weight: 700; }
-.schematic.has-input .controller-body { stroke: var(--accent); fill: var(--accent-soft); }
-.schematic.is-receiving :is(.input-signal, .keypad-input rect) { stroke: var(--signal); }
-.schematic.is-receiving .keypad-input text { fill: var(--signal); }
-.schematic.is-receiving .input-signal { stroke-width: 3; }
-.legend-dot--input { background: var(--signal); }
-.close-lock-button { display: block; width: 100%; margin-top: 14px; min-height: 44px; }
-.lock-stage { height: auto; aspect-ratio: 2 / 1; }
-.mechanism-cutaway { display: block; width: 100%; position: relative; color: var(--wire); }
-.mechanism-frame { fill: var(--panel); stroke: var(--line); }
-.mechanism-receiver, .mechanism-bolt rect { fill: var(--metal); stroke: var(--wire); }
-.mechanism-slot { fill: var(--panel-2); }
-.mechanism-bolt { transition: transform .55s cubic-bezier(.2,.8,.2,1); }
-.mechanism-bolt path { stroke: var(--wire); stroke-width: 4; }
-.mechanism-coil { fill: var(--panel); stroke: var(--wire); }
-.mechanism-turns { opacity: .65; }
-.mechanism-callouts path, .mechanism-spring { fill: none; stroke: var(--wire); stroke-width: 1.4; }
-.mechanism-callouts text { fill: var(--muted); font: 11px var(--sans); }
-.mechanism-force { opacity: 0; stroke: var(--accent); fill: none; stroke-width: 2; }
-text.mechanism-force { stroke: none; fill: var(--accent); font: 10px var(--sans); }
-.lock-stage.is-open .mechanism-bolt { transform: translateX(-65px); }
-.lock-stage.is-open .mechanism-coil { stroke: var(--accent); fill: var(--accent-soft); }
-.lock-stage.is-open .mechanism-turns { color: var(--accent); }
-.lock-stage.is-open .mechanism-force { opacity: 1; }
-.mechanism-explanation { color: var(--muted); font-size: 10px; line-height: 1.8; margin: 12px 0 0; }
-button:disabled { cursor: not-allowed; }
-.close-lock-button:disabled { opacity: 1; background: var(--panel-2); color: var(--muted); }
-.text-button:disabled, .keypad button:disabled:not(.keypad__disabled), .code-input-wrap :disabled, #backspaceButton:disabled { opacity: .4; }
-.mechanical-link { fill: none; stroke: var(--wire); stroke-width: 1; stroke-dasharray: 4 5; }
-.current-flow { fill: none; stroke: var(--accent); stroke-width: 3; stroke-dasharray: 3 13; stroke-linecap: round; opacity: 0; }
-.schematic[data-state="open"] .current-flow { opacity: 1; animation: circuit-current 1s linear infinite; }
-.schematic[data-state="open"] :is(.relay-path, .load-path, .transistor, .control-path, .contact) { stroke: var(--accent); }
-.schematic[data-state="open"] .controller-body { stroke: var(--accent); fill: var(--accent-soft); }
-.schematic[data-state="error"] .controller-body { stroke: var(--danger); fill: var(--danger-soft); }
-.circuit-readings { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin: 12px 0; }
-.circuit-readings > div { padding: 12px 0; }
-.circuit-readings span { display: block; color: var(--muted); font: 8px var(--mono); letter-spacing: .04em; }
-.circuit-readings strong { display: block; margin-top: 5px; color: var(--text); font: 12px var(--mono); }
-.circuit-panel:has([data-state="open"]) .circuit-readings strong { color: var(--accent); }
-.circuit-panel:has([data-state="open"]) .circuit-status__dot { background: var(--accent);  }
-.circuit-panel:has([data-state="error"]) .circuit-status__dot { background: var(--danger); }
-.legend-dot--supply { background: var(--supply); }
-.circuit-panel .legend { flex-wrap: wrap; gap: 8px 16px; }
-.schematic-note { margin: 12px 0 0; color: var(--muted); font-size: 10px; line-height: 1.6; }
-.schematic-scroll-hint { display: none; margin: 8px 0 0; color: var(--muted); font-size: 10px; }
-.extras-button { display: inline-grid; place-items: center; font-size: 23px; line-height: 1; flex: 0 0 34px; }
-.extras-modal { width: min(560px, calc(100% - 24px)); max-height: calc(100dvh - 32px); overflow-y: auto; }
-.extras-games { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin: 22px 0; }
-.extras-games button { display: grid; justify-items: center; gap: 10px; padding: 16px 8px; }
-.extras-games button > span { color: var(--accent); font: 24px var(--mono); }
-.extras-modal .radio-panel { margin: 0; padding: 16px; gap: 16px; box-shadow: none; }
-.extras-modal .radio-controls { flex: 1 1 100%; flex-wrap: wrap; }
-.extras-modal .radio-controls select { max-width: none; flex: 1; }
-.extras-modal .radio-volume { flex-basis: 100%; padding-top: 6px; }
-.extras-modal .radio-panel__intro .panel-kicker { font-size: 9px; color: var(--accent); margin: 0; }
-.extras-modal .radio-panel__signal { animation: none; color: var(--muted); text-shadow: none; }
-.extras-modal .radio-panel:has(audio[data-playing="true"]) .radio-panel__signal { color: var(--accent); }
-.extras-modal .extras-note { margin: 12px 0 0; color: var(--muted); font-size: 11px; }
-/* Detailed original circuit, inspector and learning controls. */
-.app-shell { width: min(1520px, calc(100% - 64px)); }
-.workspace { grid-template-columns: minmax(270px, .37fr) minmax(0, 1fr); }
-.workbench-column { padding: 26px; }
-.scheme-heading-actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 14px; }
-.scheme-intro { max-width: 640px; margin: -3px 0 18px; color: var(--muted); font-size: 12px; line-height: 1.8; }
-.diagram-toolbar { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 10px; }
-.view-switch, .zoom-tools { display: flex; align-items: center; gap: 3px; }
-.view-switch { background: var(--panel-2); padding: 3px; border-radius: 7px; }
-.view-switch button, .zoom-tools button, .node-selector button { min-height: 34px; padding: 7px 10px; border: 1px solid transparent; background: transparent; color: var(--muted); border-radius: 5px; font-size: 10px; cursor: pointer; }
-.view-switch [aria-pressed="true"] { color: var(--text); background: var(--panel); border-color: var(--line); }
-.zoom-tools button { border-color: var(--line); font: 12px var(--mono); }
-.zoom-tools button:disabled { opacity: .4; }
-.zoom-tools button:hover:not(:disabled), .node-selector button:hover { background: var(--accent-soft); }
-.zoom-tools[hidden], #diagramHost[hidden], #originalCircuit[hidden] { display: none; }
-.schematic-scroll { max-height: 650px; overflow: auto; background: var(--panel-2); }
-#diagramHost { min-width: 900px; }
-.schematic { width: 100%; min-width: 0; color: var(--wire); }
-.schematic .drawing { fill: none; stroke: var(--wire); stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round; }
-.schematic text { fill: var(--text); stroke: none; font: 12px var(--mono); }
-.schematic .schematic-muted { font-size: 10px; fill: var(--muted); }
-.schematic .zone-label { fill: var(--muted); font-size: 11px; }
-.schematic .patch-label { font-size: 9px; fill: var(--muted); }
-.schematic .mains-label { font-size: 10px; text-anchor: middle; }
-.schematic .junction { fill: var(--wire); stroke: none; }
-.circuit-part { cursor: pointer; }
-.circuit-part rect, .circuit-part circle { fill: var(--panel-2); }
-.circuit-part:hover, .circuit-part:focus, .circuit-part.is-selected { stroke: var(--accent); outline: none; }
-.circuit-part:hover text, .circuit-part:focus text, .circuit-part.is-selected text { fill: var(--accent); }
-.circuit-part.is-selected { stroke: var(--signal); }
-.circuit-part.is-selected text { fill: var(--signal); }
-.circuit-part:focus-visible { filter: drop-shadow(0 0 2px var(--accent)); }
-.schematic .memory-device.is-conducting { stroke: var(--accent); stroke-width: 3; }
-.schematic .memory-device.is-conducting text { fill: var(--accent); }
-.schematic[data-state="error"] :is(.memory-device, [data-part="VT1"], [data-part="R19"], [data-part="C3"]) { stroke: var(--danger); }
-.schematic[data-state="error"] .memory-device text { fill: var(--danger); }
-.schematic[data-state="open"] :is([data-part="YA1"], [data-part="HL1"], [data-part="VT1"], [data-part="VT3"], [data-part="VT4"], [data-part="C1"]) { stroke: var(--accent); }
-.schematic[data-state="open"] [data-part="HL1"] circle { fill: var(--accent-soft); }
-.signal-overlay { stroke: var(--signal); stroke-width: 3; fill: none; opacity: 0; }
-.schematic.is-receiving .signal-overlay { opacity: 1; }
-.schematic .key-active { stroke: var(--signal); stroke-width: 3; }
-.schematic .current-flow { pointer-events: none; }
-.diagram-caption { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 8px; padding: 10px 0 16px; color: var(--muted); font: 9px/1.6 var(--mono); }
-#keypadSignal { color: var(--signal); }
-.thyristor-stages { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; margin: 0 0 16px; }
-.thyristor-stages > div { display: grid; grid-template-columns: 1fr auto; gap: 6px; padding: 12px; background: linear-gradient(145deg, color-mix(in srgb, var(--panel-2) 88%, var(--accent-soft)), var(--panel-2)); border: 1px solid var(--line); border-radius: 8px; box-shadow: inset 0 1px rgba(255,255,255,.08); }
-.thyristor-stages span { grid-column: 1 / -1; color: var(--muted); font: 9px var(--mono); }
-.thyristor-stages strong { font: 13px var(--mono); }
-.thyristor-stages small { font: 9px/1.8 var(--mono); color: var(--muted); }
-.thyristor-stages .is-conducting { background: var(--accent-soft); border-color: var(--accent); }
-.thyristor-stages .is-conducting :is(strong, small) { color: var(--accent); }
-.circuit-readings { margin: 4px 0 10px; }
-.circuit-readings > div { min-width: 0; }
-.simulation-actions { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
-.hold-option { display: flex; align-items: center; gap: 8px; flex-basis: 100%; margin: 5px 0 8px; font-size: 11px; color: var(--muted); cursor: pointer; }
-.hold-option input { width: 15px; height: 15px; accent-color: var(--accent); }
-.node-selector { display: flex; flex-wrap: wrap; gap: 5px; margin: 24px 0 10px; }
-.node-selector button { border-color: var(--line); }
-.node-selector [aria-pressed="true"] { background: var(--accent-soft); color: var(--accent); border-color: var(--accent); }
-.node-selector [aria-pressed="true"] { box-shadow: inset 0 -2px var(--accent), 0 0 12px var(--lab-glow); }
-.component-detail { padding: 22px; border: 1px solid color-mix(in srgb, var(--accent) 26%, var(--line)); border-radius: 10px; background: linear-gradient(145deg, color-mix(in srgb, var(--panel) 92%, var(--accent-soft)), var(--panel)); }
-.component-detail-disclosure { margin-top: 14px; }
-.component-detail-disclosure > summary { display: flex; align-items: baseline; justify-content: space-between; gap: 14px; padding: 14px 0; border-block: 1px solid var(--line); color: var(--text); cursor: pointer; font: 600 12px var(--sans); list-style: none; }
-.component-detail-disclosure > summary::-webkit-details-marker { display: none; }
-.component-detail-disclosure > summary::before { content: "+"; width: 18px; color: var(--accent); font: 16px var(--mono); }
-.component-detail-disclosure[open] > summary::before { content: "−"; }
-.component-detail-disclosure > summary span { margin-left: auto; color: var(--muted); font: 9px var(--mono); }
-.component-detail-disclosure > summary:hover { color: var(--accent); }
-.component-detail-disclosure .component-detail { margin-top: 12px; }
-.component-detail h3 { font-size: 16px; font-weight: 600; line-height: 1.6; margin: 7px 0; }
-.component-detail p { margin-bottom: 12px; color: var(--muted); font-size: 12px; line-height: 1.85; }
-.component-detail .detail-refs { font: 10px/1.8 var(--mono); color: var(--accent); }
-.detail-route { padding: 13px 15px; background: var(--panel-2); border-radius: 6px; }
-.detail-route > span { color: var(--muted); font: 8px var(--mono); letter-spacing: .06em; }
-.detail-route p { margin: 5px 0 0; }
-.component-detail .detail-state { border-left: 2px solid var(--accent); padding-left: 12px; margin: 14px 0 0; color: var(--text); }
-.component-overview { display: grid; gap: 10px; margin-top: 14px; }
-.component-overview p { margin: 0; }
-.circuit-disclosure { border-bottom: 1px solid var(--line); }
-.circuit-disclosure summary { cursor: pointer; padding: 18px 0; font-size: 12px; font-weight: 600; }
-.operation-steps { padding-left: 24px; margin: 0 0 20px; }
-.operation-steps li { color: var(--muted); padding: 0 0 12px 5px; font-size: 12px; line-height: 1.9; }
-.operation-steps b { color: var(--text); }
-.component-table-wrap { overflow: auto; margin-bottom: 18px; }
-.component-table { border-collapse: collapse; width: 100%; font-size: 11px; text-align: left; }
-.component-table th, .component-table td { border-bottom: 1px solid var(--line); padding: 10px 8px; font-weight: 400; line-height: 1.7; }
-.component-table thead { color: var(--muted); }
-.component-table button { border: 0; padding: 5px; background: var(--accent-soft); color: var(--accent); font: 10px var(--mono); cursor: pointer; border-radius: 4px; }
-.schematic-source { color: var(--muted); font-size: 10px; line-height: 1.8; margin: 12px 0 0; }
-.schematic-source a { color: var(--muted); text-underline-offset: 3px; }
-.circuit-modal { position: fixed; inset: 0; width: calc(100vw - 36px); max-width: 1600px; height: calc(100dvh - 36px); max-height: none; padding: 30px; border: 1px solid var(--line); border-radius: 14px; background: var(--panel); color: var(--text); overflow: auto; }
-.circuit-modal::backdrop { background: #11171390; backdrop-filter: blur(4px); }
-.circuit-modal .circuit-panel { max-width: 1400px; margin: 0 auto; }
-.circuit-modal .panel-heading { position: sticky; top: -1px; padding: 12px 0; background: var(--panel); z-index: 2; }
-.circuit-modal .schematic-scroll { max-height: 72vh; }
-#finishPulse:disabled { display: none; }
-#collapseCircuit, .circuit-modal #expandCircuit { display: none; }
-.circuit-modal #collapseCircuit { display: inline-block; }
-.close-lock-button { display: block; width: 100%; margin-top: 14px; min-height: 44px; }
-@media (max-width: 1100px) { .app-shell { width: calc(100% - 40px); } .workbench-column { padding: 20px; } .workspace { grid-template-columns: 280px minmax(0, 1fr); } .thyristor-stages > div { padding: 9px; } .thyristor-stages small { grid-column: 1 / -1; } }
-@media (max-width: 800px) { .workspace { grid-template-columns: 1fr; } .workbench-column:first-child { display: grid; grid-template-columns: 1fr 1fr; align-items: start; gap: 24px; } .workbench-column:first-child .panel + .panel { padding-top: 0; border: 0; } .circuit-modal { padding: 20px; } }
-@media (max-width: 540px) { .app-shell { width: calc(100% - 24px); } .workbench-column:first-child { display: flex; } .workbench-column:first-child .panel + .panel { padding-top: 24px; border-top: 1px solid var(--line); } .workbench-column { padding: 18px; } .thyristor-stages { gap: 5px; } .thyristor-stages > div { padding: 8px; } .thyristor-stages span { font-size: 8px; } .component-detail { padding: 16px; } .view-switch button { padding-inline: 7px; } .zoom-tools button { padding-inline: 8px; } .circuit-modal { padding: 16px; width: calc(100vw - 16px); height: calc(100dvh - 16px); } .scheme-heading-actions { gap: 10px; } .circuit-readings span { font-size: 7px; } .circuit-readings strong { font-size: 10px; } }
-@keyframes circuit-current { to { stroke-dashoffset: -32; } }
-@media (max-width: 800px) { .extras-button { display: inline-grid; } }
-@media (max-width: 540px) { .schematic-scroll-hint { display: block; } }
-@media (max-width: 440px) { .extras-modal { padding: 20px; } .circuit-readings > div { padding: 9px 6px; } .circuit-status { flex-wrap: wrap; } .supply-label { display: none; } }
-@media (prefers-reduced-motion: reduce) { .schematic[data-state="open"] .current-flow { animation: none; } .armature, .beam { transition: none; } }
-
-/* The working schematic consists only of SVG paths and component symbols. */
-.diagram-format { display: flex; align-items: center; gap: 8px; color: var(--muted); font: 9px/1.6 var(--mono); }
-.diagram-format > span { padding: 3px 5px; border: 1px solid var(--line); border-radius: 3px; color: var(--accent); }
-.svg-download { display: inline-flex; align-items: center; min-height: 34px; padding: 6px 8px; border: 1px solid var(--line); border-radius: 5px; color: var(--accent); font: 10px var(--mono); text-decoration: none; }
-#diagramHost { min-width: 1050px; }
-.vector-schematic .part-hitbox { fill: transparent; stroke: none; }
-.vector-schematic .circuit-part:hover .part-hitbox, .vector-schematic .circuit-part:focus .part-hitbox { fill: var(--accent-soft); opacity: .55; }
-.vector-schematic .circuit-part.is-selected .part-symbol { stroke: var(--signal); }
-.vector-schematic .circuit-part.is-selected .component-label { fill: var(--signal); }
-.vector-schematic .is-biased .part-symbol { stroke: var(--supply); }
-.vector-schematic .is-biased .arrow-fill { fill: var(--supply); }
-.vector-schematic .is-on .part-symbol, .vector-schematic .is-conducting .part-symbol { stroke: var(--accent); stroke-width: 2.5; }
-.vector-schematic .is-on .arrow-fill { fill: var(--accent); }
-.vector-schematic .is-on .component-label, .vector-schematic .is-conducting .component-label { fill: var(--accent); }
-.vector-schematic .memory-device.is-conducting .part-symbol { stroke: var(--accent); stroke-width: 2.5; }
-.vector-schematic .memory-device.is-conducting .component-label { fill: var(--accent); }
-.vector-schematic .net.is-conducting { stroke: var(--accent); stroke-width: 2.3; }
-.vector-schematic .logic-supply-net { stroke: var(--supply); }
-.vector-schematic .code-wire { stroke: var(--accent); stroke-width: 2.2; }
-.vector-schematic .exit-current { display: none; }
-.vector-schematic[data-exit="true"] .exit-current { display: inline; }
-.vector-schematic[data-exit="true"] .code-current { display: none; }
-.vector-schematic[data-state="open"] [data-part="HL1"] .lamp-bulb { fill: var(--accent-soft); stroke: var(--accent); }
-.vector-schematic[data-state="open"] [data-part="YA1"] .part-symbol, .vector-schematic[data-state="open"] [data-part="HL1"] .part-symbol { stroke: var(--accent); stroke-width: 2.5; }
-.vector-schematic[data-state="open"][data-exit="false"] [data-part="C1"] .part-symbol { stroke: var(--accent); animation: capacitor-charge 1.5s linear both; }
-.vector-schematic.is-held[data-state="open"] [data-part="C1"] .part-symbol { animation: none; }
-.vector-schematic[data-state="error"] :is(.memory-device,[data-part="VT1"],[data-part="C3"],[data-part="R19"]) .part-symbol { stroke: var(--danger); }
-.vector-schematic[data-state="error"] .fault-flow { opacity: 1; }
-.vector-schematic.is-releasing [data-part="VT3"] .part-symbol { stroke: var(--accent); }
-.vector-schematic.is-releasing :is([data-part="VT1"],[data-part="VT4"]) .part-symbol { stroke: var(--wire); }
-.vector-schematic.is-flyback .flyback-flow { opacity: 1; animation: circuit-current .7s linear infinite; }
-.vector-schematic.is-receiving .key-active .part-symbol, .vector-schematic.is-receiving .key-active { stroke: var(--signal); stroke-width: 2.5; }
-@keyframes capacitor-charge { from { opacity: .35; } to { opacity: 1; } }
-@media (max-width: 540px) { .diagram-format { max-width: 135px; font-size: 8px; } .svg-download { padding-inline: 5px; } }

+ 0 - 196
ПМ.03.02/2.2.500 Ерошко/css/components.css

@@ -1,196 +0,0 @@
-/* Workbench, controls and supporting dialogs. */
-.workspace { display: grid; grid-template-columns: .85fr 1.15fr; background: var(--panel); border: 1px solid var(--line); border-radius: 18px; overflow: hidden; }
-.workspace { background: color-mix(in srgb, var(--panel) 92%, var(--accent-soft)); box-shadow: 0 24px 60px rgba(11,29,29,.18), inset 0 1px rgba(255,255,255,.28); }
-.workspace::before { display: none; }
-.workspace { position: relative; }
-.workbench-column { min-width: 0; padding: 30px; display: flex; flex-direction: column; gap: 34px; }
-.workbench-column > .panel:first-child { margin-top: 0; }
-.workbench-column + .workbench-column { border-left: 1px solid var(--line); }
-.panel { min-width: 0; }
-.panel + .panel { padding-top: 30px; border-top: 1px solid var(--line); }
-.panel-heading { display: flex; justify-content: space-between; align-items: flex-start; gap: 14px; margin-bottom: 22px; }
-.panel-heading h2 { text-wrap: balance; }
-.panel h2 { margin: 6px 0 0; font-size: 20px; font-weight: 600; line-height: 1.35; letter-spacing: -.04em; }
-.panel h2::after { content: ""; display: block; width: 32px; height: 2px; margin-top: 9px; background: var(--accent); opacity: .65; }
-.button, .icon-button, .text-button { border: 0; cursor: pointer; transition: background .2s, color .2s, border-color .2s, transform .2s; }
-.button { min-height: 40px; padding: 11px 16px; border-radius: 8px; font-size: 11px; font-weight: 700; }
-.button--primary { background: linear-gradient(135deg, var(--accent), color-mix(in srgb, var(--accent) 72%, #15203f)); color: var(--on-accent); box-shadow: 0 5px 14px var(--lab-glow); }
-.button--primary:hover:not(:disabled) { filter: brightness(1.08); transform: translateY(-1px); }
-.button--ghost { background: color-mix(in srgb, var(--panel) 70%, transparent); border: 1px solid var(--line); }
-.button--ghost:hover { background: var(--panel-2); border-color: var(--muted); }
-.button--ghost { letter-spacing: .02em; }
-.icon-button { display: inline-grid; place-items: center; width: 36px; height: 36px; background: transparent; border-radius: 50%; font-size: 18px; color: var(--muted); }
-.icon-button svg { width: 18px; height: 18px; }
-.icon-button:hover, .icon-button.is-active { background: var(--accent-soft); color: var(--accent); }
-.text-button { padding: 6px 0; background: none; color: var(--accent); font-size: 11px; }
-.text-button:hover:not(:disabled) { text-decoration: underline; text-underline-offset: 4px; }
-.button:active:not(:disabled), .icon-button:active { transform: translateY(1px); }
-button:disabled { cursor: not-allowed; }
-.status-pill { padding: 6px 9px; border-radius: 5px; background: var(--panel-2); color: var(--muted); font: 9px var(--mono); letter-spacing: .08em; box-shadow: inset 0 0 0 1px rgba(255,255,255,.06); }
-.status-pill.is-open { background: var(--accent-soft); color: var(--accent); }
-.lock-stage { position: relative; height: 184px; overflow: hidden; border-radius: 10px; border: 1px solid var(--line); background: var(--panel-2); }
-.lock-stage__grid { position: absolute; inset: 0; background-image: radial-gradient(var(--wire) .7px, transparent .7px); background-size: 16px 16px; opacity: .13; }
-.coil { position: absolute; z-index: 2; left: 8%; top: 44px; width: 78px; height: 96px; border: 1px solid var(--wire); border-radius: 6px; background: repeating-linear-gradient(90deg, var(--panel) 0 5px, var(--metal) 5px 8px); display: grid; place-items: center; transition: border-color .25s; }
-.coil span { padding: 5px 7px; background: var(--panel); color: var(--muted); font: 9px var(--mono); border-radius: 3px; }
-.armature { position: absolute; z-index: 1; left: 24%; top: 74px; width: 66%; height: 34px; border: 1px solid var(--wire); border-radius: 3px; background: var(--metal); transition: left .55s cubic-bezier(.2,.8,.2,1); }
-.armature span { position: absolute; right: -5px; top: 5px; height: 22px; width: 10px; background: var(--wire); border-radius: 2px; }
-.receiver { position: absolute; right: 7%; top: 40px; width: 28px; height: 104px; border: 1px solid var(--wire); border-radius: 4px; background: var(--metal); }
-.receiver::after { content: ""; position: absolute; inset: 31px 5px; background: var(--panel-2); border-radius: 2px; }
-.beam { display: none; }
-.lock-stage.is-open .armature { left: 9%; }
-.lock-stage.is-open .coil { border-color: var(--accent); }
-.lock-stage.is-open .coil span { color: var(--accent); background: var(--accent-soft); }
-.readout { display: grid; align-items: center; grid-template-columns: auto 1fr 6px; gap: 8px; padding: 15px 0 0; color: var(--muted); font: 8px/1.6 var(--mono); }
-.readout strong { text-align: right; color: var(--text); font-size: 9px; font-weight: 400; }
-.readout-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--line); }
-.readout-dot.is-on { background: var(--accent); }
-.event-log { margin-top: 18px; }
-.event-log > span { color: var(--muted); font: 8px var(--mono); letter-spacing: .08em; }
-.event-log p { color: var(--muted); margin: 5px 0 0; min-height: 34px; font-size: 11px; line-height: 1.6; }
-.event-log { padding: 12px; border: 1px dashed var(--line); border-radius: 6px; background: color-mix(in srgb, var(--panel-2) 60%, transparent); }
-.attempt-log { margin-top: 10px; padding: 12px; border: 1px solid var(--line); border-radius: 6px; background: color-mix(in srgb, var(--panel-2) 42%, transparent); }
-.attempt-log__heading { display: flex; align-items: center; justify-content: space-between; gap: 8px; color: var(--muted); font: 8px var(--mono); letter-spacing: .08em; }
-.attempt-log__heading .text-button { padding: 2px 0; font: 9px var(--sans); letter-spacing: 0; }
-.attempt-log__heading .text-button:disabled { opacity: .45; cursor: default; }
-.attempt-log ol { display: grid; gap: 5px; margin: 9px 0 0; padding: 0; list-style: none; }
-.attempt-log__empty { color: var(--muted); font-size: 10px; }
-.attempt-log__item { display: grid; grid-template-columns: minmax(45px, .7fr) minmax(52px, 1fr) auto; align-items: center; gap: 8px; padding: 6px 8px; border-radius: 4px; background: var(--panel-2); font-size: 10px; }
-.attempt-log__code, .attempt-log__item time { font-family: var(--mono); }
-.attempt-log__result { color: var(--danger); }
-.attempt-log__item--success .attempt-log__result { color: var(--accent); }
-.attempt-log__item time { color: var(--muted); font-size: 9px; }
-.explanation { display: flex; gap: 14px; padding: 16px 0; border-block: 1px solid var(--line); }
-.explanation__number { color: var(--accent); font: 20px/1.3 var(--mono); flex-shrink: 0; }
-.explanation p { margin: 0; font-size: 11px; color: var(--muted); line-height: 1.7; }
-.legend { display: flex; gap: 14px; margin-top: 15px; color: var(--muted); font-size: 9px; }
-.legend-dot { display: inline-block; width: 5px; height: 5px; border-radius: 50%; margin-right: 5px; background: var(--wire); }
-.legend-dot--active { background: var(--accent); }
-.legend-dot--error { background: var(--danger); }
-.code-display { display: grid; grid-template-columns: 40px 1fr 32px; align-items: center; margin-bottom: 14px; padding: 8px 0 16px; border-bottom: 1px solid var(--line); }
-.code-display > span { color: var(--muted); font: 9px var(--mono); }
-.code-display strong { color: var(--text); text-align: center; font: 24px var(--mono); letter-spacing: .18em; }
-.code-display button { border: 0; cursor: pointer; background: transparent; color: var(--muted); font-size: 18px; }
-.lamp { width: 8px; height: 8px; margin-top: 7px; background: var(--line); border-radius: 50%; transition: background .2s; }
-.lamp.is-success { background: var(--accent); }
-.lamp.is-error { background: var(--danger); }
-.keypad { display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px; }
-.keypad button { height: 42px; border: 1px solid transparent; border-radius: 6px; background: var(--panel-2); color: var(--text); font: 14px var(--mono); cursor: pointer; transition: background .15s, border-color .15s, transform .15s; }
-.keypad button:hover:not(:disabled) { background: var(--accent-soft); color: var(--accent); border-color: var(--accent); }
-.keypad button:active:not(:disabled) { transform: scale(.96); }
-.keypad .keypad__disabled { visibility: hidden; }
-.keypad .keypad__reset { font-size: 9px; color: var(--muted); background: transparent; }
-.keyboard-tip { color: var(--muted); font-size: 10px; margin: 14px 0 0; }
-.field-label { display: flex; justify-content: space-between; gap: 10px; margin-bottom: 10px; color: var(--text); font-size: 12px; }
-.field-label span { font-size: 10px; color: var(--muted); }
-.code-input-wrap { display: flex; gap: 8px; }
-.code-input-wrap input { width: 100%; min-width: 0; padding: 9px 13px; border: 1px solid var(--line); border-radius: 8px; background: var(--panel); color: var(--text); font: 16px var(--mono); letter-spacing: .18em; }
-.field-hint { margin: 10px 0 22px; color: var(--muted); font-size: 10px; line-height: 1.7; }
-.control-actions { display: flex; flex-wrap: wrap; gap: 8px; }
-.save-note { font: 8px var(--mono); color: var(--muted); }
-.modal { width: min(520px, calc(100% - 32px)); max-height: calc(100dvh - 32px); overflow-y: auto; border: 1px solid var(--line); border-radius: 16px; padding: 28px; color: var(--text); background: var(--panel); box-shadow: 0 18px 90px #0002; }
-.modal[open] { animation: dialog-enter .2s ease-out; }
-.modal::backdrop { background: #11171370; backdrop-filter: blur(5px); }
-.modal__top { display: flex; justify-content: space-between; align-items: center; }
-.modal-close { display: grid; place-items: center; min-width: 32px; height: 32px; border: 0; border-radius: 50%; background: var(--panel-2); color: var(--muted); font-size: 22px; cursor: pointer; transition: color .2s, transform .2s; }
-.modal-close:hover { color: var(--text); transform: rotate(90deg); }
-.modal h2 { margin: 16px 0; font-size: 28px; font-weight: 500; letter-spacing: -.04em; }
-.modal p, .modal li { color: var(--muted); font-size: 13px; line-height: 1.8; }
-.modal ol { padding-left: 20px; }
-.modal__note { border-left: 2px solid var(--accent); padding: 12px; background: var(--accent-soft); font-size: 11px !important; }
-.quiz-progress { color: var(--muted); font: 10px var(--mono); }
-.quiz-options { display: grid; gap: 8px; }
-.quiz-options button, .archive-file { padding: 14px; border: 1px solid var(--line); border-radius: 8px; background: var(--panel-2); color: var(--text); text-align: left; cursor: pointer; transition: border-color .2s; }
-.quiz-options button:hover, .archive-file:hover { border-color: var(--accent); }
-.quiz-result { color: var(--accent) !important; font-size: 20px !important; }
-.radio-panel { display: flex; flex-wrap: wrap; gap: 16px; padding: 18px; border: 1px solid var(--line); border-radius: 10px; }
-.radio-panel__intro { display: flex; align-items: center; gap: 10px; }
-.radio-panel__intro strong { display: block; margin-top: 4px; font-size: 13px; }
-.radio-panel__signal { font-size: 10px; }
-.radio-controls { display: flex; align-items: center; gap: 8px; min-width: 0; }
-
-/* VM-16 inspired industrial keypad reconstruction. The surrounding PWA stays modern;
-   this module is deliberately warmer and more tactile, like a replaced laboratory unit. */
-.keypad-panel {
-  position: relative;
-  padding: 22px;
-  border: 1px solid color-mix(in srgb, var(--wire) 42%, var(--line));
-  border-radius: 9px;
-  background:
-    linear-gradient(135deg, rgba(255,255,255,.16), transparent 38%),
-    linear-gradient(180deg, #d8d9d0 0%, #c3c7bd 100%);
-  color: #20251f;
-  box-shadow: inset 0 1px rgba(255,255,255,.55), inset 0 -2px rgba(44,52,45,.14), 0 10px 24px rgba(12,18,14,.12);
-}
-:root[data-theme="dark"] .keypad-panel {
-  background: linear-gradient(135deg, rgba(255,255,255,.12), transparent 38%), linear-gradient(180deg, #777d72, #5e675e);
-  color: #182019;
-}
-.keypad-panel::before, .keypad-panel::after { content: ""; position: absolute; width: 6px; height: 6px; border-radius: 50%; background: #6f776d; box-shadow: inset 0 1px 1px rgba(255,255,255,.55); }
-.keypad-panel::before { top: 9px; left: 10px; }
-.keypad-panel::after { top: 9px; right: 10px; }
-.keypad-panel .panel-heading { align-items: center; margin-bottom: 15px; padding: 0 4px 13px; border-bottom: 1px solid rgba(44,52,45,.26); }
-.keypad-panel .panel-kicker { color: #566156; }
-.keypad-panel h2 { color: #202820; font: 700 17px/1.2 var(--mono); letter-spacing: -.04em; }
-:root[data-theme="dark"] .keypad-panel h2 { color: #1b241d; }
-.keypad-panel .lamp { position: relative; width: 19px; height: 19px; margin: 0 3px 0 0; border: 3px solid #596257; border-radius: 50%; background: radial-gradient(circle at 35% 30%, #d9ded5 0 12%, #8f988b 35%, #646c62 72%); box-shadow: inset 0 1px 2px rgba(255,255,255,.55), 0 1px 1px rgba(0,0,0,.28); transition: box-shadow .18s, background .18s, border-color .18s; }
-.keypad-panel .lamp::after { content: "HL1"; position: absolute; top: 24px; right: -2px; color: #566156; font: 7px var(--mono); }
-.keypad-panel .lamp.is-success { border-color: #5d624d; background: radial-gradient(circle at 34% 28%, #fff7c5 0 10%, #e6bb54 34%, #9b6b1c 74%); box-shadow: 0 0 0 2px rgba(175,139,48,.18), 0 0 14px rgba(226,183,72,.7), inset 0 1px 2px #fff7c5; }
-.keypad-panel .lamp.is-error { border-color: #67453e; background: radial-gradient(circle at 34% 28%, #ffd0bd 0 10%, #d96c4d 36%, #813a2d 74%); box-shadow: 0 0 0 2px rgba(190,70,46,.16), 0 0 11px rgba(211,86,60,.5); }
-.keypad-panel .code-display { margin: 0 4px 16px; padding: 10px 11px; border: 1px solid rgba(49,57,49,.3); border-radius: 3px; background: rgba(46,57,47,.12); box-shadow: inset 0 2px 4px rgba(24,31,25,.12); }
-.keypad-panel .code-display > span { color: #566156; font-size: 8px; }
-.keypad-panel .code-display strong { color: #283129; font-size: 21px; letter-spacing: .2em; text-shadow: 0 1px rgba(255,255,255,.42); }
-.keypad-panel .code-display button { color: #4f5c50; border: 1px solid rgba(49,57,49,.26); border-radius: 3px; background: rgba(255,255,255,.18); line-height: 1; }
-.keypad-panel .code-display button:hover { color: #273128; background: rgba(255,255,255,.36); }
-.keypad { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 9px; padding: 12px; border: 1px solid rgba(49,57,49,.3); border-radius: 4px; background: #aab1a6; box-shadow: inset 0 2px 5px rgba(35,43,36,.2), 0 1px rgba(255,255,255,.45); }
-.keypad button { position: relative; height: 48px; border: 1px solid #697267; border-radius: 4px; background: linear-gradient(180deg, #e5e6df 0%, #c8ccc3 65%, #b1b7ad 100%); color: #273128; font: 700 16px var(--mono); text-shadow: 0 1px rgba(255,255,255,.7); box-shadow: 0 3px 0 #727a70, 0 5px 7px rgba(38,46,39,.2), inset 0 1px rgba(255,255,255,.8); transition: transform .09s ease, box-shadow .09s ease, filter .15s ease, border-color .15s ease; }
-.keypad button::after { content: ""; position: absolute; inset: 3px; border: 1px solid rgba(255,255,255,.28); border-radius: 2px; pointer-events: none; }
-.keypad button:hover:not(:disabled) { border-color: #4d6a9b; filter: saturate(1.05) brightness(1.03); color: #233d78; background: linear-gradient(180deg, #eef0ea, #cbd2d0); }
-.keypad button:active:not(:disabled) { transform: translateY(3px); box-shadow: 0 0 0 #727a70, 0 1px 2px rgba(38,46,39,.25), inset 0 2px 4px rgba(38,46,39,.18); }
-.keypad button:focus-visible { outline-color: #3659d9; outline-offset: 3px; }
-.keypad .keypad__disabled { visibility: visible; opacity: .42; color: #657064; background: #b7bdb4; box-shadow: 0 2px 0 #858d82; cursor: default; }
-.keypad .keypad__disabled::after { content: "·"; border: 0; inset: auto; }
-.keypad .keypad__reset { color: #4e5b50; background: linear-gradient(180deg, #cfd3ca, #aeb5ab); font-size: 9px; letter-spacing: .04em; }
-.keyboard-tip { margin: 13px 4px 0; color: #566156; font: 9px var(--mono); }
-@media (max-width: 480px) {
-  .keypad-panel { padding: 17px 14px; }
-  .keypad { gap: 7px; padding: 9px; }
-  .keypad button { height: 45px; }
-}
-.radio-controls select { min-width: 0; padding: 10px; border: 1px solid var(--line); border-radius: 8px; background: var(--panel); color: var(--text); font-size: 11px; }
-.radio-play { width: 40px; padding-inline: 0; }
-.radio-volume { display: flex; align-items: center; gap: 10px; color: var(--muted); }
-.radio-volume input { width: 100%; min-width: 0; accent-color: var(--accent); }
-.radio-volume output { font: 10px var(--mono); }
-.radio-stations { display: flex; gap: 8px; order: 4; width: 100%; overflow-x: auto; padding: 3px; scrollbar-color: var(--wire) transparent; }
-.radio-station-card { flex: 0 0 60px; padding: 4px; border: 1px solid var(--line); border-radius: 7px; background: var(--panel); cursor: pointer; }
-.radio-station-card:hover, .radio-station-card.is-selected { border-color: var(--accent); }
-.radio-station-card img { width: 50px; height: 50px; border-radius: 4px; object-fit: cover; }
-.radio-station-card span { display: block; max-width: 50px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--muted); font-size: 8px; }
-.radio-credit { color: var(--muted); font-size: 10px; }
-.minesweeper-heading { display: flex; align-items: center; justify-content: space-between; gap: 14px; }
-.minesweeper-status { display: flex; justify-content: space-between; gap: 10px; padding: 12px 0; color: var(--muted); font: 10px var(--mono); }
-.minesweeper-board { display: grid; grid-template-columns: repeat(9, 1fr); gap: 3px; }
-.mine-cell { aspect-ratio: 1; border: 1px solid var(--line); border-radius: 4px; background: var(--panel-2); color: var(--text); font: 12px var(--mono); cursor: pointer; }
-.mine-cell:hover { border-color: var(--accent); }
-.mine-cell.is-open { border-color: transparent; background: var(--panel); }
-.mine-cell.is-mine { background: var(--danger-soft); color: var(--danger); }
-.mine-cell.is-flagged, .mine-cell.number-1 { color: var(--accent); }
-.mine-cell.number-2 { color: var(--supply); }
-.mine-cell.number-3, .mine-cell.number-4 { color: var(--danger); }
-.minesweeper-tip { font-size: 10px !important; margin: 14px 0 6px; }
-.minesweeper-flag-toggle { display: block; }
-.archive-files { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin-block: 20px; }
-.archive-file span, .archive-file small { display: block; font: 9px var(--mono); color: var(--muted); }
-.archive-file strong { display: block; margin: 12px 0; font-size: 12px; }
-.archive-file.is-open { border-color: var(--accent); }
-.archive-reader { padding: 16px; background: var(--panel-2); border-radius: 8px; color: var(--muted); font-size: 12px; }
-.archive-reader p { margin: 0; }
-.archive-reader button { margin-top: 10px; }
-.signal-test__top { display: flex; justify-content: space-between; gap: 10px; font: 9px var(--mono); margin-bottom: 12px; }
-.signal-lamps { display: grid; grid-template-columns: repeat(5, 1fr); gap: 8px; }
-.signal-lamp { aspect-ratio: 1; border: 1px solid var(--line); border-radius: 50%; background: var(--panel); cursor: pointer; }
-.signal-lamp.is-target { background: var(--accent); border-color: var(--accent); }
-@keyframes dialog-enter { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
-@media (max-width: 1000px) { .workbench-column { padding: 24px; } }
-@media (max-width: 800px) { .workspace { grid-template-columns: 1fr; } .workbench-column + .workbench-column { border-left: 0; border-top: 1px solid var(--line); } }
-@media (max-width: 480px) { .workspace { border-radius: 12px; } .workbench-column { padding: 20px; gap: 26px; } .panel h2 { font-size: 18px; } .modal { padding: 20px; } .archive-files { grid-template-columns: 1fr; } .minesweeper-heading { flex-wrap: wrap; } }

+ 0 - 24
ПМ.03.02/2.2.500 Ерошко/css/weather.css

@@ -1,24 +0,0 @@
-.topbar-actions .weather-button { display: inline-flex; align-items: center; white-space: nowrap; }
-.weather-modal { background: var(--panel); }
-.weather-modal .weather-location, .weather-modal #weatherStatus { color: var(--muted); font-size: 11px; }
-.weather-current { display: flex; align-items: center; gap: 20px; margin: 24px 0 16px; }
-.weather-icon { font-size: 52px; color: var(--accent); }
-.weather-current strong { font: 700 52px var(--mono); }
-.weather-current p, .weather-modal .weather-details { color: var(--muted); font-size: 12px; margin: 6px 0; }
-.weather-forecast { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; margin: 22px 0; }
-.weather-forecast > div { display: grid; gap: 12px; padding: 14px 6px; text-align: center; border: 1px solid var(--line); border-radius: 12px; font-size: 11px; }
-.weather-forecast > div > span:nth-child(2) { font-size: 26px; color: var(--accent); }
-.weather-forecast strong { font: 700 13px var(--mono); }
-.weather-footer { display: flex; justify-content: space-between; gap: 12px; border-top: 1px solid var(--line); padding-top: 16px; }
-.weather-footer a { color: var(--muted); font-size: 11px; }
-.weather-footer button { color: var(--accent); }
-.weather-footer button:disabled { opacity: .5; cursor: wait; }
-@media (max-width: 600px) {
-  .weather-modal { padding: 20px; }
-}
-
-.weather-button { gap: 6px; }
-@media (max-width: 800px) {
-  .topbar-actions .weather-button { display: inline-flex; justify-content: center; width: 36px; min-height: 36px; padding: 0; border: 0; border-radius: 50%; color: var(--muted); }
-  .weather-label { display: none; }
-}

+ 0 - 37
ПМ.03.02/2.2.500 Ерошко/docs/INTERACTION.md

@@ -1,37 +0,0 @@
-# Логика стенда ЗКЭ-4-10
-
-Стенд использует дискретную учебную модель тиристорного кодового устройства. Схема 114.2.082.002 ПС полностью перерисована в SVG. Все символы, подписи и проводники создаются кодом; фотографий и растровых подложек внутри схемы нет. Номиналы компонентов и вторичные напряжения не восстановлены; электрический расчёт не выполняется. Исходный снимок хранится только как материал разработки. Отдельный векторный чертёж доступен по ссылке «SVG ↓».
-
-## Состояния
-
-1. `idle`, пустой ввод: VD5–VD8 закрыты, YA1 обесточен.
-2. Каждая следующая верная цифра добавляет одну проводящую ступень VD5 → VD6 → VD7 → VD8. Путь импульса подсвечивается на 650 мс. Проводящие тиристоры и карточки ступеней остаются выделенными.
-3. Полный код вызывает `openLock()`: подсвечиваются составной ключ VT5 / VT2 и нагрузка, включаются HL1 и анимация движения ригеля. Открытое состояние сохраняется без ограничения времени; таймер автоматического закрытия отсутствует.
-4. «Закрыть замок», «Сбросить» и `СБР` вызывают `reset()` и закрывают замок вручную. Кнопки сброса доступны и в открытом состоянии.
-5. Неверная цифра сбрасывает весь ввод и показывает `error` на 700 мс. Ввод на это время заблокирован. Ошибка не включает YA1.
-6. S2 непосредственно замыкает цепь YA1 / HL1, шунтируя составной ключ. В модели первое нажатие удерживает S2, повторное отпускает. Таймер C1 не ограничивает этот режим; ступени памяти и VT5 / VT2 не включаются.
-
-`activeCode` хранится в `localStorage` (`zke-code`). Допускаются четыре разные цифры: одна физическая клавиша оригинала не может быть одновременно соединена с несколькими кодовыми входами. Старая некорректная настройка заменяется в рабочей модели на 1234. В этой версии скрытый архивный код больше не считается правильным префиксом кода замка.
-
-Сброс памяти выполняется целиком: поштучного удаления уже открытых тиристорных ступеней нет. Настройка кода и клавиатура заблокированы во время импульса открытия. Любой сброс отменяет таймеры ошибки и подсветки. Таймера закрытия нет; импульс ввода по-прежнему гаснет через 650 мс.
-
-## Чтение схемы
-
-- `js/schematic.js` строит 49 векторных компонентов и 47 электрических цепей. 143 вывода явно перечислены в сети соединений; проверено, что каждый вывод принадлежит одной цепи. `js/circuit.js` добавляет выбор деталей и визуальные состояния. Кнопки узлов и справочник дают альтернативный доступ к описаниям.
-- Выбранный узел показывает назначение деталей, путь сигнала и текущее состояние модели.
-- Масштаб: 75–200%, с горизонтальной прокруткой. «Развернуть» переносит ту же панель в нативный `dialog`, сохраняя состояние и обработчики. Escape возвращает панель на место.
-- «Следующая цифра» подаёт очередную цифру настроенного кода. Этот учебный инструмент не существует в оригинальном устройстве.
-- Во время просмотра развёрнутой схемы цифровая клавиатура продолжает вводить код. В других модальных окнах и полях ввода перехвата цифр нет.
-- Графика механизма показывает катушку YA1, якорь с ригелем, ответную планку и возвратную пружину. Это пояснение принципа движения, а не механический чертёж изделия.
-
-Звук, погода, тема, тест, игры и радио остаются самостоятельными функциями. При `prefers-reduced-motion` движения и бегущая подсветка отключаются. Основная лаборатория работает из локального `index.html`; сетевые функции требуют подключения.
-
-## Источники
-
-- Снимок пользователя: `assets/circuit-original.png`.
-- Статья, указанная пользователем: https://pikabu.ru/story/naberite_kod_istoriya_i_ustroystvo_samogo_massovogo_kodovogo_zamka_sssr_14324774
-- Публикация автора: https://habr.com/ru/companies/timeweb/articles/1079122/
-
-## Электрические узлы
-
-VT5 и VT2 — составной транзистор: коллекторы соединены, эмиттер VT5 управляет базой VT2. R9 / R11 задают управление VT5, R10 связан с базой VT2. После полного кода этот ключ подаёт ток на YA1. R12 заряжает C1; каскад VT3 / VT4 после выдержки через R18 изменяет режим VT1, снимая удержание тиристорной цепочки. Некодовые клавиши связаны с цепью R19 / C3; VD1–VD4 участвуют в развязке сигналов. Общая шина соответствует положительному выводу выпрямителей, полярность C1 / C2 сохранена по исходнику.

+ 0 - 1101
ПМ.03.02/2.2.500 Ерошко/docs/circuit-netlist.json

@@ -1,1101 +0,0 @@
-{
-  "pins": {
-    "S1": {
-      "C": [
-        245,
-        790
-      ],
-      "K1": [
-        330,
-        385
-      ],
-      "K2": [
-        330,
-        425
-      ],
-      "K3": [
-        330,
-        465
-      ],
-      "K4": [
-        330,
-        505
-      ],
-      "K5": [
-        330,
-        545
-      ],
-      "K6": [
-        330,
-        585
-      ],
-      "K7": [
-        330,
-        625
-      ],
-      "K8": [
-        330,
-        665
-      ],
-      "K9": [
-        330,
-        705
-      ],
-      "K10": [
-        330,
-        745
-      ]
-    },
-    "XP/XS": {
-      "C": [
-        376,
-        830
-      ],
-      "I1": [
-        400,
-        385
-      ],
-      "O1": [
-        435,
-        385
-      ],
-      "I2": [
-        400,
-        425
-      ],
-      "O2": [
-        435,
-        425
-      ],
-      "I3": [
-        400,
-        465
-      ],
-      "O3": [
-        435,
-        465
-      ],
-      "I4": [
-        400,
-        505
-      ],
-      "O4": [
-        435,
-        505
-      ],
-      "I5": [
-        400,
-        545
-      ],
-      "O5": [
-        435,
-        545
-      ],
-      "I6": [
-        400,
-        585
-      ],
-      "O6": [
-        435,
-        585
-      ],
-      "I7": [
-        400,
-        625
-      ],
-      "O7": [
-        435,
-        625
-      ],
-      "I8": [
-        400,
-        665
-      ],
-      "O8": [
-        435,
-        665
-      ],
-      "I9": [
-        400,
-        705
-      ],
-      "O9": [
-        435,
-        705
-      ],
-      "I10": [
-        400,
-        745
-      ],
-      "O10": [
-        435,
-        745
-      ]
-    },
-    "R1": {
-      "A": [
-        450,
-        385
-      ],
-      "B": [
-        500,
-        385
-      ]
-    },
-    "R3": {
-      "A": [
-        450,
-        425
-      ],
-      "B": [
-        500,
-        425
-      ]
-    },
-    "R5": {
-      "A": [
-        450,
-        465
-      ],
-      "B": [
-        500,
-        465
-      ]
-    },
-    "R7": {
-      "A": [
-        450,
-        505
-      ],
-      "B": [
-        500,
-        505
-      ]
-    },
-    "R2": {
-      "A": [
-        575,
-        475
-      ],
-      "B": [
-        625,
-        475
-      ]
-    },
-    "R4": {
-      "A": [
-        575,
-        545
-      ],
-      "B": [
-        625,
-        545
-      ]
-    },
-    "R6": {
-      "A": [
-        575,
-        615
-      ],
-      "B": [
-        625,
-        615
-      ]
-    },
-    "R8": {
-      "A": [
-        729,
-        340
-      ],
-      "B": [
-        729,
-        390
-      ]
-    },
-    "R9": {
-      "A": [
-        660,
-        685
-      ],
-      "B": [
-        660,
-        735
-      ]
-    },
-    "R11": {
-      "A": [
-        660,
-        765
-      ],
-      "B": [
-        660,
-        815
-      ]
-    },
-    "R10": {
-      "A": [
-        730,
-        765
-      ],
-      "B": [
-        730,
-        815
-      ]
-    },
-    "R12": {
-      "A": [
-        835,
-        685
-      ],
-      "B": [
-        835,
-        735
-      ]
-    },
-    "R13": {
-      "A": [
-        875,
-        640
-      ],
-      "B": [
-        875,
-        695
-      ]
-    },
-    "R14": {
-      "A": [
-        875,
-        765
-      ],
-      "B": [
-        875,
-        815
-      ]
-    },
-    "R15": {
-      "A": [
-        915,
-        580
-      ],
-      "B": [
-        915,
-        635
-      ]
-    },
-    "R16": {
-      "A": [
-        945,
-        660
-      ],
-      "B": [
-        945,
-        695
-      ]
-    },
-    "R17": {
-      "A": [
-        955,
-        765
-      ],
-      "B": [
-        955,
-        815
-      ]
-    },
-    "R18": {
-      "A": [
-        1020,
-        580
-      ],
-      "B": [
-        1020,
-        630
-      ]
-    },
-    "R19": {
-      "A": [
-        935,
-        855
-      ],
-      "B": [
-        985,
-        855
-      ]
-    },
-    "R20": {
-      "A": [
-        920,
-        340
-      ],
-      "B": [
-        920,
-        390
-      ]
-    },
-    "C1": {
-      "A": [
-        835,
-        745
-      ],
-      "B": [
-        835,
-        805
-      ]
-    },
-    "C2": {
-      "A": [
-        1070,
-        710
-      ],
-      "B": [
-        1070,
-        790
-      ]
-    },
-    "C3": {
-      "A": [
-        945,
-        640
-      ],
-      "B": [
-        985,
-        640
-      ]
-    },
-    "C4": {
-      "A": [
-        620,
-        493
-      ],
-      "B": [
-        660,
-        493
-      ]
-    },
-    "C5": {
-      "A": [
-        620,
-        565
-      ],
-      "B": [
-        660,
-        565
-      ]
-    },
-    "C6": {
-      "A": [
-        620,
-        637
-      ],
-      "B": [
-        660,
-        637
-      ]
-    },
-    "VD1": {
-      "K": [
-        555,
-        610
-      ],
-      "A": [
-        555,
-        665
-      ]
-    },
-    "VD2": {
-      "A": [
-        580,
-        585
-      ],
-      "K": [
-        625,
-        585
-      ]
-    },
-    "VD3": {
-      "A": [
-        565,
-        680
-      ],
-      "K": [
-        565,
-        735
-      ]
-    },
-    "VD4": {
-      "A": [
-        580,
-        655
-      ],
-      "K": [
-        625,
-        655
-      ]
-    },
-    "VD5": {
-      "K": [
-        660,
-        440
-      ],
-      "A": [
-        660,
-        475
-      ],
-      "G": [
-        638,
-        445
-      ]
-    },
-    "VD6": {
-      "K": [
-        660,
-        510
-      ],
-      "A": [
-        660,
-        545
-      ],
-      "G": [
-        638,
-        515
-      ]
-    },
-    "VD7": {
-      "K": [
-        660,
-        580
-      ],
-      "A": [
-        660,
-        615
-      ],
-      "G": [
-        638,
-        585
-      ]
-    },
-    "VD8": {
-      "K": [
-        660,
-        650
-      ],
-      "A": [
-        660,
-        685
-      ],
-      "G": [
-        638,
-        655
-      ]
-    },
-    "VD9": {
-      "A": [
-        885,
-        375
-      ],
-      "K": [
-        885,
-        415
-      ]
-    },
-    "VT1": {
-      "B": [
-        700,
-        390
-      ],
-      "E": [
-        660,
-        355
-      ],
-      "C": [
-        660,
-        425
-      ]
-    },
-    "VT5": {
-      "B": [
-        675,
-        720
-      ],
-      "C": [
-        710,
-        685
-      ],
-      "E": [
-        710,
-        755
-      ]
-    },
-    "VT2": {
-      "B": [
-        755,
-        720
-      ],
-      "C": [
-        790,
-        685
-      ],
-      "E": [
-        790,
-        755
-      ]
-    },
-    "VT3": {
-      "B": [
-        875,
-        710
-      ],
-      "C": [
-        910,
-        675
-      ],
-      "E": [
-        910,
-        745
-      ]
-    },
-    "VT4": {
-      "B": [
-        985,
-        710
-      ],
-      "C": [
-        1020,
-        675
-      ],
-      "E": [
-        1020,
-        745
-      ]
-    },
-    "YA1": {
-      "A": [
-        847,
-        375
-      ],
-      "B": [
-        847,
-        415
-      ]
-    },
-    "HL1": {
-      "A": [
-        920,
-        435
-      ],
-      "B": [
-        920,
-        465
-      ]
-    },
-    "S2": {
-      "A": [
-        980,
-        535
-      ],
-      "B": [
-        1025,
-        535
-      ]
-    },
-    "VD10": {
-      "MINUS": [
-        1160,
-        532
-      ],
-      "PLUS": [
-        1160,
-        588
-      ],
-      "AC1": [
-        1132,
-        560
-      ],
-      "AC2": [
-        1188,
-        560
-      ]
-    },
-    "VD11": {
-      "MINUS": [
-        1160,
-        642
-      ],
-      "PLUS": [
-        1160,
-        698
-      ],
-      "AC1": [
-        1132,
-        670
-      ],
-      "AC2": [
-        1188,
-        670
-      ]
-    },
-    "T1": {
-      "S5": [
-        1230,
-        525
-      ],
-      "S6": [
-        1230,
-        605
-      ],
-      "S4": [
-        1230,
-        650
-      ],
-      "S3": [
-        1230,
-        730
-      ],
-      "P1": [
-        1287,
-        560
-      ],
-      "P2": [
-        1287,
-        700
-      ]
-    },
-    "FU1": {
-      "A": [
-        1320,
-        560
-      ],
-      "B": [
-        1380,
-        560
-      ]
-    }
-  },
-  "nets": [
-    {
-      "id": "key-0",
-      "connections": [
-        "S1.K1",
-        "XP/XS.I1"
-      ],
-      "d": "M330 385H400"
-    },
-    {
-      "id": "key-1",
-      "connections": [
-        "S1.K2",
-        "XP/XS.I2"
-      ],
-      "d": "M330 425H400"
-    },
-    {
-      "id": "key-2",
-      "connections": [
-        "S1.K3",
-        "XP/XS.I3"
-      ],
-      "d": "M330 465H400"
-    },
-    {
-      "id": "key-3",
-      "connections": [
-        "S1.K4",
-        "XP/XS.I4"
-      ],
-      "d": "M330 505H400"
-    },
-    {
-      "id": "key-4",
-      "connections": [
-        "S1.K5",
-        "XP/XS.I5"
-      ],
-      "d": "M330 545H400"
-    },
-    {
-      "id": "key-5",
-      "connections": [
-        "S1.K6",
-        "XP/XS.I6"
-      ],
-      "d": "M330 585H400"
-    },
-    {
-      "id": "key-6",
-      "connections": [
-        "S1.K7",
-        "XP/XS.I7"
-      ],
-      "d": "M330 625H400"
-    },
-    {
-      "id": "key-7",
-      "connections": [
-        "S1.K8",
-        "XP/XS.I8"
-      ],
-      "d": "M330 665H400"
-    },
-    {
-      "id": "key-8",
-      "connections": [
-        "S1.K9",
-        "XP/XS.I9"
-      ],
-      "d": "M330 705H400"
-    },
-    {
-      "id": "key-9",
-      "connections": [
-        "S1.K10",
-        "XP/XS.I10"
-      ],
-      "d": "M330 745H400"
-    },
-    {
-      "id": "plug-0",
-      "connections": [
-        "XP/XS.O1",
-        "R1.A"
-      ],
-      "d": "M435 385H450"
-    },
-    {
-      "id": "plug-1",
-      "connections": [
-        "XP/XS.O2",
-        "R3.A"
-      ],
-      "d": "M435 425H450"
-    },
-    {
-      "id": "plug-2",
-      "connections": [
-        "XP/XS.O3",
-        "R5.A"
-      ],
-      "d": "M435 465H450"
-    },
-    {
-      "id": "plug-3",
-      "connections": [
-        "XP/XS.O4",
-        "R7.A"
-      ],
-      "d": "M435 505H450"
-    },
-    {
-      "id": "common",
-      "connections": [
-        "S1.C",
-        "XP/XS.C",
-        "R2.A",
-        "R4.A",
-        "R6.A",
-        "R11.B",
-        "R10.B",
-        "VT2.E",
-        "R14.B",
-        "C1.B",
-        "R17.B",
-        "C2.B",
-        "VD10.PLUS",
-        "VD11.PLUS",
-        "S2.B"
-      ],
-      "d": "M245 790H360V830H1160 M376 830V810 M540 475V830 M540 475H575 M540 545H575 M540 615H575 M660 815V830 M730 815V830 M790 755V830 M835 805V830 M875 815V830 M955 815V830 M1070 790V830 M1160 588V610H1110V830 M1160 698V830 M1025 535H1090V830"
-    },
-    {
-      "id": "stage-1-gate",
-      "connections": [
-        "R1.B",
-        "VD5.G"
-      ],
-      "d": "M500 385H542V445H638"
-    },
-    {
-      "id": "stage-2-gate",
-      "connections": [
-        "R3.B",
-        "VD6.G",
-        "C4.A"
-      ],
-      "d": "M500 425H525V515H638 M620 493V515"
-    },
-    {
-      "id": "stage-3-input",
-      "connections": [
-        "R5.B",
-        "VD2.A",
-        "VD1.K"
-      ],
-      "d": "M500 465H508V585H580 M555 585V610"
-    },
-    {
-      "id": "stage-3-gate",
-      "connections": [
-        "VD2.K",
-        "VD7.G",
-        "C5.A"
-      ],
-      "d": "M625 585H638 M620 565V585H625"
-    },
-    {
-      "id": "stage-4-input",
-      "connections": [
-        "R7.B",
-        "VD4.A",
-        "VD3.A"
-      ],
-      "d": "M500 505H492V655H580 M565 655V680"
-    },
-    {
-      "id": "stage-4-gate",
-      "connections": [
-        "VD4.K",
-        "VD8.G",
-        "C6.A"
-      ],
-      "d": "M625 655H638 M620 637V655H625"
-    },
-    {
-      "id": "memory-1",
-      "connections": [
-        "VD5.A",
-        "VD6.K",
-        "R2.B",
-        "C4.B"
-      ],
-      "d": "M660 475V510 M625 475H660"
-    },
-    {
-      "id": "memory-2",
-      "connections": [
-        "VD6.A",
-        "VD7.K",
-        "R4.B",
-        "C5.B"
-      ],
-      "d": "M660 545V580 M625 545H660"
-    },
-    {
-      "id": "memory-3",
-      "connections": [
-        "VD7.A",
-        "VD8.K",
-        "R6.B",
-        "C6.B"
-      ],
-      "d": "M660 615V650 M625 615H660"
-    },
-    {
-      "id": "chain-supply",
-      "connections": [
-        "VT1.C",
-        "VD5.K"
-      ],
-      "d": "M660 425V440"
-    },
-    {
-      "id": "full-code",
-      "connections": [
-        "VD8.A",
-        "R9.A",
-        "R12.A"
-      ],
-      "d": "M660 685H675V665H835V685"
-    },
-    {
-      "id": "key-base",
-      "connections": [
-        "R9.B",
-        "R11.A",
-        "VT5.B"
-      ],
-      "d": "M660 735V765 M660 750H675V720"
-    },
-    {
-      "id": "darlington",
-      "connections": [
-        "VT5.E",
-        "VT2.B",
-        "R10.A"
-      ],
-      "d": "M710 755H755V720 M730 755V765"
-    },
-    {
-      "id": "load-return",
-      "connections": [
-        "VT5.C",
-        "VT2.C",
-        "YA1.B",
-        "HL1.B",
-        "S2.A",
-        "VD9.K"
-      ],
-      "d": "M710 685V690H790V535H980 M790 535V685 M847 415V535 M920 465V535 M885 415H847"
-    },
-    {
-      "id": "load-negative",
-      "connections": [
-        "YA1.A",
-        "R20.A",
-        "VD9.A",
-        "VD10.MINUS"
-      ],
-      "d": "M847 375V310H1160V532 M920 310V340 M885 375H847"
-    },
-    {
-      "id": "lamp",
-      "connections": [
-        "R20.B",
-        "HL1.A"
-      ],
-      "d": "M920 390V435"
-    },
-    {
-      "id": "logic-negative",
-      "connections": [
-        "VT1.E",
-        "R8.A",
-        "R13.A",
-        "R15.A",
-        "C2.A",
-        "VD11.MINUS"
-      ],
-      "d": "M660 355V328H805V550H1070V710 M729 328V340 M875 550V640 M915 550V580 M1050 550V642H1160"
-    },
-    {
-      "id": "vt1-base",
-      "connections": [
-        "VT1.B",
-        "R8.B",
-        "R18.A"
-      ],
-      "d": "M700 390H729V580H1020"
-    },
-    {
-      "id": "timer-input",
-      "connections": [
-        "R12.B",
-        "C1.A",
-        "R13.B",
-        "R14.A",
-        "VT3.B"
-      ],
-      "d": "M835 735V745H875 M875 695V765 M875 710H875"
-    },
-    {
-      "id": "timer-emitter",
-      "connections": [
-        "VT3.E",
-        "VT4.E",
-        "R17.A"
-      ],
-      "d": "M910 745V755H1020V745 M955 755V765"
-    },
-    {
-      "id": "vt3-collector",
-      "connections": [
-        "VT3.C",
-        "R15.B",
-        "R16.A",
-        "C3.A"
-      ],
-      "d": "M915 635V640H945V660 M910 675V640H915"
-    },
-    {
-      "id": "vt4-base",
-      "connections": [
-        "VT4.B",
-        "R16.B"
-      ],
-      "d": "M945 695V710H985"
-    },
-    {
-      "id": "vt4-collector",
-      "connections": [
-        "VT4.C",
-        "R18.B"
-      ],
-      "d": "M1020 630V675"
-    },
-    {
-      "id": "feedback",
-      "connections": [
-        "R19.B",
-        "C3.B"
-      ],
-      "d": "M985 855H995V640H985"
-    },
-    {
-      "id": "error-bus",
-      "connections": [
-        "XP/XS.O5",
-        "XP/XS.O6",
-        "XP/XS.O7",
-        "XP/XS.O8",
-        "XP/XS.O9",
-        "XP/XS.O10",
-        "VD1.A",
-        "VD3.K",
-        "R19.A"
-      ],
-      "d": "M435 545H450V855H935 M435 585H450 M435 625H450 M435 665H450 M435 705H450 M435 745H450 M555 665V855 M565 735V855"
-    },
-    {
-      "id": "ac-load-5",
-      "connections": [
-        "VD10.AC1",
-        "T1.S5"
-      ],
-      "d": "M1230 525H1115V560H1132"
-    },
-    {
-      "id": "ac-load-6",
-      "connections": [
-        "VD10.AC2",
-        "T1.S6"
-      ],
-      "d": "M1230 605H1205V560H1188"
-    },
-    {
-      "id": "ac-logic-4",
-      "connections": [
-        "VD11.AC2",
-        "T1.S4"
-      ],
-      "d": "M1230 650H1205V670H1188"
-    },
-    {
-      "id": "ac-logic-3",
-      "connections": [
-        "VD11.AC1",
-        "T1.S3"
-      ],
-      "d": "M1230 730H1115V670H1132"
-    },
-    {
-      "id": "mains-fuse",
-      "connections": [
-        "T1.P1",
-        "FU1.A"
-      ],
-      "d": "M1287 560H1320"
-    },
-    {
-      "id": "mains-19",
-      "connections": [
-        "FU1.B"
-      ],
-      "d": "M1380 560H1410 M1404 555l6 5l-6 5"
-    },
-    {
-      "id": "mains-18",
-      "connections": [
-        "T1.P2"
-      ],
-      "d": "M1287 700H1410 M1404 695l6 5l-6 5"
-    }
-  ]
-}

BIN
ПМ.03.02/2.2.500 Ерошко/docs/circuit-preview.png


+ 0 - 75
ПМ.03.02/2.2.500 Ерошко/docs/issues.md

@@ -1,75 +0,0 @@
-# Issues / входящие запросы
-
-Ниже — примеры запросов, которые могли поступить от пользователей проекта. Это не фактическая переписка, а демонстрационный бэклог для развития ЗКЭ-4-10 LAB.
-
-## 1. Показать правильный код после обучения
-
-> «Я прошёл симулятор, но не понял, где посмотреть демонстрационный код. Добавь в справку подсказку, которая раскрывается только по нажатию.»
-
-**Ожидание:** в справке появляется необязательная скрытая подсказка с текущим кодом.
-
-## 2. История попыток
-
-> «Хочу на уроке видеть не только последнее событие, но и последние пять попыток: код, результат и время. Так проще разбирать ошибки группы.»
-
-**Ожидание:** отдельный журнал попыток с очисткой и сохранением на время сессии.
-
-## 3. Режим экзамена
-
-> «Сделай режим преподавателя: код не должен быть виден, а после трёх ошибок замок должен блокировать клавиатуру на 15 секунд.»
-
-**Ожидание:** переключатель режима, таймер блокировки и понятное сообщение о причине.
-
-## 4. Улучшить маленькие экраны
-
-> «На телефоне всё читается, но схема выглядит слишком компактной. Хочу, чтобы каскады можно было листать или раскрывать подробнее.»
-
-**Ожидание:** мобильная версия схемы без мелкого текста и потери логики.
-
-## 5. Доступность без мыши
-
-> «Я работаю только с клавиатуры. Проверь, что можно переключиться на все кнопки Tab-ом, изменить код и пройти тест без курсора.»
-
-**Ожидание:** корректный фокус, видимый индикатор фокуса и навигация во всех диалогах.
-
-## 6. Объяснить каскады проще
-
-> «Слова “тиристор” и “каскад” понятны не всем. Добавь короткое человеческое объяснение рядом со схемой — без технической воды.»
-
-**Ожидание:** всплывающая подсказка или простой режим текста для новичков.
-
-## 7. Реалистичный звук механизма
-
-> «Клик клавиш есть, но хочется, чтобы открытие ощущалось как срабатывание тяжёлого электромагнита и защёлки.»
-
-**Ожидание:** ненавязчивая звуковая последовательность при успешном вводе с возможностью выключить звук.
-
-## 8. Экспорт результатов теста
-
-> «После мини-теста мне нужно показать результат преподавателю. Можно сделать кнопку “Скопировать результат” без регистрации и сервера?»
-
-**Ожидание:** копирование краткого текстового результата в буфер обмена.
-
-## 9. Светлая тема для аудитории
-
-> «В тёмной аудитории интерфейс выглядит отлично, но днём на проекторе хочется более светлый режим. Добавь переключатель, но сохрани характерный стиль.»
-
-**Ожидание:** контрастная светлая тема с теми же акцентами и без резкого белого фона.
-
-## 10. Кнопка отмены последней цифры
-
-> «Иногда нажал не ту цифру случайно. Сделай кнопку, которая удаляет последний шаг, пока замок ещё не открылся.»
-
-**Ожидание:** действие `⌫` убирает последний активный каскад и обновляет экран ввода.
-
-## 11. Настраиваемое время открытия
-
-> «Для разных демонстраций хочу менять длительность открытия замка: например, 1,5 секунды или 5 секунд.»
-
-**Ожидание:** настройка времени с ограничениями и отображением обратного отсчёта.
-
-## 12. Офлайн-демонстрация
-
-> «Нужно показывать стенд в кабинете без интернета. Проверь, что при отсутствии сети не ломается основной интерфейс и все кнопки работают.»
-
-**Ожидание:** отсутствие внешних критических зависимостей; допустима замена веб-шрифта системным.

+ 3 - 12
ПМ.03.02/2.2.500 Ерошко/index.html

@@ -4,16 +4,11 @@
     <meta charset="UTF-8" />
     <meta name="viewport" content="width=device-width, initial-scale=1.0" />
     <meta name="theme-color" content="#f4f4ef" />
-    <script src="js/theme.js?v=minimal-1"></script>
     <title>ЗКЭ-4-10 · Лаборатория кодового замка</title>
     <link rel="preconnect" href="https://fonts.googleapis.com" />
     <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
     <link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&family=Space+Mono:wght@400;700&display=swap" rel="stylesheet" />
-    <link rel="stylesheet" href="css/base.css?v=minimal-1" />
-    <link rel="stylesheet" href="css/components.css?v=minimal-1" />
-    <link rel="stylesheet" href="css/weather.css?v=minimal-1" />
-    <link rel="stylesheet" href="css/arcade.css?v=minimal-1" />
-    <link rel="stylesheet" href="css/circuit.css?v=vector-3" />
+    <link rel="stylesheet" href="style.css?v=consolidated-1" />
   </head>
   <body>
     <main class="app-shell" id="top">
@@ -86,7 +81,7 @@
           <div class="panel-heading"><div><p class="panel-kicker">03 / принципиальная схема</p><h2 id="schemeHeading">Внутри ЗКЭ-4-10</h2></div><div class="scheme-heading-actions"><button class="text-button" id="expandCircuit">↗ Развернуть</button><button class="text-button" id="collapseCircuit">Свернуть ×</button><button class="text-button" id="resetButton">Сбросить</button></div></div>
           <p class="scheme-intro">Отдельные детали вместо условных блоков. Наберите код и выберите элемент схемы — его назначение и путь сигнала появятся ниже.</p>
           <div class="circuit-status"><span class="circuit-status__dot"></span><span id="circuitStatus" role="status">Ожидание кода</span></div>
-          <div class="diagram-toolbar"><div class="diagram-format"><span>SVG</span>Принципиальная схема · 49 элементов</div><div id="zoomTools" class="zoom-tools"><button id="zoomOut" aria-label="Уменьшить схему">−</button><button id="zoomFit" aria-label="Сбросить масштаб"><span id="zoomValue">100%</span></button><button id="zoomIn" aria-label="Увеличить схему">+</button><a class="svg-download" href="assets/zke-4-10-circuit.svg" download>SVG ↓</a></div></div>
+          <div class="diagram-toolbar"><div class="diagram-format"><span>SVG</span>Принципиальная схема · 49 элементов</div><div id="zoomTools" class="zoom-tools"><button id="zoomOut" aria-label="Уменьшить схему">−</button><button id="zoomFit" aria-label="Сбросить масштаб"><span id="zoomValue">100%</span></button><button id="zoomIn" aria-label="Увеличить схему">+</button><button class="svg-download" id="downloadSvg" type="button">SVG ↓</button></div></div>
           <div class="schematic-scroll" tabindex="0" role="region" aria-label="Векторная принципиальная схема. Доступны горизонтальная прокрутка и увеличение."><div id="diagramHost"></div></div>
           <aside class="component-tooltip" id="componentTooltip" role="status" aria-live="polite" hidden><button type="button" class="component-tooltip__close" aria-label="Закрыть подсказку">×</button><span class="component-tooltip__label">ЭЛЕМЕНТ СХЕМЫ</span><strong id="tooltipTitle"></strong><p id="tooltipText"></p></aside>
           <div class="diagram-caption"><span id="keypadSignal" role="status">S1 · ожидание клавиши</span><span>Нажмите на обозначение детали · ↔ прокрутка</span></div>
@@ -166,9 +161,6 @@
       <div id="weatherContent" hidden></div>
       <div class="weather-footer"><a href="https://open-meteo.com/" target="_blank" rel="noopener">Данные Open-Meteo</a><button class="text-button" id="weatherRefresh">Обновить</button></div>
     </dialog>
-    <script src="js/schematic.js?v=vector-3"></script>
-    <script src="js/circuit.js?v=manual-close-4"></script>
-    <script src="js/app.js?v=manual-close-4"></script>
     <dialog class="modal arcade-modal" id="arcadeModal" aria-labelledby="arcadeTitle">
       <div class="arcade-heading"><h2 id="arcadeTitle">Прыжки</h2><button id="arcadeClose" class="modal-close" aria-label="Закрыть игру">×</button></div>
       <div class="arcade-hud"><span id="arcadeScore">Счёт: 0</span><span id="arcadeBest">Рекорд: 0</span></div>
@@ -178,7 +170,6 @@
       <div class="arcade-touch" aria-label="Управление игрой"><button data-control="left" aria-label="Влево">←</button><button data-control="up" aria-label="Вверх">↑</button><button data-control="down" aria-label="Вниз">↓</button><button data-control="right" aria-label="Вправо">→</button><button data-control="shoot" aria-label="Стрелять">Огонь</button></div>
       <p class="arcade-help" id="arcadeHelp"></p>
     </dialog>
-    <script src="js/arcade.js?v=minimal-1"></script>
-    <script src="js/weather.js?v=minimal-1"></script>
+    <script src="app.js?v=consolidated-1"></script>
   </body>
 </html>

+ 0 - 236
ПМ.03.02/2.2.500 Ерошко/js/app.js

@@ -1,236 +0,0 @@
-/* app.js — interaction layer: code validation, visual state, sound, dialogs and local persistence. */
-const $ = (selector) => document.querySelector(selector);
-let activeCode = localStorage.getItem('zke-code') || '1234';
-if (!/^\d{4}$/.test(activeCode) || new Set(activeCode).size !== 4) activeCode = '1234';
-let exitOverride = false;
-let entered = [];
-let faultId;
-let inputPulseId;
-let circuitState = 'idle';
-let soundEnabled = localStorage.getItem('zke-sound') === 'true';
-const quiz = [
-  ['Что происходит при неверной цифре?', ['Цепь продолжает работу', 'Цепь сбрасывается', 'Дверь открывается'], 1],
-  ['Какой элемент физически убирает защёлку?', ['Электромагнит', 'Тиристор', 'Клавиша'], 0],
-  ['Что подаёт питание на электромагнит?', ['Транзисторный выходной каскад', 'Микроконтроллер DD1', 'Резистор R1'], 0],
-  ['Какую функцию выполняют VD5–VD8?', ['Хранят последовательность цифр', 'Выпрямляют сетевое напряжение', 'Подсвечивают лампу'], 0],
-  ['Какой элемент формирует выдержку после открытия?', ['C1', 'FU1', 'S1'], 0],
-  ['Для чего нужен VD9?', ['Для подавления выброса самоиндукции YA1', 'Для выбора кода', 'Для усиления звука'], 0],
-  ['Что происходит при нажатии S2?', ['Нагрузка включается напрямую', 'Код стирается навсегда', 'Схема переходит в режим настройки'], 0],
-  ['Какую роль играет HL1?', ['Сигнализирует об открытии замка', 'Запоминает первую цифру', 'Понижает напряжение'], 0],
-  ['Что соединяет наборное поле XP/XS?', ['Выбранные клавиши с каскадами кода', 'Сеть с трансформатором', 'Лампу с корпусом'], 0],
-  ['Как закрывается замок в учебном стенде?', ['Кнопкой «Закрыть замок» или сбросом', 'Автоматически через 1 секунду', 'Только после выключения браузера'], 0]
-];
-
-function tone(type = 'click') { if (!soundEnabled || !window.AudioContext) return; const ctx = new AudioContext(); const osc = ctx.createOscillator(); const gain = ctx.createGain(); osc.connect(gain); gain.connect(ctx.destination); const now = ctx.currentTime; osc.type = type === 'error' ? 'sawtooth' : 'sine'; osc.frequency.setValueAtTime(type === 'success' ? 780 : type === 'error' ? 140 : 350, now); if (type === 'success') osc.frequency.exponentialRampToValueAtTime(1180, now + .12); gain.gain.setValueAtTime(.06, now); gain.gain.exponentialRampToValueAtTime(.001, now + (type === 'error' ? .3 : .12)); osc.start(now); osc.stop(now + .35); }
-function setLog(text) { $('#eventLog').textContent = text; }
-const attemptHistory = [];
-const attemptLogList = $('#attemptLogList');
-function recordAttempt(code, result) {
-  attemptHistory.unshift({ code, result, time: new Date() });
-  attemptHistory.length = Math.min(attemptHistory.length, 5);
-  renderAttemptHistory();
-}
-function renderAttemptHistory() {
-  $('#clearAttemptLog').disabled = attemptHistory.length === 0;
-  if (!attemptHistory.length) {
-    attemptLogList.innerHTML = '<li class="attempt-log__empty">Попыток пока нет.</li>';
-    return;
-  }
-  attemptLogList.innerHTML = attemptHistory.map(({code, result, time}) => `<li class="attempt-log__item attempt-log__item--${result === 'Успех' ? 'success' : 'error'}"><span class="attempt-log__code">${code.split('').join(' ')}</span><span class="attempt-log__result">${result}</span><time datetime="${time.toISOString()}">${time.toLocaleTimeString('ru-RU', {hour:'2-digit', minute:'2-digit', second:'2-digit'})}</time></li>`).join('');
-}
-$('#clearAttemptLog').onclick = () => { attemptHistory.length = 0; renderAttemptHistory(); };
-function updateCircuit(state = circuitState) {
-  circuitState = state;
-  const powered = state === 'open';
-  window.lockDiagram.render(state, entered, activeCode, exitOverride);
-  $('#closeLockButton').disabled = !powered;
-  $('#finishPulse').disabled = !powered;
-  $('#lockReadout').textContent = powered ? (exitOverride ? 'S2 УДЕРЖИВАЕТСЯ' : 'ДО ЗАКРЫТИЯ КНОПКОЙ') : 'ОЖИДАНИЕ КОДА';
-  ['#backspaceButton', '#codeInput', '#applyCode', '#nextStep'].forEach(selector => {
-    $(selector).disabled = powered || state === 'error';
-  });
-  $('#resetButton').disabled = false;
-  $('#keypadReset').disabled = false;
-  $('#exitButton').disabled = state === 'error' || (powered && !exitOverride);
-  $('#exitButton').textContent = exitOverride ? 'S2 · Отпустить кнопку' : 'S2 · Выход изнутри';
-  document.querySelectorAll('[data-key]').forEach(b => b.disabled = powered || state === 'error');
-  document.querySelectorAll('[data-stage-digit]').forEach(p => p.textContent = activeCode[Number(p.dataset.stageDigit)]);
-  $('#driverReading').textContent = `${entered.length} / 4`;
-  $('#relayReading').textContent = powered ? (exitOverride ? 'S2 нажата' : 'Вручную') : 'Нет';
-  $('#coilReading').textContent = powered ? 'Включён' : 'Выключен';
-  $('#circuitStatus').textContent = powered
-    ? (exitOverride ? 'S2 удерживается · YA1 включён напрямую' : 'Код принят · замок открыт до закрытия кнопкой')
-    : state === 'error' ? 'Неверная цифра · тиристорная память сброшена'
-    : entered.length ? `Проводят ${entered.length} из 4 тиристоров · ожидание следующей цифры`
-    : 'Ожидание кода · VD5–VD8 закрыты';
-  $('#logicText').textContent = powered
-    ? (exitOverride ? 'S2 замкнула цепь нагрузки в обход составного ключа VT5 / VT2. YA1 включён, HL1 горит. Нажмите S2 повторно, чтобы отпустить кнопку и закрыть замок.' : 'VD5–VD8 проводят. Составной ключ VT5 / VT2 включил YA1; HL1 горит. Замок и схема остаются в открытом состоянии до нажатия «Закрыть замок» или «Сбросить».')
-    : state === 'error' ? 'Сработала цепь сброса R19 / C3 и управляющий каскад: ток удержания цепочки прерван, VD5–VD8 закрыты. Начните набор сначала.'
-    : entered.length ? `Цифра ${entered[entered.length-1]} принята: VD${entered.length+4} проводит. Ток удержания сохраняет состояние. ${entered.length < 4 ? `Следующая ступень — VD${entered.length+5}.` : ''} Электромагнит пока выключен.`
-    : 'VD5–VD8 закрыты. Выпрямители питают схему, но импульс открытия отсутствует. YA1 обесточен; пружина удерживает защёлку.';
-}
-function clearInputPulse() {
-  clearTimeout(inputPulseId);
-  inputPulseId = undefined;
-  window.lockDiagram.clearPulse();
-}
-function pulseInput(key) {
-  clearInputPulse();
-  window.lockDiagram.pulse(key);
-  inputPulseId = setTimeout(clearInputPulse, 650);
-}
-function updateScreen() {
-  $('#codeDisplay').textContent = [...entered, ...Array(4 - entered.length).fill('—')].join(' ');
-  $('#progressNumber').textContent = `${entered.length}/4`;
-  updateCircuit();
-}
-function reset(message = 'Система сброшена. Ожидание нового ввода.') {
-  exitOverride = false;
-  clearTimeout(faultId);
-  faultId = undefined;
-  clearInputPulse();
-  entered = [];
-  circuitState = 'idle';
-  $('#lockStage').classList.remove('is-open');
-  $('#lockPill').classList.remove('is-open');
-  $('#lockPill').textContent = 'ЗАКРЫТ';
-  $('#lamp').className = 'lamp';
-  $('#powerDot').classList.remove('is-on');
-  updateScreen();
-  setLog(message);
-}
-function openLock(fromExit = false) {
-  exitOverride = fromExit;
-  updateCircuit('open');
-  $('#lockStage').classList.add('is-open');
-  $('#lockPill').classList.add('is-open');
-  $('#lockPill').textContent = 'ОТКРЫТ';
-  $('#lamp').className = 'lamp is-success';
-  $('#powerDot').classList.add('is-on');
-  setLog(fromExit ? 'S2 удерживается. Прямое питание YA1; нажмите S2 повторно, чтобы отпустить.' : 'Код принят. VT5 / VT2 включили YA1. Замок открыт до нажатия «Закрыть замок» или «Сбросить».');
-  if (!fromExit) recordAttempt(entered.join(''), 'Успех');
-  tone('success');
-}
-function incorrect(key) {
-  recordAttempt(entered.join('') + key, 'Ошибка');
-  entered = [];
-  circuitState = 'error';
-  updateScreen();
-  $('#lamp').className = 'lamp is-error';
-  setLog('Неверная цифра — ввод сброшен. Тиристорная память сброшена, YA1 выключен.');
-  tone('error');
-  faultId = setTimeout(() => reset(), 700);
-}
-function input(key) {
-  if (circuitState === 'open' || circuitState === 'error' || entered.length >= 4) return;
-  tone();
-  pulseInput(key);
-  const candidate = entered.join('') + key;
-  if (!activeCode.startsWith(candidate)) return incorrect(key);
-  entered.push(key);
-  updateScreen();
-  if (candidate === activeCode) return openLock();
-  setLog(`Введено ${entered.length} из 4 цифр. Очередной тиристор проводит; набор ещё не завершён.`);
-}
-function applyCode() {
-  const value = $('#codeInput').value.trim();
-  if (!/^\d{4}$/.test(value) || new Set(value).size !== 4) {
-    $('#codeHint').textContent = 'Нужны четыре разные цифры: у каждой клавиши оригинала один провод к наборному полю.';
-    $('#codeHint').style.color = 'var(--danger)'; return;
-  }
-  activeCode = value; localStorage.setItem('zke-code', value);
-  $('#codeInput').value = value;
-  $('#codeHint').textContent = 'Соединения XP/XS обновлены в модели. Код сохранён в этом браузере.';
-  $('#codeHint').style.color = ''; $('#saveNote').textContent = 'сохранено сейчас';
-  reset('Наборное поле перенастроено. Можно вводить новый код.');
-}
-function renderQuiz(index = 0, score = 0) { const host = $('#quizContent'); if (index === quiz.length) { host.innerHTML = `<h2>Готово</h2><p class="quiz-result">${score} из ${quiz.length} — ${score === quiz.length ? 'идеально!' : 'ещё один заход?'}</p><button class="button button--primary" id="restartQuiz">Пройти ещё раз</button>`; $('#restartQuiz').onclick = () => renderQuiz(); return; } const [question, options, answer] = quiz[index]; host.innerHTML = `<p class="quiz-progress">ВОПРОС ${index + 1} / ${quiz.length}</p><h2>${question}</h2><div class="quiz-options">${options.map((option, i) => `<button data-answer="${i}">${option}</button>`).join('')}</div>`; host.querySelectorAll('button').forEach(button => button.onclick = () => renderQuiz(index + 1, score + (+button.dataset.answer === answer))); }
-
-$('#codeInput').value = activeCode; updateScreen();
-$('#keypad').addEventListener('click', e => { const key = e.target.dataset.key; if (key) input(key); });
-$('#closeLockButton').onclick = () => reset('Замок закрыт кнопкой. YA1 выключен; пружина вернула защёлку.');
-$('#finishPulse').onclick = $('#closeLockButton').onclick;
-$('#nextStep').onclick = () => input(activeCode[entered.length]);
-$('#exitButton').onclick = () => { if (exitOverride) return reset('S2 отпущена. YA1 выключен; пружина вернула защёлку.'); clearInputPulse(); entered = []; updateScreen(); openLock(true); };
-$('#keypadReset').onclick = () => reset(); $('#resetButton').onclick = () => reset(); $('#backspaceButton').onclick = () => reset('Тиристорная память сброшена целиком. Начните с первой цифры.');
-$('#applyCode').onclick = applyCode; $('#codeInput').addEventListener('keydown', e => { if (e.key === 'Enter') applyCode(); });
-document.addEventListener('keydown', e => {
-  if (document.querySelector('dialog[open]:not(#circuitModal)') || e.target.closest('input, select, textarea, [contenteditable="true"]') || e.ctrlKey || e.altKey || e.metaKey) return;
-  if (/^[0-9]$/.test(e.key)) { e.preventDefault(); input(e.key); }
-});
-const extrasModal = $('#extrasModal');
-extrasModal.addEventListener('close', () => $('#extrasButton').setAttribute('aria-expanded', 'false'));
-extrasModal.addEventListener('click', event => {
-  const bounds = extrasModal.getBoundingClientRect();
-  if (event.target === extrasModal && (event.clientX < bounds.left || event.clientX > bounds.right || event.clientY < bounds.top || event.clientY > bounds.bottom)) extrasModal.close();
-});
-
-function updateSoundButton() {
-  $('#soundButton').classList.toggle('is-active', soundEnabled);
-  $('#soundButton').setAttribute('aria-pressed', String(soundEnabled));
-  $('#soundButton').setAttribute('aria-label', soundEnabled ? 'Выключить звук' : 'Включить звук');
-}
-updateSoundButton();
-$('#soundButton').onclick = () => {
-  soundEnabled = !soundEnabled;
-  localStorage.setItem('zke-sound', soundEnabled);
-  updateSoundButton();
-  tone();
-};
-document.querySelectorAll('[data-open-modal]').forEach(button => button.onclick = () => { const modal = $(`#${button.dataset.openModal}`); if (modal.id === 'quizModal') renderQuiz(); modal.showModal(); if (modal.id === 'extrasModal') $('#extrasButton').setAttribute('aria-expanded', 'true'); }); document.querySelectorAll('.modal-close').forEach(button => button.onclick = () => button.closest('dialog').close());
-
-// Compact minesweeper: mines are placed after the first move, so it is always safe.
-const mineBoard = $('#minesweeperBoard');
-const mineMessage = $('#minesweeperMessage');
-const mineCounter = $('#minesweeperMines');
-const mineFlagToggle = $('#minesweeperFlagToggle');
-const mineSize = 9;
-const mineTotal = 10;
-let mineCells = [];
-let mineGameOver = false;
-let mineFlagMode = false;
-const mineIndex = (row, column) => row * mineSize + column;
-function mineNeighbours(index) { const row = Math.floor(index / mineSize); const column = index % mineSize; const result = []; for (let r = row - 1; r <= row + 1; r++) for (let c = column - 1; c <= column + 1; c++) if (r >= 0 && r < mineSize && c >= 0 && c < mineSize && (r !== row || c !== column)) result.push(mineIndex(r, c)); return result; }
-function renderMineCell(index) { const cell = mineCells[index]; const button = mineBoard.children[index]; button.className = 'mine-cell'; button.textContent = ''; if (cell.open) { button.classList.add('is-open'); if (cell.mine) { button.classList.add('is-mine'); button.textContent = '✹'; } else if (cell.count) { button.classList.add(`number-${Math.min(cell.count, 4)}`); button.textContent = cell.count; } } else if (cell.flagged) { button.classList.add('is-flagged'); button.textContent = '⚑'; } }
-function updateMineCounter() { mineCounter.textContent = `МИНЫ: ${mineTotal - mineCells.filter(cell => cell.flagged).length}`; }
-function prepareMines(safeIndex) { const forbidden = new Set([safeIndex, ...mineNeighbours(safeIndex)]); const candidates = Array.from({ length: mineSize * mineSize }, (_, i) => i).filter(i => !forbidden.has(i)); for (let i = candidates.length - 1; i > 0; i--) { const swap = Math.floor(Math.random() * (i + 1)); [candidates[i], candidates[swap]] = [candidates[swap], candidates[i]]; } candidates.slice(0, mineTotal).forEach(index => mineCells[index].mine = true); mineCells.forEach((cell, index) => cell.count = mineNeighbours(index).filter(neighbour => mineCells[neighbour].mine).length); }
-function openMineCell(index) { const cell = mineCells[index]; if (mineGameOver || cell.open || cell.flagged) return; if (!mineCells.some(item => item.mine)) prepareMines(index); cell.open = true; if (cell.mine) { mineGameOver = true; mineMessage.textContent = 'Мина. Попробуйте ещё раз.'; mineCells.forEach((item, i) => { if (item.mine) { item.open = true; renderMineCell(i); } }); tone('error'); return; } renderMineCell(index); if (!cell.count) mineNeighbours(index).forEach(openMineCell); if (!mineCells.some(item => !item.mine && !item.open)) { mineGameOver = true; mineMessage.textContent = 'Поле очищено — отлично!'; tone('success'); } }
-function flagMineCell(index) { const cell = mineCells[index]; if (mineGameOver || cell.open) return; cell.flagged = !cell.flagged; renderMineCell(index); updateMineCounter(); }
-function newMinesweeper() { mineGameOver = false; mineFlagMode = false; mineCells = Array.from({ length: mineSize * mineSize }, () => ({ mine: false, open: false, flagged: false, count: 0 })); mineBoard.innerHTML = ''; mineCells.forEach((_, index) => { const button = document.createElement('button'); button.type = 'button'; button.className = 'mine-cell'; button.setAttribute('role', 'gridcell'); button.setAttribute('aria-label', `Клетка ${index + 1}`); button.onclick = () => mineFlagMode ? flagMineCell(index) : openMineCell(index); button.oncontextmenu = event => { event.preventDefault(); flagMineCell(index); }; mineBoard.append(button); }); mineMessage.textContent = 'Поле готово'; mineFlagToggle.textContent = 'Режим: открыть клетки'; mineFlagToggle.setAttribute('aria-pressed', 'false'); updateMineCounter(); }
-$('#minesweeperReset').onclick = newMinesweeper;
-mineFlagToggle.onclick = () => { mineFlagMode = !mineFlagMode; mineFlagToggle.textContent = mineFlagMode ? 'Режим: ставить флажки' : 'Режим: открыть клетки'; mineFlagToggle.setAttribute('aria-pressed', String(mineFlagMode)); };
-newMinesweeper();
-
-// Radio uses the selected public stream directly, so the volume slider is a native draggable control.
-const radioAudio = $('#radioAudio');
-const radioStation = $('#radioStation');
-const radioPlay = $('#radioPlay');
-const radioVolume = $('#radioVolume');
-const radioVolumeValue = $('#radioVolumeValue');
-const radioStations = $('#radioStations');
-const radioOptions = [...radioStation.options];
-function setRadioVolume() { radioAudio.volume = Number(radioVolume.value) / 100; radioVolumeValue.textContent = `${radioVolume.value}%`; }
-function updateRadioButton() { const isPlaying = !radioAudio.paused; radioAudio.dataset.playing = String(isPlaying); radioPlay.textContent = isPlaying ? '❚❚' : '▶'; radioPlay.setAttribute('aria-label', isPlaying ? 'Поставить радио на паузу' : 'Включить радио'); }
-function updateRadioCards() { [...radioStations.children].forEach(card => card.classList.toggle('is-selected', card.dataset.value === radioStation.value)); }
-function selectRadioStation(value) { radioStation.value = value; radioStation.dispatchEvent(new Event('change')); }
-radioOptions.forEach(option => { const card = document.createElement('button'); card.type = 'button'; card.className = 'radio-station-card'; card.dataset.value = option.value; card.title = option.textContent; card.setAttribute('aria-label', `Включить ${option.textContent}`); card.innerHTML = `<img src="https://radiobells.com/stations/${option.dataset.logo}_70.webp" alt="" loading="lazy"><span>${option.textContent}</span>`; card.onclick = () => selectRadioStation(option.value); radioStations.append(card); });
-radioAudio.src = radioStation.value;
-setRadioVolume();
-radioVolume.addEventListener('input', setRadioVolume);
-radioPlay.onclick = async () => { if (radioAudio.paused) { try { await radioAudio.play(); } catch { radioPlay.title = 'Не удалось включить этот поток. Выберите другую станцию.'; } } else radioAudio.pause(); updateRadioButton(); };
-radioStation.onchange = async () => { const shouldResume = !radioAudio.paused; radioAudio.src = radioStation.value; if (shouldResume) { try { await radioAudio.play(); } catch { /* The play button keeps a retry available. */ } } updateRadioButton(); updateRadioCards(); };
-radioAudio.onplay = updateRadioButton;
-radioAudio.onpause = updateRadioButton;
-updateRadioCards();
-
-// Hidden story sequence: code 1984 opens a short archive and reaction test.
-const archiveModal = $('#archiveModal');
-const archiveReader = $('#archiveReader');
-let archiveInterval;
-let archiveTimeout;
-function clearArchiveTest() { clearInterval(archiveInterval); clearTimeout(archiveTimeout); archiveInterval = undefined; archiveTimeout = undefined; }
-function openArchive() { reset('Обнаружен архив №1984. Доступ предоставлен на один сеанс.'); archiveModal.showModal(); }
-function showArchiveFile(index) { document.querySelectorAll('[data-archive-file]').forEach(file => file.classList.toggle('is-open', +file.dataset.archiveFile === index)); clearArchiveTest(); if (index === 0) archiveReader.innerHTML = '<p><b>ШИФР СМЕНЫ</b><br>«Если лампа горит слишком ровно — проверьте не лампу, а того, кто на неё смотрит».</p>'; if (index === 1) archiveReader.innerHTML = '<p><b>ЗАПИСКА ОПЕРАТОРА</b><br>03:14. После четвёртого сигнала дверь открылась сама. В журнале этого нет. Не обсуждать по внутренней связи.</p>'; if (index === 2) archiveReader.innerHTML = '<p><b>ПРОВЕРКА ЛИНИИ</b><br>Найдите светящуюся лампу 6 раз за 10 секунд.</p><button class="button button--primary" id="startSignalTest">Запустить проверку</button>'; const startButton = $('#startSignalTest'); if (startButton) startButton.onclick = startSignalTest; }
-function startSignalTest() { clearArchiveTest(); let score = 0; let current = -1; let finished = false; archiveReader.innerHTML = '<div class="signal-test"><div class="signal-test__top"><span id="signalTestScore">СИГНАЛЫ: 0 / 6</span><span>10.0 c</span></div><div class="signal-lamps">' + Array.from({ length: 5 }, (_, index) => `<button class="signal-lamp" data-signal-lamp="${index}" aria-label="Сигнальная лампа ${index + 1}"></button>`).join('') + '</div></div>'; const scoreLabel = $('#signalTestScore'); const lamps = [...archiveReader.querySelectorAll('[data-signal-lamp]')]; const nextLamp = () => { lamps[current]?.classList.remove('is-target'); current = Math.floor(Math.random() * lamps.length); lamps[current].classList.add('is-target'); }; lamps.forEach((lamp, index) => lamp.onclick = () => { if (finished || index !== current) return; score++; scoreLabel.textContent = `СИГНАЛЫ: ${score} / 6`; if (score === 6) { finished = true; clearArchiveTest(); archiveReader.innerHTML = '<p><b>ЛИНИЯ ЧИСТА.</b><br>Наблюдатель отключён. Запись автоматически удалится после закрытия архива.</p>'; tone('success'); } else nextLamp(); }); nextLamp(); archiveInterval = setInterval(nextLamp, 850); archiveTimeout = setTimeout(() => { if (finished) return; finished = true; clearArchiveTest(); archiveReader.innerHTML = `<p><b>ПРОВЕРКА ЗАВЕРШЕНА.</b><br>Зафиксировано сигналов: ${score} из 6. Архив остаётся доступен.</p>`; tone('error'); }, 10000); }
-document.querySelectorAll('[data-archive-file]').forEach(file => file.onclick = () => showArchiveFile(+file.dataset.archiveFile));
-archiveModal.addEventListener('close', clearArchiveTest);

+ 0 - 307
ПМ.03.02/2.2.500 Ерошко/js/arcade.js

@@ -1,307 +0,0 @@
-(() => {
-  'use strict';
-  const modal = document.querySelector('#arcadeModal');
-  const canvas = document.querySelector('#arcadeCanvas');
-  const ctx = canvas.getContext('2d');
-  const ui = name => document.querySelector('#arcade' + name);
-  const W = 360, H = 480;
-  const held = new Set(), pointers = new Map();
-  let mode, state, paused = false, over = false, raf = 0, last = 0, accumulator = 0, best = 0;
-  const random = (a,b) => a + Math.random() * (b-a);
-  const clamp = (v,a,b) => Math.max(a,Math.min(b,v));
-  function record() {
-    if (state.score > best) {
-      best = state.score;
-      try { localStorage.setItem('zke-arcade-' + mode, String(best)); } catch {}
-    }
-    ui('Score').textContent = 'Счёт: ' + state.score;
-    if (mode === 'jump') ui('Score').textContent += ' · Уровень ' + (1 + Math.floor(difficulty(state.score) * 5));
-    ui('Best').textContent = 'Рекорд: ' + best;
-  }
-  function end(message) {
-    over = true; record(); held.clear(); pointers.clear();
-    ui('Status').textContent = message + ' Счёт: ' + state.score;
-    ui('Pause').disabled = true;
-  }
-  function pause(value = !paused) {
-    if (over || !modal.open) return;
-    paused = value; held.clear(); pointers.clear();
-    ui('Pause').textContent = paused ? 'Продолжить' : 'Пауза';
-    ui('Status').textContent = paused ? 'Пауза' : 'Игра идёт';
-    last = 0; accumulator = 0; draw();
-  }
-  function restart() {
-    cancelAnimationFrame(raf); paused = false; over = false; held.clear(); pointers.clear();
-    last = 0; accumulator = 0;
-    try { best = Number(localStorage.getItem('zke-arcade-' + mode)) || 0; } catch { best = 0; }
-    ui('Pause').textContent = 'Пауза'; ui('Pause').disabled = false;
-    ui('Status').textContent = 'Игра идёт';
-    state = mode === 'snake' ? newSnake() : newJump();
-    record(); draw(); raf = requestAnimationFrame(frame); canvas.focus();
-  }
-  function open(type) {
-    mode = type;
-    ui('Title').textContent = type === 'jump' ? 'Прыжок оператора' : 'Змейка';
-    ui('Help').textContent = type === 'jump'
-      ? '← → или A/D — движение · W / Пробел — огонь · P — пауза. Края соединены. Зелёные площадки прочные, голубые движутся, рыжие ломаются, белые исчезают. Пружина ↑, ранец J, щит S.'
-      : 'Стрелки или WASD — поворот · P — пауза. Собирайте еду, избегайте стен и своего хвоста.';
-    document.querySelectorAll('[data-control]').forEach(button => {
-      button.hidden = type === 'jump' ? ['up','down'].includes(button.dataset.control) : button.dataset.control === 'shoot';
-    });
-    if (!modal.open) modal.showModal();
-    restart();
-  }
-  // All input and animation belong to the open game only.
-  document.querySelectorAll('[data-arcade]').forEach(button => button.addEventListener('click', () => open(button.dataset.arcade)));
-  ui('Restart').onclick = restart; ui('Pause').onclick = () => pause();
-  ui('Close').onclick = () => modal.close();
-  modal.addEventListener('close', () => { cancelAnimationFrame(raf); held.clear(); pointers.clear(); last = 0; });
-  window.addEventListener('blur', () => pause(true));
-  document.addEventListener('visibilitychange', () => { if (document.hidden) pause(true); });
-  const keys = { ArrowLeft:'left', ArrowRight:'right', ArrowUp:'up', ArrowDown:'down', KeyA:'left', KeyD:'right', KeyW:'up', KeyS:'down', Space:'shoot' };
-  document.addEventListener('keydown', event => {
-    if (!modal.open) return;
-    if (event.code === 'Escape') return;
-    event.stopImmediatePropagation();
-    if (event.code === 'KeyP') { event.preventDefault(); if (!event.repeat) pause(); return; }
-    const control = mode === 'jump' && event.code === 'KeyW' ? 'shoot' : keys[event.code];
-    if (!control) return;
-    event.preventDefault();
-    if (paused || over) return;
-    held.add(event.code);
-    if (mode === 'snake') turn(control);
-  }, true);
-  document.addEventListener('keyup', event => { if (modal.open) held.delete(event.code); });
-  document.querySelectorAll('[data-control]').forEach(button => {
-    button.addEventListener('pointerdown', event => {
-      if (paused || over) return;
-      event.preventDefault(); button.setPointerCapture(event.pointerId);
-      pointers.set(event.pointerId, button.dataset.control);
-      if (mode === 'snake') turn(button.dataset.control);
-    });
-    const release = event => pointers.delete(event.pointerId);
-    button.addEventListener('pointerup', release);
-    button.addEventListener('pointercancel', release);
-    button.addEventListener('lostpointercapture', release);
-  });
-  const pressed = key => [...held].some(code => (mode === 'jump' && code === 'KeyW' ? 'shoot' : keys[code]) === key) || [...pointers.values()].includes(key);
-  function frame(time) {
-    if (!modal.open) return;
-    const dt = last ? Math.min((time-last)/1000, .05) : 0; last = time;
-    if (!paused && !over) {
-      accumulator += dt;
-      while (accumulator >= 1/120 && !over) {
-        mode === 'jump' ? stepJump(1/120) : stepSnake(1/120);
-        accumulator -= 1/120;
-      }
-      record();
-    }
-    draw();
-    raf = requestAnimationFrame(frame);
-  }
-  function newSnake() {
-    const s = { body:[{x:9,y:12},{x:8,y:12},{x:7,y:12}], dir:{x:1,y:0}, queue:[], elapsed:0, score:0 };
-    food(s); return s;
-  }
-  function food(s) {
-    const free = [];
-    for(let y=0;y<24;y++) for(let x=0;x<18;x++)
-      if(!s.body.some(p => p.x===x && p.y===y)) free.push({x,y});
-    s.food = free[Math.floor(Math.random()*free.length)] || null;
-  }
-  function turn(control) {
-    const dirs = {left:{x:-1,y:0},right:{x:1,y:0},up:{x:0,y:-1},down:{x:0,y:1}};
-    const next = dirs[control], previous = state.queue.at(-1) || state.dir;
-    if (!next || state.queue.length >= 2 || (next.x===previous.x && next.y===previous.y) || (next.x===-previous.x && next.y===-previous.y)) return;
-    state.queue.push(next);
-  }
-  function stepSnake(dt) {
-    const s=state; s.elapsed+=dt;
-    const interval=Math.max(.065,.17-s.score*.003);
-    if(s.elapsed<interval) return;
-    s.elapsed-=interval;
-    if(s.queue.length) s.dir=s.queue.shift();
-    const head={x:s.body[0].x+s.dir.x,y:s.body[0].y+s.dir.y};
-    const eating=s.food && head.x===s.food.x && head.y===s.food.y;
-    const occupied=eating?s.body:s.body.slice(0,-1);
-    if(head.x<0||head.x>=18||head.y<0||head.y>=24||occupied.some(p=>p.x===head.x&&p.y===head.y)) return end('Игра окончена.');
-    s.body.unshift(head);
-    if(eating) { s.score++; food(s); if(!s.food) end('Всё поле заполнено. Победа!'); }
-    else s.body.pop();
-  }
-  // World Y grows downward; upward progress moves the camera, never the physics.
-  function newJump() {
-    const s={player:{x:170,y:390,vy:-520}, platforms:[{x:135,y:430,w:90,type:'solid',item:null}], enemies:[], shots:[], debris:[], time:0, camera:0, top:430, anchor:135, score:0, boost:0, shield:0, cooldown:0, generated:0};
-    generate(s); return s;
-  }
-  // Difficulty rises over 600 points, then stays within normal jump reach.
-  function difficulty(score) { return clamp(score / 600, 0, 1); }
-  function generate(s) {
-    const d = difficulty(s.score);
-    while(s.top>s.camera-110) {
-      s.top-=random(50+24*d,68+18*d); s.generated++;
-      s.anchor=clamp(s.anchor+random(-60-10*d,60+10*d),12,W-88);
-      const roll=Math.random();
-      // Permanent landing in every row keeps the main route reachable.
-      const platform={x:s.anchor,y:s.top,w:86-30*d,type:'solid',item:null};
-      if(s.generated>5 && roll<.12+.48*d) { platform.type='moving'; platform.base=platform.x; platform.phase=random(0,6); platform.speed=1+1.3*d; }
-      if(s.generated%8===0) platform.item=['spring','jet','shield'][Math.floor(Math.random()*3)];
-      s.platforms.push(platform);
-      if(s.generated>5 && Math.random()<.25+.4*d) s.platforms.push({x:clamp(s.anchor+(s.anchor<180?130:-130),0,300),y:s.top+25,w:56-10*d,type:Math.random()<.5?'break':'vanish',item:null});
-      const enemyInterval = Math.max(3,8-Math.floor(d*5));
-      if(s.generated>14 && s.generated%enemyInterval===0) s.enemies.push({x:s.anchor<180?280:55,y:s.top-30,base:s.anchor<180?280:55,phase:random(0,6),r:16,speed:1.5+2*d,range:18+20*d});
-    }
-  }
-  function stepJump(dt) {
-    const s=state,p=s.player;
-    s.time += dt;
-    s.debris.forEach(d => { d.life -= dt; d.x += d.vx*dt; d.y += d.vy*dt; d.vy += d.gravity*dt; d.angle += d.spin*dt; });
-    s.debris = s.debris.filter(d => d.life > 0);
-    s.boost=Math.max(0,s.boost-dt); s.shield=Math.max(0,s.shield-dt); s.cooldown-=dt;
-    const speed=240;
-    p.x+=((pressed('right')?1:0)-(pressed('left')?1:0))*speed*dt;
-    if(p.x>W) p.x=-24; if(p.x<-24) p.x=W;
-    const oldBottom=p.y+30;
-    p.vy=s.boost>0?-640:p.vy+1450*dt;
-    p.y+=p.vy*dt;
-    s.platforms.forEach(a => {
-      if(a.type==='moving') { a.phase+=dt*(a.speed||1); a.x=clamp(a.base+Math.sin(a.phase)*22,0,W-a.w); }
-    });
-    if(p.vy>0) {
-      const landing=s.platforms.filter(a=>!a.dead&&oldBottom<=a.y&&p.y+30>=a.y&&p.x+24>a.x&&p.x<a.x+a.w).sort((a,b)=>a.y-b.y)[0];
-      if(landing) {
-        if(landing.type==='break') shatter(landing);
-        else {
-          p.y=landing.y-30; p.vy=-520;
-          if(landing.type==='vanish') shatter(landing);
-          if(landing.item==='spring') p.vy=-850;
-          if(landing.item==='jet') s.boost=2.5;
-          if(landing.item==='shield') s.shield=7;
-          if(landing.item!=='spring') landing.item=null;
-        }
-      }
-    }
-    if(pressed('shoot')&&s.cooldown<=0) { s.shots.push({x:p.x+12,y:p.y}); s.cooldown=.2; }
-    s.shots.forEach(b=>b.y-=750*dt);
-    s.enemies.forEach(e=>{
-      e.phase+=dt*(e.speed||2); e.x=e.base+Math.sin(e.phase)*(e.range||22);
-      s.shots.forEach(b=>{ if(!b.dead&&!e.dead&&Math.hypot(b.x-e.x,b.y-e.y)<e.r+6) {e.dead=true;b.dead=true;} });
-      if(!e.dead&&p.x+24>e.x-e.r&&p.x<e.x+e.r&&p.y+30>e.y-e.r&&p.y<e.y+e.r) {
-        if(s.shield>0||s.boost>0) e.dead=true;
-        else if(p.vy>0&&oldBottom<=e.y) {e.dead=true;p.vy=-520;}
-        else end('Столкновение с охранным дроном.');
-      }
-    });
-    s.camera=Math.min(s.camera,p.y-H*.42);
-    s.score=Math.max(s.score,Math.floor(-s.camera/10));
-    s.platforms=s.platforms.filter(a=>!a.dead&&a.y<s.camera+H+70);
-    s.enemies=s.enemies.filter(e=>!e.dead&&e.y<s.camera+H+60);
-    s.shots=s.shots.filter(b=>!b.dead&&b.y>s.camera-30);
-    generate(s);
-    if(p.y>s.camera+H+30) end('Падение. Попробуйте ещё раз!');
-  }
-  function shatter(platform) {
-    if(platform.dead) return;
-    platform.dead=true;
-    const ghost=platform.type==='vanish';
-    for(let i=0;i<8;i++) {
-      state.debris.push({
-        x:platform.x+(i+.5)*platform.w/8,y:platform.y+5,
-        w:platform.w/8-1,h:ghost?5:10,
-        vx:(i-3.5)*random(12,24),vy:ghost?random(-65,-15):random(-120,-45),
-        gravity:ghost?30:550,angle:0,spin:random(-5,5),
-        life:ghost?.65:1.1,color:ghost?'#b2e9ea':'#bd8151',
-        highlight:ghost?'#ffffff':'#f5c68a'
-      });
-    }
-  }
-  function rounded(x,y,w,h,r,color) {
-    ctx.fillStyle=color; ctx.beginPath(); ctx.roundRect(x,y,w,h,r); ctx.fill();
-  }
-  function drawOperator(x,y,vy,cooldown) {
-    // Outlined suit, helmet visor and boots retain the 24 × 30 collision silhouette.
-    ctx.save(); ctx.translate(x+12,y+15);
-    ctx.rotate(clamp(((pressed('right')?1:0)-(pressed('left')?1:0))*.09,-.09,.09));
-    rounded(-12,-3,24,18,5,'#15262a');
-    rounded(-10,-2,20,15,4,'#e6d5a5');
-    rounded(-14,0,5,10,2,'#789a83'); rounded(9,0,5,10,2,'#789a83');
-    rounded(-9,10,7,5,2,'#405a5f'); rounded(2,10,7,5,2,'#405a5f');
-    rounded(-12,-16,24,20,8,'#15262a');
-    rounded(-10,-15,20,18,7,'#f2e7c9');
-    rounded(-8,-12,16,11,4,'#255769');
-    rounded(-6,-11,12,4,2,'#67bbcd');
-    rect(-5,-10,4,2,'#d9faff');
-    rounded(-6,4,12,6,2,'#526c64');
-    circle(-3,7,1.3,'#c9ff6b'); circle(2,7,1.3,'#ffa967');
-    rect(-1,-18,3,5,'#b8cbd1');
-    if(cooldown>.14) { circle(.5,-20,4,'#ffe98a'); circle(.5,-20,2,'#fffbea'); }
-    ctx.restore();
-  }
-  function drawDrone(x,y,time) {
-    ctx.save(); ctx.translate(x,y);
-    // Wide rotor arms, armoured shell and one red sensor distinguish enemies.
-    rect(-25,-6,50,4,'#738395');
-    [-23,23].forEach(dx=>{
-      rect(dx-2,-12,4,10,'#a0b0b9');
-      const span=8+Math.sin(time*50)*4;
-      rounded(dx-span,-13,span*2,3,1,'#c1d6df');
-    });
-    rounded(-17,-12,34,26,9,'#18222f');
-    rounded(-15,-10,30,22,8,'#718496');
-    rounded(-12,-9,24,6,3,'#afbec8');
-    rounded(-12,-3,24,11,5,'#293345');
-    circle(0,2,7,'#752a46'); circle(0,2,4.5,'#ff526e'); circle(-1,0,1.7,'#ffe4d1');
-    rect(-10,11,5,4,'#344959'); rect(5,11,5,4,'#344959');
-    rect(-11,-5,3,2,'#ffcb77'); rect(8,-5,3,2,'#ffcb77');
-    ctx.restore();
-  }
-  function rect(x,y,w,h,color) {ctx.fillStyle=color;ctx.fillRect(x,y,w,h);}
-  function circle(x,y,r,color) {ctx.fillStyle=color;ctx.beginPath();ctx.arc(x,y,r,0,Math.PI*2);ctx.fill();}
-  function draw() {
-    rect(0,0,W,H,'#0d191c');
-    ctx.strokeStyle='#1a3033';ctx.lineWidth=1;
-    const grid = mode === 'snake' ? 20 : 24;
-    const offset=mode==='jump'?((-state.camera)%grid):0;
-    for(let x=0;x<W;x+=grid){ctx.beginPath();ctx.moveTo(x,0);ctx.lineTo(x,H);ctx.stroke();}
-    for(let y=offset;y<H;y+=grid){ctx.beginPath();ctx.moveTo(0,y);ctx.lineTo(W,y);ctx.stroke();}
-    if(mode==='snake') drawSnake(); else drawJump();
-    if(paused||over) {
-      rect(0,0,W,H,'rgba(4,10,12,.72)');
-      ctx.fillStyle='#dfffbd';ctx.font='bold 24px monospace';ctx.textAlign='center';
-      ctx.fillText(paused?'ПАУЗА':'КОНЕЦ ИГРЫ',W/2,H/2-10);
-      ctx.font='14px monospace';ctx.fillText('Счёт: '+state.score,W/2,H/2+22);ctx.textAlign='left';
-    }
-  }
-  function drawSnake() {
-    const s=state;
-    if(s.food) {circle(s.food.x*20+10,s.food.y*20+10,7,'#ff965e');}
-    s.body.forEach((p,i)=>rect(p.x*20+1,p.y*20+1,18,18,i===0?'#dfff91':'#77b86c'));
-    const h=s.body[0];rect(h.x*20+5,h.y*20+5,3,3,'#13201c');rect(h.x*20+12,h.y*20+5,3,3,'#13201c');
-  }
-  function drawJump() {
-    const s=state,p=s.player,cy=s.camera;
-    s.platforms.forEach(a=>{
-      const y=a.y-cy;
-      rect(a.x,y,a.w,9,({solid:'#9ce578',moving:'#65c5e6',break:'#c68d62',vanish:'#e0e9df'})[a.type]);
-      rect(a.x+3,y+9,a.w-6,3,'#38524c');
-      if(a.type==='break') {ctx.strokeStyle='#302522';ctx.beginPath();ctx.moveTo(a.x+20,y);ctx.lineTo(a.x+29,y+5);ctx.lineTo(a.x+22,y+9);ctx.stroke();}
-      if(a.item) {rect(a.x+a.w/2-9,y-20,18,18,a.item==='spring'?'#ffe38a':a.item==='jet'?'#ff986e':'#8cdeed');ctx.fillStyle='#152322';ctx.font='bold 14px monospace';ctx.fillText(({spring:'↑',jet:'J',shield:'S'})[a.item],a.x+a.w/2-4,y-6);}
-    });
-    s.debris.forEach(d => {
-      ctx.save(); ctx.globalAlpha = Math.min(1,d.life/.35);
-      ctx.translate(d.x,d.y-cy); ctx.rotate(d.angle);
-      rect(-d.w/2,-d.h/2,d.w,d.h,d.color);
-      rect(-d.w/2,-d.h/2,d.w,2,d.highlight);
-      ctx.restore();
-    });
-    s.enemies.forEach(e=>drawDrone(e.x,e.y-cy,s.time));
-    s.shots.forEach(b=>rect(b.x-2,b.y-cy,4,12,'#fff2a1'));
-    const y=p.y-cy;
-    if(s.shield>0){ctx.strokeStyle='#8cdeed';ctx.lineWidth=2;ctx.beginPath();ctx.arc(p.x+12,y+15,25,0,Math.PI*2);ctx.stroke();}
-    if(s.boost>0){rect(p.x+5,y+28,14,18+Math.sin(performance.now()/50)*5,'#ff965e');}
-    drawOperator(p.x,y,p.vy,s.cooldown);
-    ctx.fillStyle='#95b5aa';ctx.font='11px monospace';
-    if(s.boost>0)ctx.fillText('РАНЕЦ '+s.boost.toFixed(1),10,20);
-    else if(s.shield>0)ctx.fillText('ЩИТ '+Math.ceil(s.shield),10,20);
-  }
-})();

+ 0 - 137
ПМ.03.02/2.2.500 Ерошко/js/circuit.js

@@ -1,137 +0,0 @@
-/* Native SVG reconstruction of the supplied 114.2.082.002 ПС photograph.
- * Reference designators are retained; animation is a discrete teaching model,
- * not an electrical solver. No component values are inferred from the photo. */
-(() => {
-  const host = document.querySelector('#diagramHost');
-  const schematic = buildLockSchematic();
-  host.innerHTML = schematic.svg;
-  let releaseTimer, previousState = 'idle', previousExit = false;
-
-  const groups = {
-    input: { title: 'S1 · Клавиатура и наборное поле', refs: 'S1, XP1–XP10 / XS1–XS10, R1, R3, R5, R7', text: 'Каждая цифра имеет отдельный провод. Наборное поле соединяет четыре выбранные клавиши с четырьмя ступенями кода. Резисторы ограничивают ток управляющих электродов тиристоров.', route: 'Клавиша S1 → соединитель XP/XS → R1 / R3 / R5 / R7 → управляющий электрод VD5 / VD6 / VD7 / VD8.', state: () => `В модели установлено соединение для кода ${currentCode.split('').join(' → ')}. Остальные клавиши относятся к цепи сброса.` },
-    memory: { title: 'VD5–VD8 · Четыре ступени памяти', refs: 'VD5, VD6, VD7, VD8; R2, R4, R6; C4, C5, C6', text: 'Тиристор открывается управляющим импульсом и сохраняет проводящее состояние, пока через него проходит достаточный ток. Последовательность ступеней задаёт порядок цифр. Межступенчатые RC-цепи участвуют в переходных процессах и переключении цепочки.', route: 'Первая цифра → VD5; вторая → VD6; третья → VD7; четвёртая → VD8. Полный набор меняет состояние управляющего каскада VT1.', state: () => `Проводящих ступеней в учебной модели: ${currentEntered.length} из 4. ${currentState === 'error' ? 'После ошибки вся цепочка сброшена.' : 'Синим выделены принятые ступени.'}` },
-    reset: { title: 'VD1–VD4, R19 · Цепи сброса и развязки', refs: 'VD1–VD4, R19, C3; управляющие каскады VT3 / VT4 / VT1', text: 'Некодовые клавиши объединены в отдельную шину. Через цепь обратной связи R19 и C3 воздействие поступает в управляющий каскад. Изменение режима VT1 прерывает ток удержания тиристоров, и память стирается. VD1–VD4 участвуют в развязке входных и сбросовых цепей.', route: 'Некодовая клавиша → общая шина ошибки → R19 / C3 → управляющий каскад → запирание VT1 → снятие тока удержания VD5–VD8.', state: () => currentState === 'error' ? 'Сработал сброс: ступени памяти выключены, электромагнит не включался.' : 'Ошибки нет. Цепь готова обнулить последовательность при неверном вводе.' },
-    timer: { title: 'C1, VT3, VT4 · Выдержка открытия', refs: 'C1, C3; VT1, VT3, VT4; R8, R12–R18', text: 'После полного кода через R12 начинает заряжаться C1. Напряжение на входе VT3 изменяется. После выдержки каскад VT3 / VT4 переключается и через R18 меняет смещение VT1. Ток цепочки тиристоров прекращается, составной ключ VT5 / VT2 выключается. C3 связан с цепью обратной связи через R19.', route: 'VD8 открыт → R12 → заряд C1 → VT3 / VT4 → R18 → VT1 закрыт → VD5–VD8 и силовой ключ выключены.', state: () => currentState === 'open' ? 'В стенде автоматическое закрытие отключено. Открытое состояние сохраняется до нажатия «Закрыть замок» или «Сбросить».' : 'Импульс открытия не запущен. Точные напряжения C1 по фотографии не определяются.' },
-    output: { title: 'VT5 + VT2, YA1 · Силовой ключ и защёлка', refs: 'VT5, VT2; R9–R11; YA1, VD9, HL1, R20, S2', text: 'VT5 и VT2 образуют составной транзисторный ключ: эмиттер VT5 соединён с базой VT2, а их коллекторы объединены. Он усиливает управляющий ток и замыкает цепь электромагнита. YA1 втягивает якорь; HL1 сигнализирует об открытии. VD9 ограничивает выброс самоиндукции. S2 напрямую замыкает цепь нагрузки при удержании.', route: 'Общий (+) → силовой ключ VT2 / VT5 → YA1 → отрицательный выход VD10. Ветвь R20 + HL1 включена параллельно YA1. S2 шунтирует ключ.', state: () => currentState === 'open' ? 'YA1 включён, HL1 горит, якорь втянут. Защёлка освобождена.' : 'YA1 обесточен, HL1 не горит. Пружина удерживает защёлку в закрытом положении.' },
-    power: { title: 'T1 · Два канала питания', refs: 'T1, FU1, VD10, VD11, C2', text: 'FU1 стоит во входной сетевой цепи. Трансформатор T1 понижает напряжение и отделяет низковольтные цепи от сети. Две вторичные обмотки и два моста питают нагрузку и электронику раздельно. C2 сглаживает пульсации питания электроники.', route: 'Сеть ~220 В / 50 Гц → FU1 → T1 → VD10 (электромагнит) и VD11 + C2 (схема управления).', state: () => 'Питание присутствует и при закрытом замке. Номиналы вторичных напряжений на исходном снимке не указаны.' }
-  };
-  const stageRouteAfterResistor = [
-    'M500 385H542V445H638',
-    'M500 425H525V515H638 M620 493V515',
-    'M500 465H508V585H580 M555 585V610 M580 585H625H638',
-    'M500 505H492V655H580 M565 655V680 M580 655H625H638'
-  ];
-  const codeRoute = (keyRow, stage) => {
-    const resistorRow = 385 + stage * 40;
-    const railX = 446 + stage * 8;
-    return `M330 ${keyRow}H400V${keyRow}H435H${railX}V${resistorRow}H450H500` + stageRouteAfterResistor[stage].slice(`M500 ${resistorRow}`.length);
-  };
-  const names = {
-    S1:'Десятиклавишный блок ввода', 'XP/XS':'Наборные соединители кода', S2:'Кнопка выхода изнутри', T1:'Трансформатор с двумя каналами', FU1:'Сетевой предохранитель', YA1:'Катушка электромагнита защёлки', HL1:'Сигнальная лампа открытия',
-    VT1:'Управляющий транзистор цепочки кода', VT2:'Выходной транзистор составного ключа', VT5:'Входной транзистор составного ключа', VT3:'Транзистор времязадающего каскада', VT4:'Транзистор выходного каскада',
-    VD9:'Диод подавления самоиндукции YA1', VD10:'Мост питания электромагнита', VD11:'Мост питания электроники', C1:'Времязадающий конденсатор', C2:'Сглаживающий конденсатор питания', C3:'Конденсатор связи каскадов', R8:'Резистор смещения VT1', R20:'Ограничение тока лампы HL1',
-    R1:'Резистор управляющей цепи VD5', R3:'Резистор управляющей цепи VD6', R5:'Резистор управляющей цепи VD7', R7:'Резистор управляющей цепи VD8',
-    R2:'Резистор межступенчатой цепи', R4:'Резистор межступенчатой цепи', R6:'Резистор межступенчатой цепи',
-    R9:'Резистор управления базой VT5', R10:'Резистор базы VT2', R11:'Резистор смещения базы VT5',
-    R12:'Резистор цепи C1', R13:'Резистор цепи управления VT3', R14:'Резистор цепи управления VT3',
-    R15:'Резистор каскада выдержки', R16:'Резистор каскада выдержки', R17:'Резистор общей цепи VT3 / VT4', R18:'Резистор цепи VT4', R19:'Резистор связи выходного и входного узлов',
-    C4:'Конденсатор связи первой и второй ступеней', C5:'Конденсатор связи второй и третьей ступеней', C6:'Конденсатор связи третьей и четвёртой ступеней',
-    VD1:'Диод связи цепи сброса', VD2:'Диод развязки сигналов', VD3:'Диод связи цепи сброса', VD4:'Диод развязки сигналов'
-  };
-  const groupFor = id => id === 'S1' || id === 'XP/XS' || /^R[1357]$/.test(id) ? 'input'
-    : /^VD[5-8]$/.test(id) || /^R[246]$/.test(id) || /^C[4-6]$/.test(id) ? 'memory'
-    : /^(VD[1-4]|R19)$/.test(id) ? 'reset'
-    : /^(T1|FU1|VD10|VD11|C2)$/.test(id) ? 'power'
-    : /^(VT[25]|R9|R10|R11|YA1|VD9|HL1|R20|S2)$/.test(id) ? 'output' : 'timer';
-  let selected = 'memory', selectedPart = '', currentState = 'idle', currentEntered = [], currentCode = '1234', currentExit = false;
-  function showComponentTooltip(group, id) {
-    if (!id) return;
-    const tooltip = document.querySelector('#componentTooltip');
-    const info = groups[group];
-    document.querySelector('#tooltipTitle').textContent = `${id} · ${names[id] || (/^VD[5-8]$/.test(id) ? `Тиристор ${Number(id.slice(2))-4}-й ступени` : /^R/.test(id) ? 'Резистор цепи' : /^C/.test(id) ? 'Конденсатор цепи' : info.title.split(' · ')[1])}`;
-    document.querySelector('#tooltipText').textContent = names[id] ? `${names[id]}. ${info.text}` : info.text;
-    tooltip.hidden = false;
-  }
-  function inspect(group, id = '', showHint = false) {
-    selected = group; selectedPart = id;
-    const info = groups[group];
-    document.querySelectorAll('[data-part]').forEach(p => p.classList.toggle('is-selected', id ? p.dataset.part === id : groupFor(p.dataset.part) === group));
-    if (showHint) showComponentTooltip(group, id);
-  }
-  host.addEventListener('click', e => { const p = e.target.closest('[data-part]'); if (p) inspect(groupFor(p.dataset.part), p.dataset.part, true); });
-  host.addEventListener('keydown', e => { if ((e.key === 'Enter' || e.key === ' ') && e.target.matches('[data-part]')) { e.preventDefault(); inspect(groupFor(e.target.dataset.part), e.target.dataset.part, true); } });
-  function renderNodeInfo(group) {
-    const info = groups[group];
-    document.querySelector('#detailTitle').textContent = info.title;
-    document.querySelector('#detailRefs').textContent = info.refs;
-    document.querySelector('#detailText').textContent = info.text;
-    document.querySelector('#detailRoute').textContent = info.route;
-    document.querySelector('#detailState').textContent = info.state();
-    document.querySelectorAll('[data-node]').forEach(b => b.setAttribute('aria-pressed', String(b.dataset.node === group)));
-    document.querySelector('.component-detail-disclosure').open = true;
-  }
-  document.querySelectorAll('[data-node]').forEach(b => b.onclick = () => renderNodeInfo(b.dataset.node));
-  const componentIds = ['S1','XP/XS',...Array.from({length:20},(_,i)=>'R'+(i+1)),...Array.from({length:6},(_,i)=>'C'+(i+1)),...Array.from({length:11},(_,i)=>'VD'+(i+1)),...Array.from({length:5},(_,i)=>'VT'+(i+1)),'YA1','HL1','S2','T1','FU1'];
-  document.querySelector('#componentList').innerHTML = componentIds.map(id => `<tr><th scope="row"><button type="button" data-inspect="${id}">${id}</button></th><td>${names[id] || (/^VD[5-8]$/.test(id) ? 'Тиристор памяти кода' : /^VD/.test(id) ? 'Диод развязки сигналов' : /^C/.test(id) ? 'Межступенчатый конденсатор' : groups[groupFor(id)].title.split(' · ')[1])}</td><td>${groupFor(id) === 'memory' ? 'Память' : groups[groupFor(id)].title.split(' · ')[1]}</td></tr>`).join('');
-  document.querySelector('#componentList').onclick = e => { const b = e.target.closest('[data-inspect]'); if (b) inspect(groupFor(b.dataset.inspect),b.dataset.inspect, true); };
-  document.querySelector('#componentTooltip .component-tooltip__close').onclick = () => { document.querySelector('#componentTooltip').hidden = true; };
-  document.addEventListener('keydown', e => { if (e.key === 'Escape') document.querySelector('#componentTooltip').hidden = true; });
-  let zoom = 100;
-  const setZoom = value => { zoom = Math.max(75,Math.min(200,value)); host.style.minWidth = '0'; host.style.width = `${Math.max(host.parentElement.clientWidth,1050)*zoom/100}px`; document.querySelector('#zoomValue').textContent = `${zoom}%`; document.querySelector('#zoomOut').disabled = zoom === 75; document.querySelector('#zoomIn').disabled = zoom === 200; };
-  const fitZoom = () => { const available = host.parentElement.clientWidth; const fitted = Math.floor((available / 1050) * 100); setZoom(Math.max(75, Math.min(100, fitted))); };
-  window.addEventListener('resize', fitZoom);
-  fitZoom();
-  renderNodeInfo('memory');
-  document.querySelector('#zoomIn').onclick = () => setZoom(zoom+25);
-  document.querySelector('#zoomOut').onclick = () => setZoom(zoom-25);
-  document.querySelector('#zoomFit').onclick = fitZoom;
-  const panel = document.querySelector('.circuit-panel');
-  const placeholder = document.createComment('circuit panel home'); panel.before(placeholder);
-  const modal = document.querySelector('#circuitModal');
-  document.querySelector('#expandCircuit').onclick = () => { modal.append(panel); modal.showModal(); setZoom(zoom); document.querySelector('#collapseCircuit').focus(); };
-  document.querySelector('#collapseCircuit').onclick = () => modal.close();
-  modal.addEventListener('close', () => { placeholder.after(panel); setZoom(zoom); document.querySelector('#expandCircuit').focus(); });
-  window.lockDiagram = {
-    render(state, entered, code, fromExit = false) {
-      currentState = state; currentEntered = [...entered]; currentCode = code; currentExit = fromExit;
-      const drawing = document.querySelector('#lockCircuit');
-      drawing.dataset.state = state;
-      drawing.dataset.exit = String(fromExit);
-      drawing.classList.toggle('is-held', state === 'open' && !fromExit);
-      drawing.querySelector('#exitContact').setAttribute('d',fromExit ? 'M980 535H1025' : 'M980 535L1015 519');
-      clearTimeout(releaseTimer);
-      drawing.classList.remove('is-releasing','is-flyback');
-      drawing.querySelectorAll('[data-device]').forEach(p => {
-        p.classList.toggle('is-biased', ['VT1','VT4'].includes(p.dataset.device) && state !== 'error');
-        p.classList.toggle('is-on', ['VT5','VT2'].includes(p.dataset.device) && state === 'open' && !fromExit);
-      });
-      if (previousState === 'open' && state === 'idle') {
-        drawing.classList.add('is-flyback');
-        if (!previousExit) drawing.classList.add('is-releasing');
-        releaseTimer = setTimeout(() => drawing.classList.remove('is-releasing','is-flyback'),600);
-      }
-      previousState = state; previousExit = fromExit;
-      drawing.querySelectorAll('[data-key-contact]').forEach(p => p.classList.toggle('key-active', false));
-      const keyRows = Object.fromEntries([...drawing.querySelectorAll('[data-key-contact]')].map((p, i) => [p.dataset.keyContact, 385 + i * 40]));
-      drawing.querySelector('#codeWires').innerHTML = [...code].map((digit, stage) => {
-        const resistorRow = 385 + stage * 40;
-        const railX = 446 + stage * 8;
-        return `<path class="net code-wire" data-code-stage="${stage + 1}" d="M435 ${keyRows[digit]}H${railX}V${resistorRow}H450"/>`;
-      }).join('');
-      drawing.querySelectorAll('[data-stage]').forEach(p => p.classList.toggle('is-conducting',Number(p.dataset.stage) <= entered.length));
-      drawing.querySelectorAll('.memory-net').forEach((p,i) => p.classList.toggle('is-conducting',i < entered.length));
-      document.querySelectorAll('[data-stage-card]').forEach(p => { const active = Number(p.dataset.stageCard) <= entered.length; p.classList.toggle('is-conducting',active); p.querySelector('small').textContent = active ? 'проводит' : 'закрыт'; });
-
-      inspect(selected, selectedPart);
-    },
-    pulse(key) {
-      const index = currentCode.indexOf(key);
-      document.querySelector('#keypadSignal').textContent = `S1 · нажата ${key}${index < 0 ? ' → сброс' : ` → ступень ${index+1}`}`;
-      document.querySelector('#lockCircuit').classList.add('is-receiving');
-      const keyRow = 385 + ((Number(key) + 9) % 10) * 40;
-      document.querySelector('#inputTrace').setAttribute('d', index < 0 ? `M330 ${keyRow}H400H435V855H935` : codeRoute(keyRow, index));
-      document.querySelectorAll('[data-key-contact]').forEach(p=>p.classList.toggle('key-active',p.dataset.keyContact === key));
-    },
-    clearPulse() { document.querySelector('#lockCircuit').classList.remove('is-receiving'); document.querySelectorAll('.key-active').forEach(p=>p.classList.remove('key-active')); document.querySelector('#keypadSignal').textContent = 'S1 · ожидание клавиши'; }
-  };
-})();

+ 0 - 205
ПМ.03.02/2.2.500 Ерошко/js/schematic.js

@@ -1,205 +0,0 @@
-/* Native vector reconstruction of 114.2.082.002 ПС.
- * Component terminals and net membership are explicit so the drawing can be
- * checked independently of its teaching animation. Coordinates follow the
- * supplied drawing, with extra room around labels and the transformer.
- */
-function buildLockSchematic() {
-  const pins = {}, elements = [], wires = [], nets = [], junctions = [];
-  const esc = value => String(value).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&apos;'}[c]));
-  const line = d => `<path d="${d}"/>`;
-  const text = (x,y,value,cls='',anchor='start') => `<text x="${x}" y="${y}" class="${cls}" text-anchor="${anchor}">${esc(value)}</text>`;
-  const register = (id, terminals, body, label, bounds, extra='') => {
-    pins[id] = terminals;
-    const [x,y,w,h] = bounds;
-    elements.push(`<g class="circuit-part ${extra}" data-part="${esc(id)}" tabindex="0" role="button" aria-label="${esc(id)}: показать описание"><title>${esc(id)} — назначение и состояние</title><rect class="part-hitbox" x="${x}" y="${y}" width="${w}" height="${h}" rx="5"/><g class="part-symbol">${body}</g>${label}</g>`);
-  };
-  function resistor(id,x,y,vertical=false,length=50,labelSide=1) {
-    const terminals = {A:[x,y],B:vertical?[x,y+length]:[x+length,y]};
-    const middle=length/2;
-    const body=vertical
-      ? line(`M${x} ${y}v${middle-14} M${x} ${y+middle+14}V${y+length}`)+`<rect x="${x-5}" y="${y+middle-14}" width="10" height="28"/>`
-      : line(`M${x} ${y}h${middle-14} M${x+middle+14} ${y}H${x+length}`)+`<rect x="${x+middle-14}" y="${y-5}" width="28" height="10"/>`;
-    const label=vertical ? text(x+labelSide*12,y+middle+4,id,'component-label',labelSide<0?'end':'start') : text(x+middle,y-13,id,'component-label','middle');
-    register(id,terminals,body,label,vertical?[x-10,y-4,20,length+8]:[x-4,y-10,length+8,20]);
-  }
-  function capacitor(id,x,y,vertical=false,length=40,polar=false,labelPosition=null) {
-    const middle=length/2, gap=4;
-    const body=vertical
-      ? line(`M${x} ${y}V${y+middle-gap} M${x-12} ${y+middle-gap}h24 M${x-12} ${y+middle+gap}h24 M${x} ${y+middle+gap}V${y+length}`)
-      : line(`M${x} ${y}H${x+middle-gap} M${x+middle-gap} ${y-12}v24 M${x+middle+gap} ${y-12}v24 M${x+middle+gap} ${y}H${x+length}`);
-    const [lx,ly] = labelPosition || (vertical?[x+18,y+middle-8]:[x+middle,y-19]);
-    const labels=text(lx,ly,id,'component-label',vertical?'start':'middle')+(polar?text(x-15,y+middle+19,'+','polarity','end'):'');
-    register(id,{A:[x,y],B:vertical?[x,y+length]:[x+length,y]},body,labels,vertical?[x-16,y-3,32,length+6]:[x-3,y-16,length+6,32]);
-  }
-  function diode(id,x,y,{vertical=false,up=false,length=45,labelPosition=null}={}) {
-    let body, terminals;
-    if (!vertical) {
-      const m=x+length/2;
-      body=line(`M${x} ${y}H${m-9} M${m-9} ${y-9}l18 9l-18 9Z M${m+9} ${y-11}v22 M${m+9} ${y}H${x+length}`);
-      terminals={A:[x,y],K:[x+length,y]};
-    } else {
-      const m=y+length/2;
-      body=up
-        ? line(`M${x} ${y}V${m-9} M${x-10} ${m-9}h20 M${x-9} ${m+9}l9 -18l9 18Z M${x} ${m+9}V${y+length}`)
-        : line(`M${x} ${y}V${m-9} M${x-9} ${m-9}l9 18l9 -18Z M${x-10} ${m+9}h20 M${x} ${m+9}V${y+length}`);
-      terminals=up?{K:[x,y],A:[x,y+length]}:{A:[x,y],K:[x,y+length]};
-    }
-    const [lx,ly]=labelPosition || (vertical?[x+16,y+length/2+4]:[x+length/2,y-16]);
-    register(id,terminals,body,text(lx,ly,id,'component-label',vertical?'start':'middle'),vertical?[x-14,y-3,28,length+6]:[x-3,y-14,length+6,28]);
-  }
-  function thyristor(id,y,stage) {
-    const x=660, m=y+17.5;
-    const body=line(`M${x} ${y}V${m-9} M${x-10} ${m-9}h20 M${x-9} ${m+9}l9 -18l9 18Z M${x} ${m+9}V${y+35} M638 ${y+5}h8l10 13`);
-    register(id,{K:[x,y],A:[x,y+35],G:[638,y+5]},body,text(678,m+5,id,'component-label'),[632,y-6,66,50],`memory-device" data-stage="${stage}`);
-  }
-  function transistor(id,cx,cy,{upperEmitter=false,pnp=true,labelPosition=null}={}) {
-    let body, terminals;
-    if (upperEmitter) {
-      // VT1 has its base on the right and its emitter at the top.
-      terminals={B:[cx+30,cy],E:[cx-10,cy-35],C:[cx-10,cy+35]};
-      body=`<circle cx="${cx}" cy="${cy}" r="20"/>`+line(`M${cx+10} ${cy-13}v26 M${cx+30} ${cy}H${cx+10} M${cx+10} ${cy-7}l-20 -13v-15 M${cx+10} ${cy+7}l-20 13v15`)
-        +`<path class="arrow-fill" d="M${cx-8} ${cy-19}l8 2l-4 5Z"/>`;
-    } else {
-      terminals={B:[cx-25,cy],C:[cx+10,cy-35],E:[cx+10,cy+35]};
-      body=`<circle cx="${cx}" cy="${cy}" r="20"/>`+line(`M${cx-10} ${cy-13}v26 M${cx-25} ${cy}H${cx-10} M${cx-10} ${cy-7}l20 -13v-15 M${cx-10} ${cy+7}l20 13v15`)
-        +`<path class="arrow-fill" d="${pnp?`M${cx-3} ${cy+11}l8 1l-4 5Z`:`M${cx+8} ${cy+19}l-8 -1l4 -5Z`}"/>`;
-    }
-    const [lx,ly]=labelPosition||[cx+23,cy-21];
-    register(id,terminals,body,text(lx,ly,id,'component-label'),[cx-29,cy-39,65,78],`transistor-device" data-device="${id}`);
-  }
-  function bridge(id,cx,cy) {
-    const r=28;
-    const body=line(`M${cx} ${cy-r}l${r} ${r}l-${r} ${r}l-${r} -${r}Z M${cx-9} ${cy-8}l9 16l9 -16Z M${cx-10} ${cy+9}h20`);
-    register(id,{MINUS:[cx,cy-r],PLUS:[cx,cy+r],AC1:[cx-r,cy],AC2:[cx+r,cy]},body,
-      text(cx-12,cy-r-13,id,'component-label')+text(cx+5,cy-r-2,'−','polarity')+text(cx+5,cy+r+13,'+','polarity')+text(cx-r-8,cy+4,'~','polarity','end')+text(cx+r+8,cy+4,'~','polarity'),[cx-38,cy-42,82,85]);
-  }
-  function net(id,connections,d,cls='',dots=[]) {
-    nets.push({id,connections,d});
-    wires.push(`<path class="net ${cls}" data-net="${id}" d="${d}"/>`);
-    dots.forEach(([x,y])=>junctions.push(`<circle class="junction ${cls}" cx="${x}" cy="${y}" r="2.5"/>`));
-  }
-
-  // S1 and plug field are drawn as actual contacts and connectors.
-  let keyboard='<rect class="device-outline" x="268" y="349" width="62" height="420" rx="3"/>'+text(280,370,'S1','component-label')+line('M245 385V790');
-  const keyPins={C:[245,790]}, plugPins={C:[376,830]};
-  let plugs='<rect class="device-outline" x="350" y="349" width="89" height="444" rx="3"/>';
-  for(let i=0;i<10;i++) {
-    const y=385+i*40, digit=(i+1)%10;
-    keyboard+=`<g data-key-contact="${digit}">${line(`M245 ${y}h33l13 -10 M297 ${y}H330`)}<circle cx="297" cy="${y}" r="1.8"/>${text(308,y-7,digit,'key-digit')}</g>`;
-    keyPins['K'+(i+1)]=[330,y]; plugPins['I'+(i+1)]=[400,y]; plugPins['O'+(i+1)]=[435,y];
-    plugs+=line(`M400 ${y}h7 M407 ${y-6}l6 6l-6 6 M417 ${y-6}l6 6l-6 6 M423 ${y}h12`)+text(393,y-12,`XP${i+1} / XS${i+1}`,'connector-label');
-  }
-  keyboard+=text(253,808,'13','pin-label');
-  plugs+=line('M376 810V830')+text(386,822,'13','pin-label');
-  register('S1',keyPins,keyboard,'',[235,344,100,467]);
-  register('XP/XS',plugPins,plugs,'',[343,344,99,490]);
-  [['R1',385],['R3',425],['R5',465],['R7',505]].forEach(([id,y])=>resistor(id,450,y));
-  [['R2',475],['R4',545],['R6',615]].forEach(([id,y])=>resistor(id,575,y));
-  resistor('R8',729,340,true);
-  resistor('R9',660,685,true,50,-1);
-  resistor('R11',660,765,true,50,-1);
-  resistor('R10',730,765,true,50,-1);
-  resistor('R12',835,685,true,50,-1);
-  resistor('R13',875,640,true,55,-1);
-  resistor('R14',875,765,true,50,-1);
-  resistor('R15',915,580,true,55);
-  resistor('R16',945,660,true,35,-1);
-  resistor('R17',955,765,true);
-  resistor('R18',1020,580,true);
-  resistor('R19',935,855);
-  resistor('R20',920,340,true);
-  capacitor('C1',835,745,true,60,true,[807,783]);
-  capacitor('C2',1070,710,true,80,true);
-  capacitor('C3',945,640,false,40,false,[968,616]);
-  capacitor('C4',620,493); capacitor('C5',620,565); capacitor('C6',620,637);
-  diode('VD1',555,610,{vertical:true,up:true,length:55,labelPosition:[548,647]});
-  diode('VD2',580,585,{labelPosition:[603,572]});
-  diode('VD3',565,680,{vertical:true,length:55,labelPosition:[582,717]});
-  diode('VD4',580,655,{labelPosition:[603,678]});
-  [440,510,580,650].forEach((y,i)=>thyristor('VD'+(5+i),y,i+1));
-  diode('VD9',885,375,{vertical:true,length:40,labelPosition:[901,418]});
-  transistor('VT1',670,390,{upperEmitter:true,pnp:false,labelPosition:[672,352]});
-  transistor('VT5',700,720,{labelPosition:[719,685]});
-  transistor('VT2',780,720,{labelPosition:[775,685]});
-  transistor('VT3',900,710,{labelPosition:[883,675]});
-  transistor('VT4',1010,710,{labelPosition:[1020,675]});
-  register('YA1',{A:[847,375],B:[847,415]},line('M847 375V385 M847 405V415')+'<rect x="831" y="385" width="32" height="20"/>',text(830,363,'YA1','component-label'),[825,371,44,48]);
-  register('HL1',{A:[920,435],B:[920,465]},'<circle class="lamp-bulb" cx="920" cy="450" r="15"/>'+line('M910 440l20 20 M910 460l20 -20'),text(945,454,'HL1','component-label'),[901,431,70,39]);
-  register('S2',{A:[980,535],B:[1025,535]},'<circle cx="980" cy="535" r="2.5"/><circle cx="1025" cy="535" r="2.5"/><path id="exitContact" d="M980 535L1015 519"/>'+line('M1005 508v11 M999 508h12'),text(1010,497,'S2','component-label'),[975,493,58,48]);
-  bridge('VD10',1160,560); bridge('VD11',1160,670);
-  const transformer=line('M1255 514V752 M1262 514V752 M1230 525h8q16 0 16 10t-16 10q16 0 16 10t-16 10q16 0 16 10t-16 10q16 0 16 10t-16 10h-8 M1230 650h8q16 0 16 10t-16 10q16 0 16 10t-16 10q16 0 16 10t-16 10q16 0 16 10t-16 10h-8 M1287 560h-4q-14 0 -14 10t14 10q-14 0 -14 10t14 10q-14 0 -14 10t14 10q-14 0 -14 10t14 10q-14 0 -14 10t14 10q-14 0 -14 10t14 10q-14 0 -14 10t14 10h4');
-  register('T1',{S5:[1230,525],S6:[1230,605],S4:[1230,650],S3:[1230,730],P1:[1287,560],P2:[1287,700]},transformer,text(1235,494,'T1','component-label')+text(1222,518,'5','pin-label')+text(1222,620,'6','pin-label')+text(1222,643,'4','pin-label')+text(1222,744,'3','pin-label')+text(1296,551,'1','pin-label')+text(1296,716,'2','pin-label'),[1216,489,96,273]);
-  register('FU1',{A:[1320,560],B:[1380,560]},line('M1320 560h14 M1366 560h14 M1334 560h32')+'<rect x="1334" y="555" width="32" height="10"/>',text(1350,542,'FU1','component-label','middle'),[1316,539,68,33]);
-
-  // All component terminals belong to exactly one named electrical net.
-  for(let i=0;i<10;i++)net('key-'+i,['S1.K'+(i+1),'XP/XS.I'+(i+1)],`M330 ${385+i*40}H400`,'key-net');
-  [['R1',385],['R3',425],['R5',465],['R7',505]].forEach(([id,y],i)=>nets.push({id:'plug-'+i,connections:['XP/XS.O'+(i+1),id+'.A'],d:`M435 ${y}H450`}));
-  net('common',['S1.C','XP/XS.C','R2.A','R4.A','R6.A','R11.B','R10.B','VT2.E','R14.B','C1.B','R17.B','C2.B','VD10.PLUS','VD11.PLUS','S2.B'],
-    'M245 790H360V830H1160 M376 830V810 M540 475V830 M540 475H575 M540 545H575 M540 615H575 M660 815V830 M730 815V830 M790 755V830 M835 805V830 M875 815V830 M955 815V830 M1070 790V830 M1160 588V610H1110V830 M1160 698V830 M1025 535H1090V830','common-net',[[376,830],[540,545],[540,615],[540,830],[660,830],[730,830],[790,830],[835,830],[875,830],[955,830],[1070,830],[1090,830],[1110,830]]);
-  net('stage-1-gate',['R1.B','VD5.G'],'M500 385H542V445H638','gate-net stage-route-1');
-  net('stage-2-gate',['R3.B','VD6.G','C4.A'],'M500 425H525V515H638 M620 493V515','gate-net stage-route-2',[[620,515]]);
-  net('stage-3-input',['R5.B','VD2.A','VD1.K'],'M500 465H508V585H580 M555 585V610','gate-net stage-route-3',[[555,585]]);
-  net('stage-3-gate',['VD2.K','VD7.G','C5.A'],'M625 585H638 M620 565V585H625','gate-net stage-route-3',[[625,585]]);
-  net('stage-4-input',['R7.B','VD4.A','VD3.A'],'M500 505H492V655H580 M565 655V680','gate-net stage-route-4',[[565,655]]);
-  net('stage-4-gate',['VD4.K','VD8.G','C6.A'],'M625 655H638 M620 637V655H625','gate-net stage-route-4',[[625,655]]);
-  net('memory-1',['VD5.A','VD6.K','R2.B','C4.B'],'M660 475V510 M625 475H660','memory-net',[[660,475],[660,493]]);
-  net('memory-2',['VD6.A','VD7.K','R4.B','C5.B'],'M660 545V580 M625 545H660','memory-net',[[660,545],[660,565]]);
-  net('memory-3',['VD7.A','VD8.K','R6.B','C6.B'],'M660 615V650 M625 615H660','memory-net',[[660,615],[660,637]]);
-  net('chain-supply',['VT1.C','VD5.K'],'M660 425V440','chain-supply');
-  net('full-code',['VD8.A','R9.A','R12.A'],'M660 685H675V665H835V685','full-code-net',[[660,685]]);
-  net('key-base',['R9.B','R11.A','VT5.B'],'M660 735V765 M660 750H675V720','driver-net',[[660,750]]);
-  net('darlington',['VT5.E','VT2.B','R10.A'],'M710 755H755V720 M730 755V765','driver-net',[[730,755]]);
-  net('load-return',['VT5.C','VT2.C','YA1.B','HL1.B','S2.A','VD9.K'],'M710 685V690H790V535H980 M790 535V685 M847 415V535 M920 465V535 M885 415H847','load-return-net',[[790,690],[847,535],[920,535],[847,415]]);
-  net('load-negative',['YA1.A','R20.A','VD9.A','VD10.MINUS'],'M847 375V310H1160V532 M920 310V340 M885 375H847','load-negative-net',[[920,310],[847,375]]);
-  net('lamp',['R20.B','HL1.A'],'M920 390V435','lamp-net');
-  net('logic-negative',['VT1.E','R8.A','R13.A','R15.A','C2.A','VD11.MINUS'],'M660 355V328H805V550H1070V710 M729 328V340 M875 550V640 M915 550V580 M1050 550V642H1160','logic-supply-net',[[729,328],[875,550],[915,550],[1050,550]]);
-  net('vt1-base',['VT1.B','R8.B','R18.A'],'M700 390H729V580H1020','bias-net',[[729,390]]);
-  net('timer-input',['R12.B','C1.A','R13.B','R14.A','VT3.B'],'M835 735V745H875 M875 695V765 M875 710H875','timer-net',[[835,745],[875,745],[875,710]]);
-  net('timer-emitter',['VT3.E','VT4.E','R17.A'],'M910 745V755H1020V745 M955 755V765','timer-net',[[955,755]]);
-  net('vt3-collector',['VT3.C','R15.B','R16.A','C3.A'],'M915 635V640H945V660 M910 675V640H915','timer-net',[[915,640],[945,640]]);
-  net('vt4-base',['VT4.B','R16.B'],'M945 695V710H985','timer-net');
-  net('vt4-collector',['VT4.C','R18.B'],'M1020 630V675','timer-net');
-  net('feedback',['R19.B','C3.B'],'M985 855H995V640H985','feedback-net');
-  net('error-bus',[...Array.from({length:6},(_,i)=>'XP/XS.O'+(i+5)),'VD1.A','VD3.K','R19.A'],'M435 545H450V855H935 M435 585H450 M435 625H450 M435 665H450 M435 705H450 M435 745H450 M555 665V855 M565 735V855','error-net',[[450,585],[450,625],[450,665],[450,705],[450,745],[555,855],[565,855]]);
-  net('ac-load-5',['VD10.AC1','T1.S5'],'M1230 525H1115V560H1132','ac-net');
-  net('ac-load-6',['VD10.AC2','T1.S6'],'M1230 605H1205V560H1188','ac-net');
-  net('ac-logic-4',['VD11.AC2','T1.S4'],'M1230 650H1205V670H1188','ac-net');
-  net('ac-logic-3',['VD11.AC1','T1.S3'],'M1230 730H1115V670H1132','ac-net');
-  net('mains-fuse',['T1.P1','FU1.A'],'M1287 560H1320','mains-net');
-  net('mains-19',['FU1.B'],'M1380 560H1410 M1404 555l6 5l-6 5','mains-net');
-  net('mains-18',['T1.P2'],'M1287 700H1410 M1404 695l6 5l-6 5','mains-net');
-
-  const styles=`
-    .vector-schematic{background:var(--panel-2,#f0f1ec);color:var(--text,#242823)}
-    .vector-schematic .net,.vector-schematic .part-symbol{fill:none;stroke:var(--wire,#7e897c);stroke-width:1.65;stroke-linecap:round;stroke-linejoin:round}
-    .vector-schematic text{font:12px monospace;fill:var(--text,#242823);stroke:var(--panel-2,#f0f1ec);stroke-width:3px;paint-order:stroke fill;stroke-linejoin:round}
-    .vector-schematic .component-label{font-weight:700}.vector-schematic .connector-label{font-size:9px;fill:var(--muted,#656e63)}
-    .vector-schematic .pin-label,.vector-schematic .polarity{font-size:10px}.vector-schematic .zone-label{font:10px sans-serif;letter-spacing:1px;fill:var(--muted,#656e63)}
-    .vector-schematic .part-symbol rect,.vector-schematic .part-symbol circle{fill:var(--panel-2,#f0f1ec)}
-    .vector-schematic .part-symbol .device-outline{fill:none;stroke:var(--line,#dddfd7)}
-    .vector-schematic .part-symbol .arrow-fill{fill:var(--wire,#7e897c);stroke:none}
-    .vector-schematic .part-hitbox{fill:transparent;stroke:none}.vector-schematic .junction{fill:var(--wire,#7e897c);stroke:none}
-    .vector-schematic .current-flow,.vector-schematic .signal-overlay,.vector-schematic .fault-flow,.vector-schematic .flyback-flow{fill:none;pointer-events:none}
-    .vector-schematic .current-flow{stroke:var(--accent,#3659d9);stroke-width:3;stroke-dasharray:3 12;opacity:0}
-    .vector-schematic .signal-overlay{stroke:var(--signal,#986016);stroke-width:3;opacity:0}
-    .vector-schematic .fault-flow{stroke:var(--danger,#b43f35);stroke-width:2.5;opacity:0}
-    .vector-schematic .flyback-flow{stroke:var(--signal,#986016);stroke-width:3;stroke-dasharray:2 8;opacity:0}
-  `;
-  const svg=`<svg xmlns="http://www.w3.org/2000/svg" id="lockCircuit" class="schematic vector-schematic" viewBox="210 260 1240 655" aria-labelledby="circuitTitle circuitDescription" data-state="idle"><title id="circuitTitle">Принципиальная схема ЗКЭ-4-10, 114.2.082.002 ПС</title><desc id="circuitDescription">Векторная перерисовка: S1, наборное поле XP/XS, четыре тиристора, пять транзисторов, все резисторы и конденсаторы, электромагнит, лампа, кнопка выхода, трансформатор и выпрямители. Выберите деталь для пояснения.</desc><style>${styles}</style><defs><pattern id="vectorCircuitGrid" width="16" height="16" patternUnits="userSpaceOnUse"><circle cx="1" cy="1" r=".55" fill="currentColor" opacity=".13"/></pattern></defs><rect x="210" y="260" width="1240" height="655" fill="url(#vectorCircuitGrid)"/>
-    ${text(268,285,'01 / КЛАВИАТУРА','zone-label')}${text(533,285,'02 / КОДОВАЯ ЦЕПОЧКА','zone-label')}${text(846,285,'03 / ЭЛЕКТРОМАГНИТ','zone-label')}${text(1160,285,'04 / ПИТАНИЕ','zone-label')}
-    <g class="circuit-nets">${wires.join('')}<g id="codeWires" aria-label="Перемычки набора кода"></g>${junctions.join('')}</g><g class="circuit-components">${elements.join('')}</g>
-    ${text(1380,614,'~220 В','component-label','middle')}${text(1380,635,'50 Гц','connector-label','middle')}${text(1410,547,'19','pin-label','middle')}${text(1410,720,'18','pin-label','middle')}
-    ${text(670,892,'VT5 + VT2 · СОСТАВНОЙ КЛЮЧ','zone-label')}${text(960,892,'VT3 / VT4 · ВЫДЕРЖКА','zone-label')}${text(370,878,'Общий провод (+)','pin-label')}
-    <g class="signal-overlay" aria-hidden="true"><path id="inputTrace" d="M330 385H400H435H450H500H542V445H638"/></g>
-    <g class="current-flow code-current" aria-hidden="true"><path d="M790 830V755 M790 685V535H847V415 M847 375V310H1160V532 M1160 588V610H1110V830H790"/></g>
-    <g class="current-flow exit-current" aria-hidden="true"><path d="M1090 830V535H847V415 M847 375V310H1160V532 M1160 588V610H1110V830H1090"/></g>
-    <g class="current-flow lamp-current" aria-hidden="true"><path d="M920 535V465 M920 435V390 M920 340V310"/></g>
-    <g class="fault-flow" aria-hidden="true"><path d="M450 545V855H935 M985 855H995V640H985 M729 580H1020V675"/></g>
-    <g class="flyback-flow" aria-hidden="true"><path d="M847 415V375H885V415H847"/></g>
-    </svg>`;
-  return {svg,pins,nets,partIds:Object.keys(pins),inputRoutes:[
-    'M435 385H500H542V445H638','M435 425H500H525V515H638 M620 493V515',
-    'M435 465H500H508V585H638 M620 565V585','M435 505H500H492V655H638 M620 637V655'
-  ]};
-}
-if(typeof module !== 'undefined' && module.exports) module.exports=buildLockSchematic;

+ 0 - 29
ПМ.03.02/2.2.500 Ерошко/js/theme.js

@@ -1,29 +0,0 @@
-/* Apply the palette before paint; keep the control and browser chrome in sync. */
-(() => {
-  const systemTheme = matchMedia('(prefers-color-scheme: dark)');
-  let selectedTheme;
-  try { selectedTheme = localStorage.getItem('zke-theme'); } catch { /* Use system preference. */ }
-  const isTheme = value => value === 'light' || value === 'dark';
-  function applyTheme(theme) {
-    document.documentElement.dataset.theme = theme;
-    document.querySelector('meta[name="theme-color"]').content = theme === 'dark' ? '#171a18' : '#f4f4ef';
-    const button = document.querySelector('#themeButton');
-    if (!button) return;
-    const label = theme === 'dark' ? 'Включить светлую тему' : 'Включить тёмную тему';
-    button.setAttribute('aria-label', label);
-    button.setAttribute('title', label);
-    button.setAttribute('aria-pressed', String(theme === 'dark'));
-  }
-  applyTheme(isTheme(selectedTheme) ? selectedTheme : systemTheme.matches ? 'dark' : 'light');
-  document.addEventListener('DOMContentLoaded', () => {
-    applyTheme(document.documentElement.dataset.theme);
-    document.querySelector('#themeButton').addEventListener('click', () => {
-      selectedTheme = document.documentElement.dataset.theme === 'dark' ? 'light' : 'dark';
-      applyTheme(selectedTheme);
-      try { localStorage.setItem('zke-theme', selectedTheme); } catch { /* Theme remains active for this session. */ }
-    });
-  });
-  systemTheme.addEventListener('change', event => {
-    if (!isTheme(selectedTheme)) applyTheme(event.matches ? 'dark' : 'light');
-  });
-})();

+ 0 - 93
ПМ.03.02/2.2.500 Ерошко/js/weather.js

@@ -1,93 +0,0 @@
-(() => {
-  const modal = document.querySelector('#weatherModal');
-  const status = document.querySelector('#weatherStatus');
-  const content = document.querySelector('#weatherContent');
-  const refresh = document.querySelector('#weatherRefresh');
-  let loadedAt = 0;
-  let pending = false;
-  const url = 'https://api.open-meteo.com/v1/forecast?' + new URLSearchParams({
-    latitude: '54.2093', longitude: '79.7477',
-    current: 'temperature_2m,apparent_temperature,is_day,precipitation,weather_code',
-    daily: 'temperature_2m_max,temperature_2m_min,weather_code,sunrise,sunset',
-    forecast_days: '3', timezone: 'Europe/Moscow'
-  });
-  const degrees = value => Number.isFinite(value) ? Math.round(value) + '°' : '—';
-  function sky(code, day = 1) {
-    if (code === 0) return [day ? '☀' : '☾', 'Ясно'];
-    if ([1, 2].includes(code)) return ['⛅', 'Переменная облачность'];
-    if (code === 3) return ['☁', 'Пасмурно'];
-    if ([45, 48].includes(code)) return ['≋', 'Туман'];
-    if ([51, 53, 55, 56, 57].includes(code)) return ['☂', 'Морось'];
-    if ([61, 63, 65, 66, 67, 80, 81, 82].includes(code)) return ['☂', 'Дождь'];
-    if ([71, 73, 75, 77, 85, 86].includes(code)) return ['❄', 'Снег'];
-    if ([95, 96, 99].includes(code)) return ['ϟ', 'Гроза'];
-    return ['◇', 'Нет данных'];
-  }
-  function clock(value) { return typeof value === 'string' && value.includes('T') ? value.split('T')[1].slice(0, 5) : '—'; }
-  function render(data) {
-    const current = data.current;
-    const daily = data.daily;
-    if (!current || !Number.isFinite(current.temperature_2m) || !daily || !Array.isArray(daily.time)) throw new Error('Incomplete forecast');
-    const [icon, label] = sky(current.weather_code, current.is_day);
-    const card = document.createElement('div');
-    card.className = 'weather-current';
-    const symbol = document.createElement('span');
-    symbol.className = 'weather-icon';
-    symbol.textContent = icon;
-    symbol.setAttribute('aria-hidden', 'true');
-    const summary = document.createElement('div');
-    const temperature = document.createElement('strong');
-    temperature.textContent = degrees(current.temperature_2m);
-    const description = document.createElement('p');
-    description.textContent = label + ' · ощущается как ' + degrees(current.apparent_temperature);
-    summary.append(temperature, description);
-    card.append(symbol, summary);
-    const details = document.createElement('p');
-    details.className = 'weather-details';
-    details.textContent = 'Осадки: ' + (Number.isFinite(current.precipitation) ? current.precipitation + ' мм' : '—') + ' · Восход ' + clock(daily.sunrise?.[0]) + ' · Закат ' + clock(daily.sunset?.[0]);
-    const forecast = document.createElement('div');
-    forecast.className = 'weather-forecast';
-    daily.time.slice(0, 3).forEach((date, index) => {
-      const day = document.createElement('div');
-      const title = document.createElement('span');
-      title.textContent = index === 0 ? 'Сегодня' : index === 1 ? 'Завтра' : new Date(date + 'T12:00:00Z').toLocaleDateString('ru-RU', { day: 'numeric', month: 'short', timeZone: 'UTC' });
-      const weather = document.createElement('span');
-      const [symbol, label] = sky(daily.weather_code?.[index]);
-      weather.textContent = symbol;
-      weather.setAttribute('aria-label', label);
-      weather.title = label;
-      const range = document.createElement('strong');
-      range.textContent = degrees(daily.temperature_2m_max?.[index]) + ' / ' + degrees(daily.temperature_2m_min?.[index]);
-      day.append(title, weather, range);
-      forecast.append(day);
-    });
-    content.replaceChildren(card, details, forecast);
-    content.hidden = false;
-    status.textContent = 'Данные на ' + clock(current.time) + ' МСК';
-  }
-  async function load(force = false) {
-    if (pending || (!force && Date.now() - loadedAt < 600000)) return;
-    pending = true;
-    refresh.disabled = true;
-    status.textContent = 'Загрузка прогноза…';
-    const controller = new AbortController();
-    const timeout = setTimeout(() => controller.abort(), 12000);
-    try {
-      const response = await fetch(url, { signal: controller.signal });
-      if (!response.ok) throw new Error('Weather unavailable');
-      render(await response.json());
-      loadedAt = Date.now();
-    } catch {
-      status.textContent = loadedAt ? 'Не удалось обновить. Показан предыдущий прогноз — попробуйте ещё раз.' : 'Погода сейчас недоступна. Проверьте интернет и нажмите «Обновить».';
-    } finally {
-      clearTimeout(timeout);
-      pending = false;
-      refresh.disabled = false;
-    }
-  }
-  document.querySelector('#weatherButton').addEventListener('click', () => {
-    modal.showModal();
-    load();
-  });
-  refresh.addEventListener('click', () => load(true));
-})();

+ 531 - 0
ПМ.03.02/2.2.500 Ерошко/style.css

@@ -0,0 +1,531 @@
+/* ===== css\base.css ===== */
+/* Shared palette, typography and page composition. */
+:root {
+  color-scheme: light;
+  --bg: #f4f4ef; --panel: #fdfdfb; --panel-2: #f0f1ec; --line: #dddfd7;
+  --text: #242823; --muted: #656e63; --accent: #3659d9; --accent-soft: #e9edfd;
+  --on-accent: #fff; --danger: #b43f35; --danger-soft: #fbeeea;
+  --supply: #4e777d; --signal: #986016; --wire: #7e897c; --metal: #d2d6ce;
+  --mono: "Space Mono", monospace; --sans: "Manrope", sans-serif;
+}
+:root[data-theme="dark"] {
+  color-scheme: dark;
+  --bg: #171a18; --panel: #1f2320; --panel-2: #272d28; --line: #3b433c;
+  --text: #eceee7; --muted: #a5aea2; --accent: #a4b6ff; --accent-soft: #2d374e;
+  --on-accent: #17213d; --danger: #f69c8e; --danger-soft: #432d29;
+  --supply: #92b6bb; --signal: #e9bb78; --wire: #8c998d; --metal: #737e72;
+}
+:root {
+  --lab-glow: rgba(54,89,217,.16);
+  --lab-grid: rgba(126,137,124,.16);
+  --lab-grid-major: rgba(126,137,124,.22);
+}
+:root[data-theme="dark"] {
+  --lab-glow: rgba(164,182,255,.12);
+  --lab-grid: rgba(140,153,141,.18);
+  --lab-grid-major: rgba(140,153,141,.26);
+}
+* { box-sizing: border-box; }
+html { zoom: 1.15; scroll-behavior: smooth; scrollbar-gutter: stable; }
+body { margin: 0; min-width: 0; background: radial-gradient(circle at 12% 0%, var(--lab-glow), transparent 24%), radial-gradient(circle at 92% 18%, rgba(177,118,28,.08), transparent 26%), linear-gradient(var(--lab-grid-major) 1px, transparent 1px), linear-gradient(90deg, var(--lab-grid-major) 1px, transparent 1px), linear-gradient(var(--lab-grid) 1px, transparent 1px), linear-gradient(90deg, var(--lab-grid) 1px, transparent 1px), var(--bg); background-size: auto, auto, 128px 128px, 128px 128px, 32px 32px, 32px 32px; color: var(--text); font-family: var(--sans); -webkit-font-smoothing: antialiased; }
+body, .workspace, .modal, .lock-stage, .schematic-scroll { transition: background-color .25s, color .25s, border-color .25s; }
+button, input, select { font: inherit; }
+button { color: inherit; }
+button, a, input, select { -webkit-tap-highlight-color: transparent; }
+:focus-visible { outline: 2px solid var(--accent); outline-offset: 4px; }
+::selection { background: var(--accent); color: var(--on-accent); }
+h1, h2, p { margin-top: 0; }
+.app-shell { width: min(1280px, calc(100% - 96px)); margin-inline: auto; }
+.topbar { min-height: 94px; display: flex; align-items: center; justify-content: space-between; gap: 20px; border-bottom: 1px solid var(--line); position: relative; }
+.topbar::after { content: "LAB / INSTRUMENT CONTROL"; position: absolute; right: 0; bottom: 7px; color: var(--muted); font: 7px var(--mono); letter-spacing: .16em; opacity: .65; }
+.brand { display: flex; align-items: center; gap: 12px; color: var(--text); text-decoration: none; font-size: 13px; font-weight: 800; letter-spacing: -.02em; }
+.brand-mark { display: grid; place-items: center; width: 32px; height: 36px; color: var(--accent); }
+.brand-mark svg { width: 28px; height: 32px; }
+.brand i { font: 10px var(--mono); color: var(--muted); padding-left: 12px; border-left: 1px solid var(--line); }
+.brand-mark { filter: drop-shadow(0 0 8px var(--lab-glow)); }
+.topbar-actions { display: flex; align-items: center; gap: 8px; }
+.hero { display: grid; grid-template-columns: 1fr 310px; gap: 32px; align-items: end; padding: 64px 0 46px; }
+.eyebrow, .panel-kicker { margin: 0; font: 10px/1.6 var(--mono); letter-spacing: .09em; text-transform: uppercase; color: var(--muted); }
+.hero h1 { margin: 16px 0 0; font-size: clamp(30px, 4.4vw, 64px); font-weight: 500; line-height: 1.08; letter-spacing: -.065em; text-shadow: 0 12px 34px var(--lab-glow); }
+.hero-title__suffix { color: var(--accent); font-size: .72em; font-weight: 600; letter-spacing: -.04em; white-space: nowrap; }
+.hero h1 span { color: var(--accent); }
+.hero-copy { margin: 0 0 4px; color: var(--muted); font-size: 13px; line-height: 1.85; }
+.hero-meta { display: flex; align-items: center; gap: 8px; margin-top: 18px; font: 9px var(--mono); color: var(--muted); text-transform: uppercase; letter-spacing: .05em; }
+.hero-meta i { width: 6px; height: 6px; background: var(--accent); border-radius: 50%; }
+.hero-meta i { box-shadow: 0 0 0 4px var(--accent-soft), 0 0 12px var(--accent); }
+footer { display: flex; justify-content: space-between; gap: 16px; padding: 26px 0 36px; color: var(--muted); font: 9px/1.8 var(--mono); }
+@media (max-width: 1000px) { .app-shell { width: calc(100% - 48px); } .hero { grid-template-columns: 1fr 260px; } }
+@media (max-width: 800px) { .hero { grid-template-columns: 1fr; gap: 22px; padding-block: 38px 30px; } .hero-copy { max-width: 440px; } .hero-meta { margin-top: 12px; } .topbar-actions > .button--ghost { display: none; } }
+@media (max-width: 480px) { .app-shell { width: calc(100% - 32px); } .topbar { min-height: 76px; gap: 8px; } .brand i { display: none; } .brand { gap: 6px; font-size: 12px; } .topbar-actions { gap: 2px; } .hero h1 { font-size: clamp(28px, 8vw, 38px); } footer { flex-direction: column; gap: 4px; } }
+@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } *, *::before, *::after { animation: none !important; transition: none !important; } }
+
+/* ===== css\components.css ===== */
+/* Workbench, controls and supporting dialogs. */
+.workspace { display: grid; grid-template-columns: .85fr 1.15fr; background: var(--panel); border: 1px solid var(--line); border-radius: 18px; overflow: hidden; }
+.workspace { background: color-mix(in srgb, var(--panel) 92%, var(--accent-soft)); box-shadow: 0 24px 60px rgba(11,29,29,.18), inset 0 1px rgba(255,255,255,.28); }
+.workspace::before { display: none; }
+.workspace { position: relative; }
+.workbench-column { min-width: 0; padding: 30px; display: flex; flex-direction: column; gap: 34px; }
+.workbench-column > .panel:first-child { margin-top: 0; }
+.workbench-column + .workbench-column { border-left: 1px solid var(--line); }
+.panel { min-width: 0; }
+.panel + .panel { padding-top: 30px; border-top: 1px solid var(--line); }
+.panel-heading { display: flex; justify-content: space-between; align-items: flex-start; gap: 14px; margin-bottom: 22px; }
+.panel-heading h2 { text-wrap: balance; }
+.panel h2 { margin: 6px 0 0; font-size: 20px; font-weight: 600; line-height: 1.35; letter-spacing: -.04em; }
+.panel h2::after { content: ""; display: block; width: 32px; height: 2px; margin-top: 9px; background: var(--accent); opacity: .65; }
+.button, .icon-button, .text-button { border: 0; cursor: pointer; transition: background .2s, color .2s, border-color .2s, transform .2s; }
+.button { min-height: 40px; padding: 11px 16px; border-radius: 8px; font-size: 11px; font-weight: 700; }
+.button--primary { background: linear-gradient(135deg, var(--accent), color-mix(in srgb, var(--accent) 72%, #15203f)); color: var(--on-accent); box-shadow: 0 5px 14px var(--lab-glow); }
+.button--primary:hover:not(:disabled) { filter: brightness(1.08); transform: translateY(-1px); }
+.button--ghost { background: color-mix(in srgb, var(--panel) 70%, transparent); border: 1px solid var(--line); }
+.button--ghost:hover { background: var(--panel-2); border-color: var(--muted); }
+.button--ghost { letter-spacing: .02em; }
+.icon-button { display: inline-grid; place-items: center; width: 36px; height: 36px; background: transparent; border-radius: 50%; font-size: 18px; color: var(--muted); }
+.icon-button svg { width: 18px; height: 18px; }
+.icon-button:hover, .icon-button.is-active { background: var(--accent-soft); color: var(--accent); }
+.text-button { padding: 6px 0; background: none; color: var(--accent); font-size: 11px; }
+.text-button:hover:not(:disabled) { text-decoration: underline; text-underline-offset: 4px; }
+.button:active:not(:disabled), .icon-button:active { transform: translateY(1px); }
+button:disabled { cursor: not-allowed; }
+.status-pill { padding: 6px 9px; border-radius: 5px; background: var(--panel-2); color: var(--muted); font: 9px var(--mono); letter-spacing: .08em; box-shadow: inset 0 0 0 1px rgba(255,255,255,.06); }
+.status-pill.is-open { background: var(--accent-soft); color: var(--accent); }
+.lock-stage { position: relative; height: 184px; overflow: hidden; border-radius: 10px; border: 1px solid var(--line); background: var(--panel-2); }
+.lock-stage__grid { position: absolute; inset: 0; background-image: radial-gradient(var(--wire) .7px, transparent .7px); background-size: 16px 16px; opacity: .13; }
+.coil { position: absolute; z-index: 2; left: 8%; top: 44px; width: 78px; height: 96px; border: 1px solid var(--wire); border-radius: 6px; background: repeating-linear-gradient(90deg, var(--panel) 0 5px, var(--metal) 5px 8px); display: grid; place-items: center; transition: border-color .25s; }
+.coil span { padding: 5px 7px; background: var(--panel); color: var(--muted); font: 9px var(--mono); border-radius: 3px; }
+.armature { position: absolute; z-index: 1; left: 24%; top: 74px; width: 66%; height: 34px; border: 1px solid var(--wire); border-radius: 3px; background: var(--metal); transition: left .55s cubic-bezier(.2,.8,.2,1); }
+.armature span { position: absolute; right: -5px; top: 5px; height: 22px; width: 10px; background: var(--wire); border-radius: 2px; }
+.receiver { position: absolute; right: 7%; top: 40px; width: 28px; height: 104px; border: 1px solid var(--wire); border-radius: 4px; background: var(--metal); }
+.receiver::after { content: ""; position: absolute; inset: 31px 5px; background: var(--panel-2); border-radius: 2px; }
+.beam { display: none; }
+.lock-stage.is-open .armature { left: 9%; }
+.lock-stage.is-open .coil { border-color: var(--accent); }
+.lock-stage.is-open .coil span { color: var(--accent); background: var(--accent-soft); }
+.readout { display: grid; align-items: center; grid-template-columns: auto 1fr 6px; gap: 8px; padding: 15px 0 0; color: var(--muted); font: 8px/1.6 var(--mono); }
+.readout strong { text-align: right; color: var(--text); font-size: 9px; font-weight: 400; }
+.readout-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--line); }
+.readout-dot.is-on { background: var(--accent); }
+.event-log { margin-top: 18px; }
+.event-log > span { color: var(--muted); font: 8px var(--mono); letter-spacing: .08em; }
+.event-log p { color: var(--muted); margin: 5px 0 0; min-height: 34px; font-size: 11px; line-height: 1.6; }
+.event-log { padding: 12px; border: 1px dashed var(--line); border-radius: 6px; background: color-mix(in srgb, var(--panel-2) 60%, transparent); }
+.attempt-log { margin-top: 10px; padding: 12px; border: 1px solid var(--line); border-radius: 6px; background: color-mix(in srgb, var(--panel-2) 42%, transparent); }
+.attempt-log__heading { display: flex; align-items: center; justify-content: space-between; gap: 8px; color: var(--muted); font: 8px var(--mono); letter-spacing: .08em; }
+.attempt-log__heading .text-button { padding: 2px 0; font: 9px var(--sans); letter-spacing: 0; }
+.attempt-log__heading .text-button:disabled { opacity: .45; cursor: default; }
+.attempt-log ol { display: grid; gap: 5px; margin: 9px 0 0; padding: 0; list-style: none; }
+.attempt-log__empty { color: var(--muted); font-size: 10px; }
+.attempt-log__item { display: grid; grid-template-columns: minmax(45px, .7fr) minmax(52px, 1fr) auto; align-items: center; gap: 8px; padding: 6px 8px; border-radius: 4px; background: var(--panel-2); font-size: 10px; }
+.attempt-log__code, .attempt-log__item time { font-family: var(--mono); }
+.attempt-log__result { color: var(--danger); }
+.attempt-log__item--success .attempt-log__result { color: var(--accent); }
+.attempt-log__item time { color: var(--muted); font-size: 9px; }
+.explanation { display: flex; gap: 14px; padding: 16px 0; border-block: 1px solid var(--line); }
+.explanation__number { color: var(--accent); font: 20px/1.3 var(--mono); flex-shrink: 0; }
+.explanation p { margin: 0; font-size: 11px; color: var(--muted); line-height: 1.7; }
+.legend { display: flex; gap: 14px; margin-top: 15px; color: var(--muted); font-size: 9px; }
+.legend-dot { display: inline-block; width: 5px; height: 5px; border-radius: 50%; margin-right: 5px; background: var(--wire); }
+.legend-dot--active { background: var(--accent); }
+.legend-dot--error { background: var(--danger); }
+.code-display { display: grid; grid-template-columns: 40px 1fr 32px; align-items: center; margin-bottom: 14px; padding: 8px 0 16px; border-bottom: 1px solid var(--line); }
+.code-display > span { color: var(--muted); font: 9px var(--mono); }
+.code-display strong { color: var(--text); text-align: center; font: 24px var(--mono); letter-spacing: .18em; }
+.code-display button { border: 0; cursor: pointer; background: transparent; color: var(--muted); font-size: 18px; }
+.lamp { width: 8px; height: 8px; margin-top: 7px; background: var(--line); border-radius: 50%; transition: background .2s; }
+.lamp.is-success { background: var(--accent); }
+.lamp.is-error { background: var(--danger); }
+.keypad { display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px; }
+.keypad button { height: 42px; border: 1px solid transparent; border-radius: 6px; background: var(--panel-2); color: var(--text); font: 14px var(--mono); cursor: pointer; transition: background .15s, border-color .15s, transform .15s; }
+.keypad button:hover:not(:disabled) { background: var(--accent-soft); color: var(--accent); border-color: var(--accent); }
+.keypad button:active:not(:disabled) { transform: scale(.96); }
+.keypad .keypad__disabled { visibility: hidden; }
+.keypad .keypad__reset { font-size: 9px; color: var(--muted); background: transparent; }
+.keyboard-tip { color: var(--muted); font-size: 10px; margin: 14px 0 0; }
+.field-label { display: flex; justify-content: space-between; gap: 10px; margin-bottom: 10px; color: var(--text); font-size: 12px; }
+.field-label span { font-size: 10px; color: var(--muted); }
+.code-input-wrap { display: flex; gap: 8px; }
+.code-input-wrap input { width: 100%; min-width: 0; padding: 9px 13px; border: 1px solid var(--line); border-radius: 8px; background: var(--panel); color: var(--text); font: 16px var(--mono); letter-spacing: .18em; }
+.field-hint { margin: 10px 0 22px; color: var(--muted); font-size: 10px; line-height: 1.7; }
+.control-actions { display: flex; flex-wrap: wrap; gap: 8px; }
+.save-note { font: 8px var(--mono); color: var(--muted); }
+.modal { width: min(520px, calc(100% - 32px)); max-height: calc(100dvh - 32px); overflow-y: auto; border: 1px solid var(--line); border-radius: 16px; padding: 28px; color: var(--text); background: var(--panel); box-shadow: 0 18px 90px #0002; }
+.modal[open] { animation: dialog-enter .2s ease-out; }
+.modal::backdrop { background: #11171370; backdrop-filter: blur(5px); }
+.modal__top { display: flex; justify-content: space-between; align-items: center; }
+.modal-close { display: grid; place-items: center; min-width: 32px; height: 32px; border: 0; border-radius: 50%; background: var(--panel-2); color: var(--muted); font-size: 22px; cursor: pointer; transition: color .2s, transform .2s; }
+.modal-close:hover { color: var(--text); transform: rotate(90deg); }
+.modal h2 { margin: 16px 0; font-size: 28px; font-weight: 500; letter-spacing: -.04em; }
+.modal p, .modal li { color: var(--muted); font-size: 13px; line-height: 1.8; }
+.modal ol { padding-left: 20px; }
+.modal__note { border-left: 2px solid var(--accent); padding: 12px; background: var(--accent-soft); font-size: 11px !important; }
+.quiz-progress { color: var(--muted); font: 10px var(--mono); }
+.quiz-options { display: grid; gap: 8px; }
+.quiz-options button, .archive-file { padding: 14px; border: 1px solid var(--line); border-radius: 8px; background: var(--panel-2); color: var(--text); text-align: left; cursor: pointer; transition: border-color .2s; }
+.quiz-options button:hover, .archive-file:hover { border-color: var(--accent); }
+.quiz-result { color: var(--accent) !important; font-size: 20px !important; }
+.radio-panel { display: flex; flex-wrap: wrap; gap: 16px; padding: 18px; border: 1px solid var(--line); border-radius: 10px; }
+.radio-panel__intro { display: flex; align-items: center; gap: 10px; }
+.radio-panel__intro strong { display: block; margin-top: 4px; font-size: 13px; }
+.radio-panel__signal { font-size: 10px; }
+.radio-controls { display: flex; align-items: center; gap: 8px; min-width: 0; }
+
+/* VM-16 inspired industrial keypad reconstruction. The surrounding PWA stays modern;
+   this module is deliberately warmer and more tactile, like a replaced laboratory unit. */
+.keypad-panel {
+  position: relative;
+  padding: 22px;
+  border: 1px solid color-mix(in srgb, var(--wire) 42%, var(--line));
+  border-radius: 9px;
+  background:
+    linear-gradient(135deg, rgba(255,255,255,.16), transparent 38%),
+    linear-gradient(180deg, #d8d9d0 0%, #c3c7bd 100%);
+  color: #20251f;
+  box-shadow: inset 0 1px rgba(255,255,255,.55), inset 0 -2px rgba(44,52,45,.14), 0 10px 24px rgba(12,18,14,.12);
+}
+:root[data-theme="dark"] .keypad-panel {
+  background: linear-gradient(135deg, rgba(255,255,255,.12), transparent 38%), linear-gradient(180deg, #777d72, #5e675e);
+  color: #182019;
+}
+.keypad-panel::before, .keypad-panel::after { content: ""; position: absolute; width: 6px; height: 6px; border-radius: 50%; background: #6f776d; box-shadow: inset 0 1px 1px rgba(255,255,255,.55); }
+.keypad-panel::before { top: 9px; left: 10px; }
+.keypad-panel::after { top: 9px; right: 10px; }
+.keypad-panel .panel-heading { align-items: center; margin-bottom: 15px; padding: 0 4px 13px; border-bottom: 1px solid rgba(44,52,45,.26); }
+.keypad-panel .panel-kicker { color: #566156; }
+.keypad-panel h2 { color: #202820; font: 700 17px/1.2 var(--mono); letter-spacing: -.04em; }
+:root[data-theme="dark"] .keypad-panel h2 { color: #1b241d; }
+.keypad-panel .lamp { position: relative; width: 19px; height: 19px; margin: 0 3px 0 0; border: 3px solid #596257; border-radius: 50%; background: radial-gradient(circle at 35% 30%, #d9ded5 0 12%, #8f988b 35%, #646c62 72%); box-shadow: inset 0 1px 2px rgba(255,255,255,.55), 0 1px 1px rgba(0,0,0,.28); transition: box-shadow .18s, background .18s, border-color .18s; }
+.keypad-panel .lamp::after { content: "HL1"; position: absolute; top: 24px; right: -2px; color: #566156; font: 7px var(--mono); }
+.keypad-panel .lamp.is-success { border-color: #5d624d; background: radial-gradient(circle at 34% 28%, #fff7c5 0 10%, #e6bb54 34%, #9b6b1c 74%); box-shadow: 0 0 0 2px rgba(175,139,48,.18), 0 0 14px rgba(226,183,72,.7), inset 0 1px 2px #fff7c5; }
+.keypad-panel .lamp.is-error { border-color: #67453e; background: radial-gradient(circle at 34% 28%, #ffd0bd 0 10%, #d96c4d 36%, #813a2d 74%); box-shadow: 0 0 0 2px rgba(190,70,46,.16), 0 0 11px rgba(211,86,60,.5); }
+.keypad-panel .code-display { margin: 0 4px 16px; padding: 10px 11px; border: 1px solid rgba(49,57,49,.3); border-radius: 3px; background: rgba(46,57,47,.12); box-shadow: inset 0 2px 4px rgba(24,31,25,.12); }
+.keypad-panel .code-display > span { color: #566156; font-size: 8px; }
+.keypad-panel .code-display strong { color: #283129; font-size: 21px; letter-spacing: .2em; text-shadow: 0 1px rgba(255,255,255,.42); }
+.keypad-panel .code-display button { color: #4f5c50; border: 1px solid rgba(49,57,49,.26); border-radius: 3px; background: rgba(255,255,255,.18); line-height: 1; }
+.keypad-panel .code-display button:hover { color: #273128; background: rgba(255,255,255,.36); }
+.keypad { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 9px; padding: 12px; border: 1px solid rgba(49,57,49,.3); border-radius: 4px; background: #aab1a6; box-shadow: inset 0 2px 5px rgba(35,43,36,.2), 0 1px rgba(255,255,255,.45); }
+.keypad button { position: relative; height: 48px; border: 1px solid #697267; border-radius: 4px; background: linear-gradient(180deg, #e5e6df 0%, #c8ccc3 65%, #b1b7ad 100%); color: #273128; font: 700 16px var(--mono); text-shadow: 0 1px rgba(255,255,255,.7); box-shadow: 0 3px 0 #727a70, 0 5px 7px rgba(38,46,39,.2), inset 0 1px rgba(255,255,255,.8); transition: transform .09s ease, box-shadow .09s ease, filter .15s ease, border-color .15s ease; }
+.keypad button::after { content: ""; position: absolute; inset: 3px; border: 1px solid rgba(255,255,255,.28); border-radius: 2px; pointer-events: none; }
+.keypad button:hover:not(:disabled) { border-color: #4d6a9b; filter: saturate(1.05) brightness(1.03); color: #233d78; background: linear-gradient(180deg, #eef0ea, #cbd2d0); }
+.keypad button:active:not(:disabled) { transform: translateY(3px); box-shadow: 0 0 0 #727a70, 0 1px 2px rgba(38,46,39,.25), inset 0 2px 4px rgba(38,46,39,.18); }
+.keypad button:focus-visible { outline-color: #3659d9; outline-offset: 3px; }
+.keypad .keypad__disabled { visibility: visible; opacity: .42; color: #657064; background: #b7bdb4; box-shadow: 0 2px 0 #858d82; cursor: default; }
+.keypad .keypad__disabled::after { content: "·"; border: 0; inset: auto; }
+.keypad .keypad__reset { color: #4e5b50; background: linear-gradient(180deg, #cfd3ca, #aeb5ab); font-size: 9px; letter-spacing: .04em; }
+.keyboard-tip { margin: 13px 4px 0; color: #566156; font: 9px var(--mono); }
+@media (max-width: 480px) {
+  .keypad-panel { padding: 17px 14px; }
+  .keypad { gap: 7px; padding: 9px; }
+  .keypad button { height: 45px; }
+}
+.radio-controls select { min-width: 0; padding: 10px; border: 1px solid var(--line); border-radius: 8px; background: var(--panel); color: var(--text); font-size: 11px; }
+.radio-play { width: 40px; padding-inline: 0; }
+.radio-volume { display: flex; align-items: center; gap: 10px; color: var(--muted); }
+.radio-volume input { width: 100%; min-width: 0; accent-color: var(--accent); }
+.radio-volume output { font: 10px var(--mono); }
+.radio-stations { display: flex; gap: 8px; order: 4; width: 100%; overflow-x: auto; padding: 3px; scrollbar-color: var(--wire) transparent; }
+.radio-station-card { flex: 0 0 60px; padding: 4px; border: 1px solid var(--line); border-radius: 7px; background: var(--panel); cursor: pointer; }
+.radio-station-card:hover, .radio-station-card.is-selected { border-color: var(--accent); }
+.radio-station-card img { width: 50px; height: 50px; border-radius: 4px; object-fit: cover; }
+.radio-station-card span { display: block; max-width: 50px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--muted); font-size: 8px; }
+.radio-credit { color: var(--muted); font-size: 10px; }
+.minesweeper-heading { display: flex; align-items: center; justify-content: space-between; gap: 14px; }
+.minesweeper-status { display: flex; justify-content: space-between; gap: 10px; padding: 12px 0; color: var(--muted); font: 10px var(--mono); }
+.minesweeper-board { display: grid; grid-template-columns: repeat(9, 1fr); gap: 3px; }
+.mine-cell { aspect-ratio: 1; border: 1px solid var(--line); border-radius: 4px; background: var(--panel-2); color: var(--text); font: 12px var(--mono); cursor: pointer; }
+.mine-cell:hover { border-color: var(--accent); }
+.mine-cell.is-open { border-color: transparent; background: var(--panel); }
+.mine-cell.is-mine { background: var(--danger-soft); color: var(--danger); }
+.mine-cell.is-flagged, .mine-cell.number-1 { color: var(--accent); }
+.mine-cell.number-2 { color: var(--supply); }
+.mine-cell.number-3, .mine-cell.number-4 { color: var(--danger); }
+.minesweeper-tip { font-size: 10px !important; margin: 14px 0 6px; }
+.minesweeper-flag-toggle { display: block; }
+.archive-files { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin-block: 20px; }
+.archive-file span, .archive-file small { display: block; font: 9px var(--mono); color: var(--muted); }
+.archive-file strong { display: block; margin: 12px 0; font-size: 12px; }
+.archive-file.is-open { border-color: var(--accent); }
+.archive-reader { padding: 16px; background: var(--panel-2); border-radius: 8px; color: var(--muted); font-size: 12px; }
+.archive-reader p { margin: 0; }
+.archive-reader button { margin-top: 10px; }
+.signal-test__top { display: flex; justify-content: space-between; gap: 10px; font: 9px var(--mono); margin-bottom: 12px; }
+.signal-lamps { display: grid; grid-template-columns: repeat(5, 1fr); gap: 8px; }
+.signal-lamp { aspect-ratio: 1; border: 1px solid var(--line); border-radius: 50%; background: var(--panel); cursor: pointer; }
+.signal-lamp.is-target { background: var(--accent); border-color: var(--accent); }
+@keyframes dialog-enter { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
+@media (max-width: 1000px) { .workbench-column { padding: 24px; } }
+@media (max-width: 800px) { .workspace { grid-template-columns: 1fr; } .workbench-column + .workbench-column { border-left: 0; border-top: 1px solid var(--line); } }
+@media (max-width: 480px) { .workspace { border-radius: 12px; } .workbench-column { padding: 20px; gap: 26px; } .panel h2 { font-size: 18px; } .modal { padding: 20px; } .archive-files { grid-template-columns: 1fr; } .minesweeper-heading { flex-wrap: wrap; } }
+
+/* ===== css\weather.css ===== */
+.topbar-actions .weather-button { display: inline-flex; align-items: center; white-space: nowrap; }
+.weather-modal { background: var(--panel); }
+.weather-modal .weather-location, .weather-modal #weatherStatus { color: var(--muted); font-size: 11px; }
+.weather-current { display: flex; align-items: center; gap: 20px; margin: 24px 0 16px; }
+.weather-icon { font-size: 52px; color: var(--accent); }
+.weather-current strong { font: 700 52px var(--mono); }
+.weather-current p, .weather-modal .weather-details { color: var(--muted); font-size: 12px; margin: 6px 0; }
+.weather-forecast { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; margin: 22px 0; }
+.weather-forecast > div { display: grid; gap: 12px; padding: 14px 6px; text-align: center; border: 1px solid var(--line); border-radius: 12px; font-size: 11px; }
+.weather-forecast > div > span:nth-child(2) { font-size: 26px; color: var(--accent); }
+.weather-forecast strong { font: 700 13px var(--mono); }
+.weather-footer { display: flex; justify-content: space-between; gap: 12px; border-top: 1px solid var(--line); padding-top: 16px; }
+.weather-footer a { color: var(--muted); font-size: 11px; }
+.weather-footer button { color: var(--accent); }
+.weather-footer button:disabled { opacity: .5; cursor: wait; }
+@media (max-width: 600px) {
+  .weather-modal { padding: 20px; }
+}
+
+.weather-button { gap: 6px; }
+@media (max-width: 800px) {
+  .topbar-actions .weather-button { display: inline-flex; justify-content: center; width: 36px; min-height: 36px; padding: 0; border: 0; border-radius: 50%; color: var(--muted); }
+  .weather-label { display: none; }
+}
+
+/* ===== css\arcade.css ===== */
+.arcade-modal { width:min(430px,calc(100% - 20px)); padding:18px; max-height:calc(100dvh - 20px); overflow-y:auto; }
+.arcade-heading,.arcade-hud,.arcade-actions { display:flex; justify-content:space-between; align-items:center; gap:10px; }
+.arcade-heading h2 { margin:0 0 12px; font-size:22px; }
+.arcade-hud { font:11px var(--mono); color:var(--accent); margin-bottom:10px; }
+#arcadeCanvas { display:block; width:100%; max-height:52dvh; aspect-ratio:3/4; object-fit:contain; background:#0d191c; border-radius:12px; border:1px solid var(--line); touch-action:none; }
+#arcadeStatus { min-height:20px; margin:9px 0; font-size:12px; color:var(--text); }
+.arcade-touch { display:flex; justify-content:center; gap:8px; margin:12px 0; }
+.arcade-touch button { min-width:42px; min-height:42px; padding:8px; border:1px solid var(--line); border-radius:8px; background:var(--panel-2); color:var(--text); touch-action:none; user-select:none; }
+.arcade-touch button:active { background:var(--accent-soft); }
+.arcade-modal .arcade-help { font-size:11px; color:var(--muted); margin-bottom:0; }
+
+/* ===== css\circuit.css ===== */
+/* Electronic lock: supply, driver, relay and switched electromagnet. */
+.circuit-panel .panel-heading { margin-bottom: 17px; }
+.circuit-panel { position: relative; }
+.circuit-status { display: flex; align-items: center; gap: 8px; color: var(--muted); font-size: 10px; margin-bottom: 12px; padding: 10px 12px; border: 1px solid var(--line); border-radius: 7px; background: color-mix(in srgb, var(--panel-2) 76%, transparent); box-shadow: inset 0 0 18px rgba(22,127,138,.06); }
+.circuit-status__dot { width: 6px; height: 6px; flex: 0 0 auto; border-radius: 50%; background: var(--muted); }
+.circuit-status__dot { box-shadow: 0 0 0 3px color-mix(in srgb, var(--muted) 20%, transparent); }
+.supply-label { margin-left: auto; white-space: nowrap; color: var(--supply); font: 10px var(--mono); }
+.schematic-scroll { overflow-x: auto; border: 1px solid var(--line); border-radius: 12px; background: radial-gradient(circle at 50% 15%, var(--lab-glow), transparent 42%), var(--panel-2); scrollbar-color: var(--wire) var(--panel-2); box-shadow: inset 0 0 28px rgba(20,28,22,.08); }
+.diagram-toolbar { padding: 6px 8px; border: 1px solid var(--line); border-radius: 7px; background: color-mix(in srgb, var(--panel-2) 70%, transparent); }
+.schematic-scroll:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
+.component-tooltip { position: relative; width: 100%; margin-top: 12px; padding: 16px 44px 15px 16px; border: 1px solid var(--accent); border-radius: 8px; background: var(--panel); box-shadow: 0 8px 20px rgba(0,0,0,.12); animation: tooltip-in .16s ease-out; }
+.component-tooltip[hidden] { display: none; }
+.component-tooltip__label { display: block; margin-bottom: 5px; color: var(--accent); font: 8px var(--mono); letter-spacing: .09em; }
+.component-tooltip strong { display: block; margin-bottom: 6px; color: var(--text); font: 700 13px/1.35 var(--mono); }
+.component-tooltip p { margin: 0; color: var(--muted); font-size: 11px; line-height: 1.6; }
+.component-tooltip__close { position: absolute; top: 8px; right: 9px; border: 0; background: transparent; color: var(--muted); cursor: pointer; font-size: 18px; line-height: 1; }
+.component-tooltip__close:hover { color: var(--text); }
+@keyframes tooltip-in { from { opacity: 0; transform: translateY(5px); } to { opacity: 1; transform: translateY(0); } }
+.schematic { display: block; width: 100%; min-width: 460px; color: var(--wire); }
+.schematic-grid { opacity: .4; }
+.schematic text { fill: var(--text); font: 11px var(--mono); }
+.schematic .component-label { fill: var(--text); font-weight: 700; }
+.schematic .schematic-muted { fill: var(--muted); font-size: 10px; }
+.schematic .supply-text { fill: var(--supply); font-weight: 700; }
+.wire { stroke: var(--wire); stroke-width: 1.7; fill: none; stroke-linejoin: round; stroke-linecap: round; transition: stroke .15s; }
+.wire circle:not(.transistor circle) { fill: var(--panel-2); }
+.supply { stroke: var(--supply); }
+.ground { stroke: var(--wire); }
+.controller-body, .keypad-input rect { fill: var(--panel); stroke: var(--wire); stroke-width: 1.5; }
+.controller [data-code-cell] rect { fill: var(--panel-2); stroke: var(--wire); }
+.controller [data-code-cell] text { font-size: 10px; fill: var(--muted); }
+.controller [data-code-cell].is-filled rect { fill: var(--accent); stroke: var(--accent); }
+.controller [data-code-cell].is-filled text { fill: var(--on-accent); font-weight: 700; }
+.schematic.has-input .controller-body { stroke: var(--accent); fill: var(--accent-soft); }
+.schematic.is-receiving :is(.input-signal, .keypad-input rect) { stroke: var(--signal); }
+.schematic.is-receiving .keypad-input text { fill: var(--signal); }
+.schematic.is-receiving .input-signal { stroke-width: 3; }
+.legend-dot--input { background: var(--signal); }
+.close-lock-button { display: block; width: 100%; margin-top: 14px; min-height: 44px; }
+.lock-stage { height: auto; aspect-ratio: 2 / 1; }
+.mechanism-cutaway { display: block; width: 100%; position: relative; color: var(--wire); }
+.mechanism-frame { fill: var(--panel); stroke: var(--line); }
+.mechanism-receiver, .mechanism-bolt rect { fill: var(--metal); stroke: var(--wire); }
+.mechanism-slot { fill: var(--panel-2); }
+.mechanism-bolt { transition: transform .55s cubic-bezier(.2,.8,.2,1); }
+.mechanism-bolt path { stroke: var(--wire); stroke-width: 4; }
+.mechanism-coil { fill: var(--panel); stroke: var(--wire); }
+.mechanism-turns { opacity: .65; }
+.mechanism-callouts path, .mechanism-spring { fill: none; stroke: var(--wire); stroke-width: 1.4; }
+.mechanism-callouts text { fill: var(--muted); font: 11px var(--sans); }
+.mechanism-force { opacity: 0; stroke: var(--accent); fill: none; stroke-width: 2; }
+text.mechanism-force { stroke: none; fill: var(--accent); font: 10px var(--sans); }
+.lock-stage.is-open .mechanism-bolt { transform: translateX(-65px); }
+.lock-stage.is-open .mechanism-coil { stroke: var(--accent); fill: var(--accent-soft); }
+.lock-stage.is-open .mechanism-turns { color: var(--accent); }
+.lock-stage.is-open .mechanism-force { opacity: 1; }
+.mechanism-explanation { color: var(--muted); font-size: 10px; line-height: 1.8; margin: 12px 0 0; }
+button:disabled { cursor: not-allowed; }
+.close-lock-button:disabled { opacity: 1; background: var(--panel-2); color: var(--muted); }
+.text-button:disabled, .keypad button:disabled:not(.keypad__disabled), .code-input-wrap :disabled, #backspaceButton:disabled { opacity: .4; }
+.mechanical-link { fill: none; stroke: var(--wire); stroke-width: 1; stroke-dasharray: 4 5; }
+.current-flow { fill: none; stroke: var(--accent); stroke-width: 3; stroke-dasharray: 3 13; stroke-linecap: round; opacity: 0; }
+.schematic[data-state="open"] .current-flow { opacity: 1; animation: circuit-current 1s linear infinite; }
+.schematic[data-state="open"] :is(.relay-path, .load-path, .transistor, .control-path, .contact) { stroke: var(--accent); }
+.schematic[data-state="open"] .controller-body { stroke: var(--accent); fill: var(--accent-soft); }
+.schematic[data-state="error"] .controller-body { stroke: var(--danger); fill: var(--danger-soft); }
+.circuit-readings { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin: 12px 0; }
+.circuit-readings > div { padding: 12px 0; }
+.circuit-readings span { display: block; color: var(--muted); font: 8px var(--mono); letter-spacing: .04em; }
+.circuit-readings strong { display: block; margin-top: 5px; color: var(--text); font: 12px var(--mono); }
+.circuit-panel:has([data-state="open"]) .circuit-readings strong { color: var(--accent); }
+.circuit-panel:has([data-state="open"]) .circuit-status__dot { background: var(--accent);  }
+.circuit-panel:has([data-state="error"]) .circuit-status__dot { background: var(--danger); }
+.legend-dot--supply { background: var(--supply); }
+.circuit-panel .legend { flex-wrap: wrap; gap: 8px 16px; }
+.schematic-note { margin: 12px 0 0; color: var(--muted); font-size: 10px; line-height: 1.6; }
+.schematic-scroll-hint { display: none; margin: 8px 0 0; color: var(--muted); font-size: 10px; }
+.extras-button { display: inline-grid; place-items: center; font-size: 23px; line-height: 1; flex: 0 0 34px; }
+.extras-modal { width: min(560px, calc(100% - 24px)); max-height: calc(100dvh - 32px); overflow-y: auto; }
+.extras-games { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin: 22px 0; }
+.extras-games button { display: grid; justify-items: center; gap: 10px; padding: 16px 8px; }
+.extras-games button > span { color: var(--accent); font: 24px var(--mono); }
+.extras-modal .radio-panel { margin: 0; padding: 16px; gap: 16px; box-shadow: none; }
+.extras-modal .radio-controls { flex: 1 1 100%; flex-wrap: wrap; }
+.extras-modal .radio-controls select { max-width: none; flex: 1; }
+.extras-modal .radio-volume { flex-basis: 100%; padding-top: 6px; }
+.extras-modal .radio-panel__intro .panel-kicker { font-size: 9px; color: var(--accent); margin: 0; }
+.extras-modal .radio-panel__signal { animation: none; color: var(--muted); text-shadow: none; }
+.extras-modal .radio-panel:has(audio[data-playing="true"]) .radio-panel__signal { color: var(--accent); }
+.extras-modal .extras-note { margin: 12px 0 0; color: var(--muted); font-size: 11px; }
+/* Detailed original circuit, inspector and learning controls. */
+.app-shell { width: min(1520px, calc(100% - 64px)); }
+.workspace { grid-template-columns: minmax(270px, .37fr) minmax(0, 1fr); }
+.workbench-column { padding: 26px; }
+.scheme-heading-actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 14px; }
+.scheme-intro { max-width: 640px; margin: -3px 0 18px; color: var(--muted); font-size: 12px; line-height: 1.8; }
+.diagram-toolbar { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 10px; }
+.view-switch, .zoom-tools { display: flex; align-items: center; gap: 3px; }
+.view-switch { background: var(--panel-2); padding: 3px; border-radius: 7px; }
+.view-switch button, .zoom-tools button, .node-selector button { min-height: 34px; padding: 7px 10px; border: 1px solid transparent; background: transparent; color: var(--muted); border-radius: 5px; font-size: 10px; cursor: pointer; }
+.view-switch [aria-pressed="true"] { color: var(--text); background: var(--panel); border-color: var(--line); }
+.zoom-tools button { border-color: var(--line); font: 12px var(--mono); }
+.zoom-tools button:disabled { opacity: .4; }
+.zoom-tools button:hover:not(:disabled), .node-selector button:hover { background: var(--accent-soft); }
+.zoom-tools[hidden], #diagramHost[hidden], #originalCircuit[hidden] { display: none; }
+.schematic-scroll { max-height: 650px; overflow: auto; background: var(--panel-2); }
+#diagramHost { min-width: 900px; }
+.schematic { width: 100%; min-width: 0; color: var(--wire); }
+.schematic .drawing { fill: none; stroke: var(--wire); stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round; }
+.schematic text { fill: var(--text); stroke: none; font: 12px var(--mono); }
+.schematic .schematic-muted { font-size: 10px; fill: var(--muted); }
+.schematic .zone-label { fill: var(--muted); font-size: 11px; }
+.schematic .patch-label { font-size: 9px; fill: var(--muted); }
+.schematic .mains-label { font-size: 10px; text-anchor: middle; }
+.schematic .junction { fill: var(--wire); stroke: none; }
+.circuit-part { cursor: pointer; }
+.circuit-part rect, .circuit-part circle { fill: var(--panel-2); }
+.circuit-part:hover, .circuit-part:focus, .circuit-part.is-selected { stroke: var(--accent); outline: none; }
+.circuit-part:hover text, .circuit-part:focus text, .circuit-part.is-selected text { fill: var(--accent); }
+.circuit-part.is-selected { stroke: var(--signal); }
+.circuit-part.is-selected text { fill: var(--signal); }
+.circuit-part:focus-visible { filter: drop-shadow(0 0 2px var(--accent)); }
+.schematic .memory-device.is-conducting { stroke: var(--accent); stroke-width: 3; }
+.schematic .memory-device.is-conducting text { fill: var(--accent); }
+.schematic[data-state="error"] :is(.memory-device, [data-part="VT1"], [data-part="R19"], [data-part="C3"]) { stroke: var(--danger); }
+.schematic[data-state="error"] .memory-device text { fill: var(--danger); }
+.schematic[data-state="open"] :is([data-part="YA1"], [data-part="HL1"], [data-part="VT1"], [data-part="VT3"], [data-part="VT4"], [data-part="C1"]) { stroke: var(--accent); }
+.schematic[data-state="open"] [data-part="HL1"] circle { fill: var(--accent-soft); }
+.signal-overlay { stroke: var(--signal); stroke-width: 3; fill: none; opacity: 0; }
+.schematic.is-receiving .signal-overlay { opacity: 1; }
+.schematic .key-active { stroke: var(--signal); stroke-width: 3; }
+.schematic .current-flow { pointer-events: none; }
+.diagram-caption { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 8px; padding: 10px 0 16px; color: var(--muted); font: 9px/1.6 var(--mono); }
+#keypadSignal { color: var(--signal); }
+.thyristor-stages { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; margin: 0 0 16px; }
+.thyristor-stages > div { display: grid; grid-template-columns: 1fr auto; gap: 6px; padding: 12px; background: linear-gradient(145deg, color-mix(in srgb, var(--panel-2) 88%, var(--accent-soft)), var(--panel-2)); border: 1px solid var(--line); border-radius: 8px; box-shadow: inset 0 1px rgba(255,255,255,.08); }
+.thyristor-stages span { grid-column: 1 / -1; color: var(--muted); font: 9px var(--mono); }
+.thyristor-stages strong { font: 13px var(--mono); }
+.thyristor-stages small { font: 9px/1.8 var(--mono); color: var(--muted); }
+.thyristor-stages .is-conducting { background: var(--accent-soft); border-color: var(--accent); }
+.thyristor-stages .is-conducting :is(strong, small) { color: var(--accent); }
+.circuit-readings { margin: 4px 0 10px; }
+.circuit-readings > div { min-width: 0; }
+.simulation-actions { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
+.hold-option { display: flex; align-items: center; gap: 8px; flex-basis: 100%; margin: 5px 0 8px; font-size: 11px; color: var(--muted); cursor: pointer; }
+.hold-option input { width: 15px; height: 15px; accent-color: var(--accent); }
+.node-selector { display: flex; flex-wrap: wrap; gap: 5px; margin: 24px 0 10px; }
+.node-selector button { border-color: var(--line); }
+.node-selector [aria-pressed="true"] { background: var(--accent-soft); color: var(--accent); border-color: var(--accent); }
+.node-selector [aria-pressed="true"] { box-shadow: inset 0 -2px var(--accent), 0 0 12px var(--lab-glow); }
+.component-detail { padding: 22px; border: 1px solid color-mix(in srgb, var(--accent) 26%, var(--line)); border-radius: 10px; background: linear-gradient(145deg, color-mix(in srgb, var(--panel) 92%, var(--accent-soft)), var(--panel)); }
+.component-detail-disclosure { margin-top: 14px; }
+.component-detail-disclosure > summary { display: flex; align-items: baseline; justify-content: space-between; gap: 14px; padding: 14px 0; border-block: 1px solid var(--line); color: var(--text); cursor: pointer; font: 600 12px var(--sans); list-style: none; }
+.component-detail-disclosure > summary::-webkit-details-marker { display: none; }
+.component-detail-disclosure > summary::before { content: "+"; width: 18px; color: var(--accent); font: 16px var(--mono); }
+.component-detail-disclosure[open] > summary::before { content: "−"; }
+.component-detail-disclosure > summary span { margin-left: auto; color: var(--muted); font: 9px var(--mono); }
+.component-detail-disclosure > summary:hover { color: var(--accent); }
+.component-detail-disclosure .component-detail { margin-top: 12px; }
+.component-detail h3 { font-size: 16px; font-weight: 600; line-height: 1.6; margin: 7px 0; }
+.component-detail p { margin-bottom: 12px; color: var(--muted); font-size: 12px; line-height: 1.85; }
+.component-detail .detail-refs { font: 10px/1.8 var(--mono); color: var(--accent); }
+.detail-route { padding: 13px 15px; background: var(--panel-2); border-radius: 6px; }
+.detail-route > span { color: var(--muted); font: 8px var(--mono); letter-spacing: .06em; }
+.detail-route p { margin: 5px 0 0; }
+.component-detail .detail-state { border-left: 2px solid var(--accent); padding-left: 12px; margin: 14px 0 0; color: var(--text); }
+.component-overview { display: grid; gap: 10px; margin-top: 14px; }
+.component-overview p { margin: 0; }
+.circuit-disclosure { border-bottom: 1px solid var(--line); }
+.circuit-disclosure summary { cursor: pointer; padding: 18px 0; font-size: 12px; font-weight: 600; }
+.operation-steps { padding-left: 24px; margin: 0 0 20px; }
+.operation-steps li { color: var(--muted); padding: 0 0 12px 5px; font-size: 12px; line-height: 1.9; }
+.operation-steps b { color: var(--text); }
+.component-table-wrap { overflow: auto; margin-bottom: 18px; }
+.component-table { border-collapse: collapse; width: 100%; font-size: 11px; text-align: left; }
+.component-table th, .component-table td { border-bottom: 1px solid var(--line); padding: 10px 8px; font-weight: 400; line-height: 1.7; }
+.component-table thead { color: var(--muted); }
+.component-table button { border: 0; padding: 5px; background: var(--accent-soft); color: var(--accent); font: 10px var(--mono); cursor: pointer; border-radius: 4px; }
+.schematic-source { color: var(--muted); font-size: 10px; line-height: 1.8; margin: 12px 0 0; }
+.schematic-source a { color: var(--muted); text-underline-offset: 3px; }
+.circuit-modal { position: fixed; inset: 0; width: calc(100vw - 36px); max-width: 1600px; height: calc(100dvh - 36px); max-height: none; padding: 30px; border: 1px solid var(--line); border-radius: 14px; background: var(--panel); color: var(--text); overflow: auto; }
+.circuit-modal::backdrop { background: #11171390; backdrop-filter: blur(4px); }
+.circuit-modal .circuit-panel { max-width: 1400px; margin: 0 auto; }
+.circuit-modal .panel-heading { position: sticky; top: -1px; padding: 12px 0; background: var(--panel); z-index: 2; }
+.circuit-modal .schematic-scroll { max-height: 72vh; }
+#finishPulse:disabled { display: none; }
+#collapseCircuit, .circuit-modal #expandCircuit { display: none; }
+.circuit-modal #collapseCircuit { display: inline-block; }
+.close-lock-button { display: block; width: 100%; margin-top: 14px; min-height: 44px; }
+@media (max-width: 1100px) { .app-shell { width: calc(100% - 40px); } .workbench-column { padding: 20px; } .workspace { grid-template-columns: 280px minmax(0, 1fr); } .thyristor-stages > div { padding: 9px; } .thyristor-stages small { grid-column: 1 / -1; } }
+@media (max-width: 800px) { .workspace { grid-template-columns: 1fr; } .workbench-column:first-child { display: grid; grid-template-columns: 1fr 1fr; align-items: start; gap: 24px; } .workbench-column:first-child .panel + .panel { padding-top: 0; border: 0; } .circuit-modal { padding: 20px; } }
+@media (max-width: 540px) { .app-shell { width: calc(100% - 24px); } .workbench-column:first-child { display: flex; } .workbench-column:first-child .panel + .panel { padding-top: 24px; border-top: 1px solid var(--line); } .workbench-column { padding: 18px; } .thyristor-stages { gap: 5px; } .thyristor-stages > div { padding: 8px; } .thyristor-stages span { font-size: 8px; } .component-detail { padding: 16px; } .view-switch button { padding-inline: 7px; } .zoom-tools button { padding-inline: 8px; } .circuit-modal { padding: 16px; width: calc(100vw - 16px); height: calc(100dvh - 16px); } .scheme-heading-actions { gap: 10px; } .circuit-readings span { font-size: 7px; } .circuit-readings strong { font-size: 10px; } }
+@keyframes circuit-current { to { stroke-dashoffset: -32; } }
+@media (max-width: 800px) { .extras-button { display: inline-grid; } }
+@media (max-width: 540px) { .schematic-scroll-hint { display: block; } }
+@media (max-width: 440px) { .extras-modal { padding: 20px; } .circuit-readings > div { padding: 9px 6px; } .circuit-status { flex-wrap: wrap; } .supply-label { display: none; } }
+@media (prefers-reduced-motion: reduce) { .schematic[data-state="open"] .current-flow { animation: none; } .armature, .beam { transition: none; } }
+
+/* The working schematic consists only of SVG paths and component symbols. */
+.diagram-format { display: flex; align-items: center; gap: 8px; color: var(--muted); font: 9px/1.6 var(--mono); }
+.diagram-format > span { padding: 3px 5px; border: 1px solid var(--line); border-radius: 3px; color: var(--accent); }
+.svg-download { display: inline-flex; align-items: center; min-height: 34px; padding: 6px 8px; border: 1px solid var(--line); border-radius: 5px; color: var(--accent); font: 10px var(--mono); text-decoration: none; }
+#diagramHost { min-width: 1050px; }
+.vector-schematic .part-hitbox { fill: transparent; stroke: none; }
+.vector-schematic .circuit-part:hover .part-hitbox, .vector-schematic .circuit-part:focus .part-hitbox { fill: var(--accent-soft); opacity: .55; }
+.vector-schematic .circuit-part.is-selected .part-symbol { stroke: var(--signal); }
+.vector-schematic .circuit-part.is-selected .component-label { fill: var(--signal); }
+.vector-schematic .is-biased .part-symbol { stroke: var(--supply); }
+.vector-schematic .is-biased .arrow-fill { fill: var(--supply); }
+.vector-schematic .is-on .part-symbol, .vector-schematic .is-conducting .part-symbol { stroke: var(--accent); stroke-width: 2.5; }
+.vector-schematic .is-on .arrow-fill { fill: var(--accent); }
+.vector-schematic .is-on .component-label, .vector-schematic .is-conducting .component-label { fill: var(--accent); }
+.vector-schematic .memory-device.is-conducting .part-symbol { stroke: var(--accent); stroke-width: 2.5; }
+.vector-schematic .memory-device.is-conducting .component-label { fill: var(--accent); }
+.vector-schematic .net.is-conducting { stroke: var(--accent); stroke-width: 2.3; }
+.vector-schematic .logic-supply-net { stroke: var(--supply); }
+.vector-schematic .code-wire { stroke: var(--accent); stroke-width: 2.2; }
+.vector-schematic .exit-current { display: none; }
+.vector-schematic[data-exit="true"] .exit-current { display: inline; }
+.vector-schematic[data-exit="true"] .code-current { display: none; }
+.vector-schematic[data-state="open"] [data-part="HL1"] .lamp-bulb { fill: var(--accent-soft); stroke: var(--accent); }
+.vector-schematic[data-state="open"] [data-part="YA1"] .part-symbol, .vector-schematic[data-state="open"] [data-part="HL1"] .part-symbol { stroke: var(--accent); stroke-width: 2.5; }
+.vector-schematic[data-state="open"][data-exit="false"] [data-part="C1"] .part-symbol { stroke: var(--accent); animation: capacitor-charge 1.5s linear both; }
+.vector-schematic.is-held[data-state="open"] [data-part="C1"] .part-symbol { animation: none; }
+.vector-schematic[data-state="error"] :is(.memory-device,[data-part="VT1"],[data-part="C3"],[data-part="R19"]) .part-symbol { stroke: var(--danger); }
+.vector-schematic[data-state="error"] .fault-flow { opacity: 1; }
+.vector-schematic.is-releasing [data-part="VT3"] .part-symbol { stroke: var(--accent); }
+.vector-schematic.is-releasing :is([data-part="VT1"],[data-part="VT4"]) .part-symbol { stroke: var(--wire); }
+.vector-schematic.is-flyback .flyback-flow { opacity: 1; animation: circuit-current .7s linear infinite; }
+.vector-schematic.is-receiving .key-active .part-symbol, .vector-schematic.is-receiving .key-active { stroke: var(--signal); stroke-width: 2.5; }
+@keyframes capacitor-charge { from { opacity: .35; } to { opacity: 1; } }
+@media (max-width: 540px) { .diagram-format { max-width: 135px; font-size: 8px; } .svg-download { padding-inline: 5px; } }
+

Некоторые файлы не были показаны из-за большого количества измененных файлов