[html]
<div class="gb-page">

  <header class="gb-masthead">
    <div class="gb-banner" style="">
      <p class="gb-banner-motto" lang="la">Liber hospitvm</p>
      <h1 class="gb-title">Гостевая</h1>
      <p class="gb-banner-intro">Империя доживает последние спокойные дни. Трон принадлежит Императору, власть — Капитулу, а Барьер век за веком сужается. Шесть земель покорены, но не примирены: при дворе плетутся заговоры, среди магов зреет раскол.</p>
      <div class="gb-banner-foot">
        <div class="gb-banner-note">
          <h2 class="gb-note-title">Важно</h2>
          <p>Роли и внешности держим <strong>до открытия</strong>, если раз в <strong>три дня</strong> вы оставляете сообщение на форуме. Бронь за гостем — <strong>сутки</strong>. Нет роли в списке — значит, она свободна.</p>
        </div>
        <div class="gb-banner-note">
          <h2 class="gb-note-title">Ведут игру</h2>
          <p><a class="gb-gm" href="/profile.php?id=5">Cedric Corveyn</a> — куратор Восточного Триархата, Альсара и Латериума; техподдержка</p>
          <p><a class="gb-gm" href="/profile.php?id=4">Sabella Corveyn</a> — куратор Эринора, Рунхейма и Вельгории; квестовик затейник</p>
        </div>
      </div>
    </div>
    <nav class="gb-nav" aria-label="Полезные ссылки">
      <ul class="gb-links">
        <li><a class="gb-link" href="/viewtopic.php?id=21#p42" target="_top"><i class="fa-solid fa-compass gb-link-icon" aria-hidden="true"></i>Путеводитель</a></li>
        <li><a class="gb-link" href="/viewtopic.php?id=19#p36" target="_top"><i class="fa-solid fa-book-open gb-link-icon" aria-hidden="true"></i>Сюжет</a></li>
        <li><a class="gb-link" href="/viewforum.php?id=3" target="_top"><i class="fa-solid fa-bullhorn gb-link-icon" aria-hidden="true"></i>Акции</a></li>
        <li><a class="gb-link" href="/viewtopic.php?id=16#p32" target="_top"><i class="fa-solid fa-user-plus gb-link-icon" aria-hidden="true"></i>Нужные</a></li>
        <li><a class="gb-link" href="/viewtopic.php?id=17#p33" target="_top"><i class="fa-solid fa-eye gb-link-icon" aria-hidden="true"></i>Хочу видеть</a></li>
        <li><a class="gb-link" href="/viewtopic.php?id=20#p38" target="_top"><i class="fa-solid fa-handshake gb-link-icon" aria-hidden="true"></i>Хочу к вам</a></li>
      </ul>
    </nav>
  </header>

  <section class="gb-section" aria-label="Реестр">
    <!-- кнопки земель: скрипт вставляет «Все земли» и по одной на TERRITORIES -->
    <div class="gb-lands" id="gb-lands" role="group" aria-label="Показать землю"></div>
    <div class="gb-ledger">
      <div class="gb-col">
        <h3 class="gb-col-title">Внешности</h3>
        <ul class="gb-list" id="gb-list-faces"></ul>
      </div>
      <div class="gb-col">
        <h3 class="gb-col-title">Роли и должности</h3>
        <ul class="gb-list" id="gb-list-roles"></ul>
      </div>
    </div>
  </section>

  <section class="gb-section" aria-labelledby="gb-hold-title">
    <h2 class="gb-heading" id="gb-hold-title">Придержать роль</h2>
    <form class="gb-box gb-hold" id="gb-hold">
      <label class="gb-field">Ник
        <input class="gb-input" type="text" name="nick" placeholder="Аврелий" autocomplete="off">
      </label>
      <label class="gb-field">Внешность
        <input class="gb-input" type="text" name="face" placeholder="Mads Mikkelsen" autocomplete="off">
      </label>
      <label class="gb-field">Роль или должность
        <input class="gb-input" type="text" name="role" placeholder="Первый министр, Латериум" autocomplete="off">
      </label>
      <label class="gb-field">Ссылка на профиль
        <input class="gb-input" type="url" name="profile" placeholder="https://" autocomplete="off">
      </label>
      <button class="button gb-submit" type="submit"><i class="fa-solid fa-feather-pointed" aria-hidden="true"></i> Скопировать заявку</button>
      <p class="gb-hold-status" role="status">Заявка скопируется в буфер обмена — останется вставить её ответом в этой теме.</p>
    </form>
  </section>

</div>

<script>
  (() => {
    // Занятые внешности и роли по землям — правится прямо здесь. Запись:
    // character — имя персонажа, position — должность или титул (можно не
    // указывать), faceclaim — внешность, player — ник игрока, playerUrl —
    // ссылка на его профиль (нет ссылки — ник показывается просто текстом),
    // until — необязательная дата, до которой держится бронь, в виде
    // 'ГГГГ-ММ-ДД': после неё запись сама пропадает из реестра, а роль и
    // внешность снова считаются свободными (вычищать её руками не нужно).
    // Без character запись занимает только внешность, роль остаётся свободной.
    // entries: [] — в земле пока никого. Шаблон записи (копируйте в entries):
    // { character: 'Имя Персонажа', position: 'Должность', faceclaim: 'Имя Фамилия', player: 'Ник', playerUrl: 'https://', until: 'ГГГГ-ММ-ДД' },
    const TERRITORIES = [
      {
        id: 'laterium',
        name: 'Латериум',
        entries: [],
      },
      {
        id: 'alsar',
        name: 'Альсар',
        entries: [],
      },
      {
        id: 'erinor',
        name: 'Эринор',
        entries: [
{ character: 'Sabella Corveyn', position: 'Королева-мать Эринора', faceclaim: 'Emily Carey', player: 'Sabella Corveyn', playerUrl: '/profile.php?id=4'},
{ character: 'Cedric Corveyn', position: 'Маг Капитула', faceclaim: 'Matteo Martari', player: 'Cedric Corveyn', playerUrl: '/profile.php?id=5'},
{faceclaim: 'Joey King', player: 'АМС'},
{faceclaim: 'Miguel Bernardeau', player: 'АМС'}
],
      },
      {
        id: 'runheim',
        name: 'Рунхейм',
        entries: [],
      },
      {
        id: 'tianxia',
        name: 'Восточный Триархат',
        entries: [
{faceclaim: 'Zhang Linghe', player: 'АМС'},
{faceclaim: 'Bai Lu', player: 'АМС'}
],
      },
      {
        id: 'velgoria',
        name: 'Вельгория',
        entries: [],
      },
    ];

    const ALL = 'all';

    // бронь держится весь последний день, поэтому граница — конец дня
    const holdEnd = (until) => new Date(`${until}T23:59:59`);

    const isHeld = ({ until }) => !until || holdEnd(until) >= new Date();

    // реестр и счётчики работают только с действующими бронями
    const LANDS = TERRITORIES.map((land) => ({ ...land, entries: land.entries.filter(isHeld) }));

    // запись без character занимает только внешность: в ролях и счётчиках её нет
    const hasRole = ({ character }) => Boolean(character);

    // ---- реестр ----

    const sortBy = (items, key) => [...items].sort((a, b) => a[key].localeCompare(b[key], 'ru'));

    const EMPTY_ROW = '<li class="gb-empty">Здесь пока никого — значит, свободно</li>';

    const renderPlayer = ({ player, playerUrl }) => {
      if (!player) return '';
      return playerUrl
        ? `<a class="gb-player" href="${playerUrl}" title="${player}">${player}</a>`
        : `<span class="gb-player" title="${player}">${player}</span>`;
    };

    // 'ГГГГ-ММ-ДД' -> 'ДД.ММ', как даты новостей в шапке форума
    const formatDay = (until) => `${until.slice(8, 10)}.${until.slice(5, 7)}`;

    const renderDate = ({ until }) => {
      if (!until) return '';
      const day = formatDay(until);
      return `<time class="gb-held-date" datetime="${until}" title="Бронь до ${day}"><span class="acchide">Бронь до </span>${day}</time>`;
    };

    // дата брони (если есть) отдельно, имя и ник — в общем теле строки:
    // на телефоне стили лишь ставят тело столбиком, раскладку не меняют
    const renderRow = (entry, entryHtml) => `
      <li class="gb-row">
        ${renderDate(entry)}
        <span class="gb-row-body">${entryHtml}${renderPlayer(entry)}</span>
      </li>`;

    const renderFace = (entry) => renderRow(entry,
      `<span class="gb-entry gb-face"${entry.character ? ` title="${entry.character}"` : ''}>${entry.faceclaim}</span>`);

    const renderRole = (entry) => renderRow(entry, `
      <span class="gb-entry">
        ${entry.position ? `<span class="gb-kind">${entry.position}</span>` : ''}
        <span class="gb-name">${entry.character}</span>
      </span>`);

    // русское склонение количества — та же функция, что в articles/characters.html
    const pluralize = (n, one, few, many) => {
      const mod10 = n % 10;
      const mod100 = n % 100;
      if (mod10 === 1 && mod100 !== 11) return one;
      if ([2, 3, 4].includes(mod10) && ![12, 13, 14].includes(mod100)) return few;
      return many;
    };

    const roleCount = ({ entries }) => entries.filter(hasRole).length;

    const landCount = (land) => {
      const count = roleCount(land);
      return count ? `${count} ${pluralize(count, 'роль', 'роли', 'ролей')}` : 'свободно';
    };

    // заголовок земли — отдельный <li> в том же списке, а не вложенный список:
    // так он прилипает к верху прокручиваемой колонки (position: sticky)
    const renderLandGroup = (land) => `
      <li class="gb-group-title">${land.name}<span class="gb-group-count">${landCount(land)}</span></li>
      ${roleCount(land) ? land.entries.filter(hasRole).map(renderRole).join('') : EMPTY_ROW}`;

    const facesList = document.getElementById('gb-list-faces');
    const rolesList = document.getElementById('gb-list-roles');

    const renderLedger = (landId) => {
      const visibleLands = landId === ALL ? LANDS : LANDS.filter((land) => land.id === landId);
      const faces = sortBy(visibleLands.flatMap((land) => land.entries).filter((entry) => entry.faceclaim), 'faceclaim');
      facesList.innerHTML = faces.length ? faces.map(renderFace).join('') : EMPTY_ROW;
      rolesList.innerHTML = visibleLands.map(renderLandGroup).join('');
    };

    // ---- фильтр по землям ----

    const renderLandButton = ({ id, name, entries }) => `
      <button class="gb-land" type="button" data-land="${id}" aria-pressed="${id === ALL}">
        ${name}<span class="gb-land-count">${roleCount({ entries })}</span>
      </button>`;

    const landButtons = [{ id: ALL, name: 'Все земли', entries: LANDS.flatMap((land) => land.entries) }, ...LANDS];

    const lands = document.getElementById('gb-lands');

    const selectLand = (button) => {
      lands.querySelectorAll('.gb-land').forEach((other) => other.setAttribute('aria-pressed', String(other === button)));
      renderLedger(button.dataset.land);
    };

    lands.innerHTML = landButtons.map(renderLandButton).join('');
    lands.addEventListener('click', (event) => {
      const button = event.target.closest('.gb-land');
      if (button) selectLand(button);
    });
    renderLedger(ALL);

    // ---- форма «Придержать роль» ----

    const holdText = ({ nick, face, role, profile }) => [
      nick && `Ник: ${nick}`,
      face && `Внешность: ${face}`,
      role && `Роль/должность: ${role}`,
      profile && `Профиль: ${profile}`,
    ].filter(Boolean).join('\n');

    const readField = (form, name) => form.elements[name].value.trim();

    const setStatus = (form, text) => {
      form.querySelector('.gb-hold-status').textContent = text;
    };

    // execCommand копирует только выделенный текст из DOM, поэтому заявка на
    // миг кладётся во временное поле, спрятанное общим .acchide форума. Идёт
    // первым и синхронно, пока жив клик: Clipboard API в iframe с чужого
    // домена обычно закрыт
    const copyViaSelection = (text) => {
      const buffer = document.createElement('textarea');
      buffer.className = 'acchide';
      buffer.value = text;
      buffer.setAttribute('readonly', '');
      document.body.append(buffer);
      buffer.select();
      const copied = document.execCommand('copy');
      buffer.remove();
      return copied;
    };

    const copyText = (text) => {
      if (copyViaSelection(text)) return Promise.resolve();
      return navigator.clipboard ? navigator.clipboard.writeText(text) : Promise.reject();
    };

    const onHoldSubmit = (event) => {
      event.preventDefault();
      const form = event.currentTarget;
      const values = { nick: readField(form, 'nick'), face: readField(form, 'face'), role: readField(form, 'role'), profile: readField(form, 'profile') };
      if (!values.face && !values.role) {
        setStatus(form, 'Укажите хотя бы внешность или роль.');
        return;
      }
      const text = holdText(values);
      copyText(text)
        .then(() => setStatus(form, 'Скопировано — вставьте заявку ответом в этой теме.'))
        .catch(() => setStatus(form, `Браузер не дал скопировать. Вставьте ответом в этой теме вручную: ${text.replace(/\n/g, '; ')}`));
    };

    document.getElementById('gb-hold').addEventListener('submit', onHoldSubmit);
  })();
</script>
[/html][hideprofile]