[html]<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Календарь</title>
<style>
:root{
--bg: #0b1020;
--text: #000000;
--ring: 0 0 0 2px rgba(236,127,73,.45), 0 0 30px rgba(236,127,73,.15); /* #c4534b glow */
--radius: 16px;
}
html,body{
height:100%;
margin:0;
font: 16px/1.5 'El Messiri' !important;
color: var(--text);
background: radial-gradient(1200px 800px at 10% 0%, #101a33 0%, var(--bg) 60%);
display:flex; align-items:center; justify-content:center; padding:24px;
}
.wrap{width:min(1100px, 95vw);}
h1{font-size:clamp(22px, 3vw, 28px); margin:0 0 14px; font-weight:700; letter-spacing:.2px;}
.sub{color:#000000; margin:0 0 18px;}
.calendar{
position: relative;
background: transparent; /* фон календаря прозрачный */
border: 1px solid rgba(0,0,0,.06);
border-radius: var(--radius);
box-shadow: 0 10px 40px rgba(0,0,0,.08), inset 0 0 0 1px rgba(0,0,0,.04);
overflow: hidden;
}
.months{ display:flex; gap:2px; }
.month{
flex:1; min-width: 460px;
background: transparent; /* прозрачный фон месяца */
border-radius: calc(var(--radius) - 2px);
padding: 12px 12px 16px;
border: 1px solid rgba(0,0,0,.06);
}
.month h2{ margin: 4px 6px 10px; font-family: 'Playfair Display SC' !important;
font-size: 30px;
font-style: italic;
text-shadow: 0 0 5px #6c5443; }
table{ width:100%; border-collapse: collapse; table-layout: fixed;}
thead th{
font-size:12px; color: #000000; font-weight:600; text-transform: uppercase; letter-spacing:.08em; padding:8px 4px 10px; border-bottom:1px solid rgba(0,0,0,.06);
}
td{
vertical-align: top; height: 92px; padding:8px;
border: 1px solid rgba(0,0,0,.06); /* как у .month */
position: relative; background: rgba(171,152,145,.50); /* a49a98 @ 50% */
}
td.is-out{ opacity: .35; }
.day-number{ font-size: 12px; color:#000000; font-weight:600; }
.event{
margin-top:6px; display:inline-flex; align-items:center; gap:6px; padding:6px 8px; border-radius:10px;
background: rgba(241,84,6,.12);
border: 1px dashed rgba(241,84,6,.40);
color:#000000; cursor: pointer;
user-select:none; outline: none; white-space: nowrap; max-width: 100%; overflow:hidden; text-overflow: ellipsis;
transition: box-shadow .2s ease, transform .2s ease;
}
.event:hover{ box-shadow: var(--ring); }
.event:focus-visible{ outline: none; box-shadow: var(--ring); }
.event .dot{ width:8px; height:8px; border-radius:50%; background: #515966; flex:0 0 8px; }
/* === Панель подробностей по принципу «мироописание от духа» === */
.info-panel{
position:absolute; z-index:5;
/* Центр по ширине без translate — строгие поля дают 80% ширины */
left:10%; right:10%; width:auto; /* 80% ширины контейнера */
/* Высота не более 50% контейнера */
max-height:50%; top:25px;
background:#ab9891; color:#111;
box-shadow: 0 0 0 5px #ab9891 inset, 0 0 0 10px rgba(0,0,0,.1) inset;
border-radius: 14px; padding:25px; text-align:left;
overflow:hidden;
/* Анимация появления/сворачивания */
opacity:0; transform: translateY(-8px) scale(.98); transition: opacity .22s ease, transform .24s ease;
visibility: hidden; pointer-events:none;
}
.info-panel.is-open{ opacity:1; transform: translateY(0) scale(1); visibility: visible; pointer-events:auto; }
.info-panel > .info-panel__content{ overflow-y:auto; height: calc(100% - 35px); padding-right:5px; }
.info-panel h5{ font: 700 20px 'El Messiri', sans-serif; margin:20px 0 10px; position:relative; padding-left:45px; }
.info-panel h5:before{ content:""; height:2px; width:30px; background: rgba(0,0,0,.7); position:absolute; left:0; top:50%; transform: translateY(-50%); }
.info-panel em{ font: 400 italic 11px 'El Messiri', sans-serif; display:block; border-right:5px solid rgba(0,0,0,.1); padding-right:10px; text-align:right; margin:10px 0; }
.close-btn{ background: rgba(0,0,0,.04); border:1px solid rgba(0,0,0,.06); height:15px; text-transform:uppercase; font: 600 10px/15px 'El Messiri', sans-serif; text-align:center; margin-bottom:20px; cursor:pointer; letter-spacing:2px; }
/* Адаптив */
@media (max-width: 980px){
.months{ flex-direction: column; }
.month{ min-width: unset; }
td{ height: 80px; }
/* На мобилках делаем шире и чуть выше */
.info-panel{ left:4%; right:4%; max-height:60%; top:12px; padding:18px; }
}
</style>
</head>
<body>
<div class="wrap">
<div class="calendar" id="calendar">
<div class="months">
<!-- Сентябрь 2025 -->
<section class="month" aria-label="Сентябрь 2025">
<h2>2025, Sept</h2>
<table role="grid" aria-labelledby="m-sep">
<thead>
<tr><th>Пн</th><th>Вт</th><th>Ср</th><th>Чт</th><th>Пт</th><th>Сб</th><th>Вс</th></tr>
</thead>
<tbody>
<tr>
<td><span class="day-number">1</span>
<button class="event" data-title="ООО КровьДляВсех" data-when="01 сентября, 12:00" data-where="последний дом на Бурбон-стрит" data-text="Как дети в школу, вампиры решили пойти подкрепиться. Но немного переборщили с масштабами. В подвале последнего дома на Бурбон-стрит начала работу фабрика по извлечению крови из живых (и очень сопротивляющихся) людей." aria-haspopup="dialog">
<span class="dot" aria-hidden="true"></span> ООО КровьДляВсех
</button></td>
<td><span class="day-number">2</span></td>
<td><span class="day-number">3</span></td>
<td><span class="day-number">4</span></td>
<td><span class="day-number">5</span></td>
<td><span class="day-number">6</span></td>
<td><span class="day-number">7</span></td>
</tr>
<tr>
<td><span class="day-number">8</span></td>
<td><span class="day-number">9</span></td>
<td><span class="day-number">10</span>
<button class="event" data-title="Censured" data-when="10 сентября, 21:00" data-where="район Ривер Ридж" data-text="Обнаружение старинного артефакта усиливает способности пары суккубов, что проводит к аномальной вспышке сексуального желания у окружающих." aria-haspopup="dialog">
<span class="dot" aria-hidden="true"></span> Censured
</button></td>
<td><span class="day-number">11</span></td>
<td><span class="day-number">12</span></td>
<td><span class="day-number">13</span></td>
<td><span class="day-number">14</span></td>
</tr>
<tr>
<td><span class="day-number">15</span></td>
<td><span class="day-number">16</span>
<button class="event" data-title="Съезд СМС" data-when="16 сентября, 10:00-18:00" data-where="отель Four Seasons, Канал-Стрит 2" data-text="Обмен опытом, наработками и общение стражей со всего мира происходит в Новом Орлеане. Несомненно у организации хватает недоброжелателей, чтобы испортить им мероприятие. Когда в отеле раздаётся первый взрыв, даже обычные люди начинают догадываться, что дело вовсе не в утечке газа." aria-haspopup="dialog">
<span class="dot" aria-hidden="true"></span> Съезд СМС
</button></td>
<td><span class="day-number">17</span></td>
<td><span class="day-number">18</span></td>
<td><span class="day-number">19</span></td>
<td><span class="day-number">20</span>
<button class="event" data-title="Турнир по волейболу: вампиры против оборотней" data-when="20 сентября, 22:00–04:00" data-where="Сангрилла Секретный пляж" data-text="Командный турнир 6×6. Стоило Новому Орлеану освободиться от власти ковена Собо, магические представители города отказались от всего былого... кроме любимого события: шестёрка лучших оборотней встречается с шестью наиболее способными вампирами, чтобы в жестоком бою доказать, кто из них лучший - в волейболе. Настоящий песок у бассейна элитного комплекса и искусственное солнце, что не вредит упырям. Получить приглашение на это мероприятие - уже честь. Здесь лучшие угощения, а шоу в перерывах не уступает Half-Time Супер Боула. Здесь налаживают контакты и просто хорошо проводят время. Будет ли и в этот раз так радужно или кому-то турнир всё-таки не угодил?" aria-haspopup="dialog">
<span class="dot" aria-hidden="true"></span> Турнир по волейболу
</button>
</td>
<td><span class="day-number">21</span></td>
</tr>
<tr>
<td><span class="day-number">22</span></td>
<td><span class="day-number">23</span></td>
<td><span class="day-number">24</span></td>
<td><span class="day-number">25</span>
<button class="event" data-title="Крик баньши" data-when="25 - 27 сентября" data-where="Новый Орлеан" data-text="Возросшее количество смертей и затяжная пасмурная погода повысили концентрацию баньши в городе. Покоя от их крика нет ни простым смертным, ни магическим существам." aria-haspopup="dialog">
<span class="dot" aria-hidden="true"></span> Крик баньши
</button></td>
<td><span class="day-number">26</span>
<button class="event" data-title="Крик баньши" data-when="25 - 27 сентября" data-where="Новый Орлеан" data-text="Возросшее количество смертей и затяжная пасмурная погода повысили концентрацию баньши в городе. Покоя от их крика нет ни простым смертным, ни магическим существам." aria-haspopup="dialog">
<span class="dot" aria-hidden="true"></span>
</button></td>
<td><span class="day-number">27</span>
<button class="event" data-title="Крик баньши" data-when="25 - 27 сентября" data-where="Новый Орлеан" data-text="Возросшее количество смертей и затяжная пасмурная погода повысили концентрацию баньши в городе. Покоя от их крика нет ни простым смертным, ни магическим существам." aria-haspopup="dialog">
<span class="dot" aria-hidden="true"></span>
</button></td>
<td><span class="day-number">28</span></td>
</tr>
<tr>
<td><span class="day-number">29</span></td>
<td><span class="day-number">30</span>
<button class="event" data-title="Восстание мертвецов" data-when="30 сентября, 00:00–06:00" data-where="кладбище Сент-Луи" data-text="После снятия запрета на некроматию ведьмы города зачастили на кладбище Сент-Луи. Заигрывание с мистическими силами идёт не по плану, когда один за другим воскресают духи каждого, кто был захоронен здесь. И, если этого недостаточно, за духами выбираются их разной степени разложившиеся тела. Пока призраки завершают свои земные дела и проведывают родственников, на кладбище разворачивается через чур реалистичный эпизод сериала Walking Dead." aria-haspopup="dialog">
<span class="dot" aria-hidden="true"></span> Восстание мертвецов
</button></td>
<td class="is-out"></td>
<td class="is-out"></td>
<td class="is-out"></td>
<td class="is-out"></td>
<td class="is-out"></td>
</tr>
</tbody>
</table>
</section>
</div>
<!-- мироописание от духа -->
<div class="info-panel" id="infoPanel" aria-live="polite" aria-atomic="true" role="dialog" aria-modal="false">
<div class="close-btn" id="panelClose">— закрыть —</div>
<div class="info-panel__content">
<h5 id="panelTitle">Заголовок</h5>
<em id="panelMeta">Когда и где</em>
<div id="panelBody">Текст</div>
</div>
</div>
</div>
</div>
<script>
(function(){
// === ЭЛЕМЕНТЫ ===
const calendar = document.getElementById('calendar');
const panel = document.getElementById('infoPanel');
const panelClose = document.getElementById('panelClose');
const pTitle = document.getElementById('panelTitle');
const pMeta = document.getElementById('panelMeta');
const pBody = document.getElementById('panelBody');
let currentTrigger = null;
// === ОТКРЫТИЕ ПАНЕЛИ ===
function openFromTrigger(btn){
currentTrigger = btn;
pTitle.textContent = btn.dataset.title || btn.textContent.trim();
const whenTxt = btn.dataset.when ? btn.dataset.when : '';
const whereTxt = btn.dataset.where ? (whenTxt ? ' · ' : '') + btn.dataset.where : '';
pMeta.textContent = (whenTxt + whereTxt).trim();
pBody.textContent = btn.dataset.text || '';
panel.classList.add('is-open');
panel.setAttribute('aria-modal','true');
panelClose.focus();
}
// === ЗАКРЫТИЕ ПАНЕЛИ ===
function closePopover(){
panel.classList.remove('is-open');
panel.setAttribute('aria-modal','false');
if (currentTrigger) currentTrigger.focus();
}
// === СЛУШАТЕЛИ ===
calendar.addEventListener('click', (e)=>{
const btn = e.target.closest('.event');
if (!btn) return;
openFromTrigger(btn);
});
panelClose.addEventListener('click', closePopover);
document.addEventListener('keydown', (e)=>{ if (e.key === 'Escape') closePopover(); });
// Быстрое копирование по двойному клику по заголовку
pTitle.addEventListener('dblclick', async ()=>{
const text = `${pTitle.textContent}\n${pMeta.textContent}\n\n${pBody.textContent}`.trim();
try { await navigator.clipboard.writeText(text); } catch(e){}
});
// === Самопроверка/"тесты" в консоли ===
(function selfTest(){
const results = [];
const reqIds = ['calendar','infoPanel','panelClose','panelTitle','panelMeta','panelBody'];
const missing = reqIds.filter(id => !document.getElementById(id));
results.push(['IDs exist', missing.length===0, missing.length ? ('missing: '+missing.join(', ')) : 'ok']);
const eventsCount = document.querySelectorAll('.event').length;
results.push(['Events present', eventsCount>0, 'count='+eventsCount]);
console.groupCollapsed('%cCalendar self-test','background:#0f1630;color:#cfe0ff;padding:2px 6px;border-radius:6px');
results.forEach(([name, ok, msg])=> console[ok?'log':'warn'](`${name}: ${ok?'OK':'FAIL'} (${msg})`));
console.groupEnd();
if (location.hash === '#test' && eventsCount){
const first = document.querySelector('.event');
openFromTrigger(first);
setTimeout(closePopover, 1200);
}
})();
})();
</script>
</body>
</html>
[/html]
[html]<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Календарь</title>
<style>
:root{
--bg: #0b1020;
--text: #000000;
--ring: 0 0 0 2px rgba(236,127,73,.45), 0 0 30px rgba(236,127,73,.15); /* #c4534b glow */
--radius: 16px;
}
html,body{
height:100%;
margin:0;
font: 16px/1.5 'El Messiri' !important;
color: var(--text);
background: radial-gradient(1200px 800px at 10% 0%, #101a33 0%, var(--bg) 60%);
display:flex; align-items:center; justify-content:center; padding:24px;
}
.wrap{width:min(1100px, 95vw);}
h1{font-size:clamp(22px, 3vw, 28px); margin:0 0 14px; font-weight:700; letter-spacing:.2px;}
.sub{color:#000000; margin:0 0 18px;}
.calendar{
position: relative;
background: transparent; /* фон календаря прозрачный */
border: 1px solid rgba(0,0,0,.06);
border-radius: var(--radius);
box-shadow: 0 10px 40px rgba(0,0,0,.08), inset 0 0 0 1px rgba(0,0,0,.04);
overflow: hidden;
}
.months{ display:flex; gap:2px; }
.month{
flex:1; min-width: 460px;
background: transparent; /* прозрачный фон месяца */
border-radius: calc(var(--radius) - 2px);
padding: 12px 12px 16px;
border: 1px solid rgba(0,0,0,.06);
}
.month h2{ margin: 4px 6px 10px; font-family: 'Playfair Display SC' !important;
font-size: 30px;
font-style: italic;
text-shadow: 0 0 5px #6c5443; }
table{ width:100%; border-collapse: collapse; table-layout: fixed;}
thead th{
font-size:12px; color: #000000; font-weight:600; text-transform: uppercase; letter-spacing:.08em; padding:8px 4px 10px; border-bottom:1px solid rgba(0,0,0,.06);
}
td{
vertical-align: top; height: 92px; padding:8px;
border: 1px solid rgba(0,0,0,.06); /* как у .month */
position: relative; background: rgba(171,152,145,.50); /* a49a98 @ 50% */
}
td.is-out{ opacity: .35; }
.day-number{ font-size: 12px; color:#000000; font-weight:600; }
.event{
margin-top:6px; display:inline-flex; align-items:center; gap:6px; padding:6px 8px; border-radius:10px;
background: rgba(241,84,6,.12); /* 993933 с прежней прозрачностью */
border: 1px dashed rgba(241,84,6,.40);
color:#000000; cursor: pointer;
user-select:none; outline: none; white-space: nowrap; max-width: 100%; overflow:hidden; text-overflow: ellipsis;
transition: box-shadow .2s ease, transform .2s ease;
}
.event:hover{ box-shadow: var(--ring); }
.event:focus-visible{ outline: none; box-shadow: var(--ring); }
.event .dot{ width:8px; height:8px; border-radius:50%; background: #515966; flex:0 0 8px; }
/* === Панель подробностей по принципу «мироописание от духа» === */
.info-panel{
position:absolute; z-index:5;
/* Центр по ширине без translate — строгие поля дают 80% ширины */
left:10%; right:10%; width:auto; /* 80% ширины контейнера */
/* Высота не более 50% контейнера */
max-height:50%; top:25px;
background:#ab9891; color:#111;
box-shadow: 0 0 0 5px #ab9891 inset, 0 0 0 10px rgba(0,0,0,.1) inset;
border-radius: 14px; padding:25px; text-align:left;
overflow:hidden;
/* Анимация появления/сворачивания */
opacity:0; transform: translateY(-8px) scale(.98); transition: opacity .22s ease, transform .24s ease;
visibility: hidden; pointer-events:none;
}
.info-panel.is-open{ opacity:1; transform: translateY(0) scale(1); visibility: visible; pointer-events:auto; }
.info-panel > .info-panel__content{ overflow-y:auto; height: calc(100% - 35px); padding-right:5px; }
.info-panel h5{ font: 700 20px 'El Messiri', sans-serif; margin:20px 0 10px; position:relative; padding-left:45px; }
.info-panel h5:before{ content:""; height:2px; width:30px; background: rgba(0,0,0,.7); position:absolute; left:0; top:50%; transform: translateY(-50%); }
.info-panel em{ font: 400 italic 11px 'El Messiri', sans-serif; display:block; border-right:5px solid rgba(0,0,0,.1); padding-right:10px; text-align:right; margin:10px 0; }
.close-btn{ background: rgba(0,0,0,.04); border:1px solid rgba(0,0,0,.06); height:15px; text-transform:uppercase; font: 600 10px/15px 'El Messiri', sans-serif; text-align:center; margin-bottom:20px; cursor:pointer; letter-spacing:2px; }
/* Адаптив */
@media (max-width: 980px){
.months{ flex-direction: column; }
.month{ min-width: unset; }
td{ height: 80px; }
/* На мобилках делаем шире и чуть выше */
.info-panel{ left:4%; right:4%; max-height:60%; top:12px; padding:18px; }
}
</style>
</head>
<body>
<div class="wrap">
<div class="calendar" id="calendar">
<div class="months">
<!-- Октябрь 2025 -->
<section class="month" aria-label="Октябрь 2025">
<h2>2025, Oct</h2>
<table role="grid">
<thead>
<tr><th>Пн</th><th>Вт</th><th>Ср</th><th>Чт</th><th>Пт</th><th>Сб</th><th>Вс</th></tr>
</thead>
<tbody>
<tr>
<td class="is-out"></td>
<td class="is-out"></td>
<td><span class="day-number">1</span></td>
<td><span class="day-number">2</span></td>
<td><span class="day-number">3</span></td>
<td><span class="day-number">4</span>
<button class="event" data-title="Задержка рейсов" data-when="04 октября, 09:00–23:00" data-where="аэропорт им. Луи Армстронга" data-text="Колдун, опаздывающий на свой самолёт, - горе в семье. А ещё - в целом городе. Сверхплотный туман заволакивает городской аэропорт, создавая транспортный коллапс. НЕестественное явление не позволяет покинуть здание, а стоит туману пробраться внутрь, как становится ясно, что монстры скрываются не только во тьме, но и в мельчайших частицах воды, которые скопились в воздухе." aria-haspopup="dialog">
<span class="dot" aria-hidden="true"></span> Задержка рейсов
</button></td>
<td><span class="day-number">5</span></td>
</tr>
<tr>
<td><span class="day-number">6</span></td>
<td><span class="day-number">7</span>
<button class="event" data-title="La Maison de papier" data-when="07 октября, 13:00–19:00" data-where="магический рынок" data-text="Ещё один вторник на магическом рынке (скрытые от туристов ряды с волшебными товарами, а также отдельные магазины) становится совсем не скучным. Банда грабителей в результате неудачного стечения обстоятельств берёт в заложники персонал и посетителей самого дорого магазина. Успеют ли СМС вмещаться до того, как прольётся первая кровь? (Нет)." aria-haspopup="dialog">
<span class="dot" aria-hidden="true"></span> La Maison de papier
</button></td>
<td><span class="day-number">8</span></td>
<td><span class="day-number">9</span></td>
<td><span class="day-number">10</span></td>
<td><span class="day-number">11</span></td>
<td><span class="day-number">12</span></td>
</tr>
<tr>
<td><span class="day-number">13</span></td>
<td><span class="day-number">14</span></td>
<td><span class="day-number">15</span></td>
<td><span class="day-number">16</span>
<button class="event" data-title="Неделя террора" data-when="16 - 23 октября" data-where="болота Манчак" data-text="День, когда на болотах было обнаружено первое растерзанное тело. Ругару терроризует болота в течении недели. Кемпинг и туристические экскурсии следовало бы отложить, но остались смельчаки, что не склонны менять планы." aria-haspopup="dialog">
<span class="dot" aria-hidden="true"></span> Неделя террора
</button>
</td>
<td><span class="day-number">17</span>
<button class="event" data-title="Неделя террора" data-when="16 - 23 октября" data-where="болота Манчак" data-text="День, когда на болотах было обнаружено первое растерзанное тело. Ругару терроризует болота в течении недели. Кемпинг и туристические экскурсии следовало бы отложить, но остались смельчаки, что не склонны менять планы." aria-haspopup="dialog">
<span class="dot" aria-hidden="true"></span>
</button></td>
<td><span class="day-number">18</span>
<button class="event" data-title="Неделя террора" data-when="16 - 23 октября" data-where="болота Манчак" data-text="День, когда на болотах было обнаружено первое растерзанное тело. Ругару терроризует болота в течении недели. Кемпинг и туристические экскурсии следовало бы отложить, но остались смельчаки, что не склонны менять планы." aria-haspopup="dialog">
<span class="dot" aria-hidden="true"></span>
</button></td>
<td><span class="day-number">19</span>
<button class="event" data-title="Неделя террора" data-when="16 - 23 октября" data-where="болота Манчак" data-text="День, когда на болотах было обнаружено первое растерзанное тело. Ругару терроризует болота в течении недели. Кемпинг и туристические экскурсии следовало бы отложить, но остались смельчаки, что не склонны менять планы." aria-haspopup="dialog">
<span class="dot" aria-hidden="true"></span>
</button></td>
</tr>
<tr>
<td><span class="day-number">20</span>
<button class="event" data-title="Неделя террора" data-when="16 - 23 октября" data-where="болота Манчак" data-text="День, когда на болотах было обнаружено первое растерзанное тело. Ругару терроризует болота в течении недели. Кемпинг и туристические экскурсии следовало бы отложить, но остались смельчаки, что не склонны менять планы." aria-haspopup="dialog">
<span class="dot" aria-hidden="true"></span>
</button></td>
<td><span class="day-number">21</span>
<button class="event" data-title="Неделя террора" data-when="16 - 23 октября" data-where="болота Манчак" data-text="День, когда на болотах было обнаружено первое растерзанное тело. Ругару терроризует болота в течении недели. Кемпинг и туристические экскурсии следовало бы отложить, но остались смельчаки, что не склонны менять планы." aria-haspopup="dialog">
<span class="dot" aria-hidden="true"></span>
</button></td>
<td><span class="day-number">22</span>
<button class="event" data-title="Неделя террора" data-when="16 - 23 октября" data-where="болота Манчак" data-text="День, когда на болотах было обнаружено первое растерзанное тело. Ругару терроризует болота в течении недели. Кемпинг и туристические экскурсии следовало бы отложить, но остались смельчаки, что не склонны менять планы." aria-haspopup="dialog">
<span class="dot" aria-hidden="true"></span>
</button></td>
<td><span class="day-number">23</span>
<button class="event" data-title="Неделя террора" data-when="16 - 23 октября" data-where="болота Манчак" data-text="День, когда на болотах было обнаружено первое растерзанное тело. Ругару терроризует болота в течении недели. Кемпинг и туристические экскурсии следовало бы отложить, но остались смельчаки, что не склонны менять планы." aria-haspopup="dialog">
<span class="dot" aria-hidden="true"></span>
</button></td>
<td><span class="day-number">24</span></td>
<td><span class="day-number">25</span></td>
<td><span class="day-number">26</span></td>
</tr>
<tr>
<td><span class="day-number">27</span></td>
<td><span class="day-number">28</span></td>
<td><span class="day-number">29</span></td>
<td><span class="day-number">30</span></td>
<td><span class="day-number">31</span>
<button class="event" data-title="Krewe of BOO!" data-when="31 октября, ночь" data-where="улицы Нового Орлеана" data-text="Хэллоуин в Новом Орлеане всегда был особенным, но 2025-й обещает переписать правила игры. Krewe of BOO! выходит на улицы города: мистические платформы, колдовской джаз и маски, за которыми может скрываться кто угодно: сосед с Мариньи, древний дух с Миссисипи или существо, которое ещё вчера боялось показать клыки." aria-haspopup="dialog">
<span class="dot" aria-hidden="true"></span> Krewe of BOO!
</button>
</td>
<td class="is-out"></td>
<td class="is-out"></td>
</tr>
</tbody>
</table>
</section>
</div>
<!-- мироописание от духа -->
<div class="info-panel" id="infoPanel" aria-live="polite" aria-atomic="true" role="dialog" aria-modal="false">
<div class="close-btn" id="panelClose">— закрыть —</div>
<div class="info-panel__content">
<h5 id="panelTitle">Заголовок</h5>
<em id="panelMeta">Когда и где</em>
<div id="panelBody">Текст</div>
</div>
</div>
</div>
</div>
<script>
(function(){
// === ЭЛЕМЕНТЫ ===
const calendar = document.getElementById('calendar');
const panel = document.getElementById('infoPanel');
const panelClose = document.getElementById('panelClose');
const pTitle = document.getElementById('panelTitle');
const pMeta = document.getElementById('panelMeta');
const pBody = document.getElementById('panelBody');
let currentTrigger = null;
// === ОТКРЫТИЕ ПАНЕЛИ ===
function openFromTrigger(btn){
currentTrigger = btn;
pTitle.textContent = btn.dataset.title || btn.textContent.trim();
const whenTxt = btn.dataset.when ? btn.dataset.when : '';
const whereTxt = btn.dataset.where ? (whenTxt ? ' · ' : '') + btn.dataset.where : '';
pMeta.textContent = (whenTxt + whereTxt).trim();
pBody.textContent = btn.dataset.text || '';
panel.classList.add('is-open');
panel.setAttribute('aria-modal','true');
panelClose.focus();
}
// === ЗАКРЫТИЕ ПАНЕЛИ ===
function closePopover(){
panel.classList.remove('is-open');
panel.setAttribute('aria-modal','false');
if (currentTrigger) currentTrigger.focus();
}
// === СЛУШАТЕЛИ ===
calendar.addEventListener('click', (e)=>{
const btn = e.target.closest('.event');
if (!btn) return;
openFromTrigger(btn);
});
panelClose.addEventListener('click', closePopover);
document.addEventListener('keydown', (e)=>{ if (e.key === 'Escape') closePopover(); });
// Быстрое копирование по двойному клику по заголовку
pTitle.addEventListener('dblclick', async ()=>{
const text = `${pTitle.textContent}\n${pMeta.textContent}\n\n${pBody.textContent}`.trim();
try { await navigator.clipboard.writeText(text); } catch(e){}
});
// === Самопроверка/"тесты" в консоли ===
(function selfTest(){
const results = [];
const reqIds = ['calendar','infoPanel','panelClose','panelTitle','panelMeta','panelBody'];
const missing = reqIds.filter(id => !document.getElementById(id));
results.push(['IDs exist', missing.length===0, missing.length ? ('missing: '+missing.join(', ')) : 'ok']);
const eventsCount = document.querySelectorAll('.event').length;
results.push(['Events present', eventsCount>0, 'count='+eventsCount]);
console.groupCollapsed('%cCalendar self-test','background:#0f1630;color:#cfe0ff;padding:2px 6px;border-radius:6px');
results.forEach(([name, ok, msg])=> console[ok?'log':'warn'](`${name}: ${ok?'OK':'FAIL'} (${msg})`));
console.groupEnd();
if (location.hash === '#test' && eventsCount){
const first = document.querySelector('.event');
openFromTrigger(first);
setTimeout(closePopover, 1200);
}
})();
})();
</script>
</body>
</html>
[/html]
Все описанные выше события можно использовать для личных отыгрышей.