No notes defined.

{#
  Structure: header > top row (logo, search form, search icon, menu button) + nav with nested lists.
  An item with children is a <button> followed by its panel, which holds a back button and the child list.
  idPrefix keeps the search form's and the nav's ids unique, so two headers can share a page.
  The panels have no ids here: the JS pairs each button with the panel that follows it and adds the
  ids, aria-controls and aria-expanded itself, so there is nothing to number in the markup.
  Styles: _ds2-top-nav.scss. Behaviour: ds2-top-nav.module.js. Aiming at WCAG 2.2 AA.
#}
{% set idPrefix = idPrefix or 'ds2-top-nav' %}
{# only written when hover is turned off, so leaving the option out keeps hover on #}
<header class="ds2-top-nav"{% if desktopHover === false %} data-desktop-hover="false"{% endif %}>
  <div class="ds2-top-nav__bar">

    {# crest and wordmark are separate svgs so the wordmark can be hidden on smaller screens #}
    {# the svgs are decorative and hidden, the link is named by the text below them (WCAG 1.1.1, 2.4.4) #}
    <a class="ds2-top-nav__logo" href="{{ logo.url }}">
      <svg class="ds2-top-nav__logo-crest" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261 261.2" aria-hidden="true" focusable="false">
        <polygon points="142,168.4 146.6,168.4 146.6,126.8 156.8,126.8 156.8,119.2 142,119.2"></polygon>
        <polygon points="162.4,168.4 167,168.4 167,126.8 177.2,126.8 177.2,119.2 162.4,119.2"></polygon>
        <polygon points="169,83.4 173.2,83.4 173.2,79.6 166.4,79.6 166.4,91 169,91"></polygon>
        <polygon points="157,83.4 161.2,83.4 161.2,79.6 154.4,79.6 154.4,91 157,91"></polygon>
        <polygon points="182.8,168.4 187.4,168.4 187.4,126.8 197.6,126.8 197.6,119.2 182.8,119.2"></polygon>
        <path d="M184.2,206.3c0-8.4-6.6-14.2-14.4-14.2s-14.4,5.8-14.4,14.2c0,8.4,6.6,14.2,14.4,14.2
          S184.2,214.7,184.2,206.3z M160.8,206.3c0-5.4,4.2-9,9-9s9,3.6,9,9c0,5.4-4.2,9-9,9S160.8,211.7,160.8,206.3z"></path>
        <polygon points="137.2,112.2 202.4,112.2 205.2,108 134.4,108"></polygon>
        <path d="M0,0.1v261h114.2V192H142v69.1h4.6v-77.7H193v77.7h4.6V192h27.8v69.1H261V0.1H0z M169.6,30.1h0.4l26.8,16.4
          h-54L169.6,30.1z M142.4,49.7h54.8l2.8,19.8h-60.4L142.4,49.7z M219.6,187.4h-22v-8.6H142v8.6h-22v-19.2h3.8v-62.8h6V72.8h80v32.6
          h6v62.8h3.8V187.4z"></path>
        <polygon points="181,83.4 185.2,83.4 185.2,79.6 178.4,79.6 178.4,91 181,91"></polygon>
      </svg>
      {# viewBox starts at the crest's right edge so the original gap is kept #}
      <svg class="ds2-top-nav__logo-wordmark" xmlns="http://www.w3.org/2000/svg" viewBox="261 0 1662 261.2" aria-hidden="true" focusable="false">
        <path d="M1851.2,174c7.6,5,18.4,9.8,33.4,9.8c22,0,38.4-12.2,38.4-31.6c0-33-45.2-30.6-45.2-48.6
          c0-6.2,6.2-9.6,13.8-9.6c8.8,0,16.2,3,25,7.8l0.4-19c-6-3.6-15.4-6.4-26.2-6.4c-23.2,0-35.6,14-35.6,30c0,8,2.4,14,6.4,18.6
          c13,14.4,38.4,16.8,38.4,30.6c0,8-8.6,10.6-16.4,10.6c-9.4,0-17.4-3.8-24-8L1851.2,174z M1769.6,164.2V95.8h13.4
          c21.4,0,34.6,13.2,34.6,34.2c0,20.6-13.2,34.2-34.6,34.2H1769.6z M1748,182.4h34.6c34,0,57.6-21.6,57.6-53
          c0-30.8-21.6-51.6-57.2-51.6h-35V182.4z M1666.4,182.4h60.6v-18h-39V138h33.6v-18.2H1688V95.6h38.8V77.8h-60.4V182.4z
          M1584.8,182.4h60.6v-18h-39V138h33.4v-18.2h-33.4V95.6h38.6V77.8h-60.2V182.4z M1509.4,182.4h58.4v-18h-36.6V77.8h-21.8V182.4z
          M1401,182.4h21.8v-44h32.4v-18h-32.4V95.6h38V77.8H1401V182.4z M1292.6,130c0-21.6,14.2-35.8,33-35.8s33.2,14.2,33.2,35.8
          c0,21.8-14.4,35.8-33.2,35.8S1292.6,151.8,1292.6,130 M1269.8,130c0,31,24,53.8,55.8,53.8c32,0,55.8-22.8,55.8-53.8
          s-23.8-53.8-55.8-53.8C1293.8,76.2,1269.8,99,1269.8,130 M1142.8,77.8l36.4,63.2v41.4h21.8v-41.2l36-63.4h-22.6l-16,29.4
          c-2.4,4.4-5.4,10-7.6,14.8h-0.2c-2.4-4.8-5.2-10.6-7.8-15l-16-29.2H1142.8z M1047.8,95.6h31.4v86.8h21.6V95.6h31.4V77.8h-84.4V95.6
          z M1007.6,182.4h21.6V77.8h-21.6V182.4z M915.2,174c7.4,5,18.2,9.8,33.2,9.8c22.4,0,38.4-11.6,38.4-31.6c0-32.4-45.2-31-45.2-48.6
          c0-6.2,6.2-9.6,14-9.6c8.6,0,16,3,25,7.8l0.2-19c-5.8-3.6-15.2-6.4-26.2-6.4c-23.2,0-35.6,14-35.6,30c0,8,2.4,14,6.6,18.6
          c12.8,14.4,38.2,16.8,38.2,30.6c0,8-8.4,10.6-16.4,10.6c-9.4,0-17.2-3.8-24-8L915.2,174z M847.4,126V95.6h9.2
          c10.4,0,16.2,6.2,16.2,14.4c0,9-7,16-16.2,16H847.4z M825.6,182.4h21.8V142h8.2c3.6,1.4,10,13.2,26,40.4h24.8
          c-15.8-26.8-25.8-44.4-31.2-47.4v-0.2c9.6-4.4,19-13.4,19-27c0-16.2-10.2-30-38-30h-30.6V182.4z M744,182.4h60.8v-18h-39V138h33.4
          v-18.2h-33.4V95.6h38.6V77.8H744V182.4z M624,77.8l43.4,105.4h17.4l44.6-105.4h-22.2l-22.8,55.8c-2.8,6.6-5,13.2-7.2,19.4h-0.4
          c-2-6.2-4.4-12.6-7-19.4l-22-55.8H624z M587.6,182.4h21.6V77.8h-21.6V182.4z M465,182.4h21.4V125c0-4.8,0-11.2-0.4-14.6h0.4
          c3,4.6,6,8.6,10,13.8l45.4,58.2H562V77.8h-21.4V131c0,4.4,0,11.4,0.2,16.4h-0.2c-3-4.4-5.8-8.6-10-13.8l-44-55.8H465V182.4z
          M351.2,141.6c0,27.6,15.4,42.2,44.6,42.2c28,0,45.6-14.2,45.6-42.6V77.8h-21.8V141c0,16.2-8,24.6-23.4,24.6
          c-14.6,0-23.2-8.8-23.2-24.6V77.8h-21.8V141.6z"></path>
      </svg>
      <span class="ds2-top-nav__visually-hidden">{{ logo.label }}</span>
    </a>

    {# the arrow and the box travel together, so on desktop they share one flex box rather than
       sitting in separate grid columns. It's display: contents below the desktop breakpoint,
       so the bar's own flex layout is unchanged there #}
    <div class="ds2-top-nav__search-group">
    {# closes the search box. Only shown while the box is open, so it's the one visible way out on a touch screen #}
    {# the same arrow as the menu's back buttons, named for what it does rather than where it goes #}
    <button class="ds2-top-nav__search-close" type="button">
      <span class="ds2-top-nav__back-arrow" aria-hidden="true"></span>
      <span class="ds2-top-nav__visually-hidden">{{ search.closeLabel }}</span>
    </button>

    {# a real form in a search landmark, so it's announced as search and Enter submits (WCAG 1.3.1) #}
    {# the input has a proper label, not just a placeholder, and the label starts with the visible word "Search" (3.3.2, 2.5.3) #}
    <form class="ds2-top-nav__search" id="{{ idPrefix }}-search" role="search" action="{{ search.action }}" method="get">
      <label class="ds2-top-nav__search-label ds2-top-nav__visually-hidden" for="{{ idPrefix }}-search-input">{{ search.label }}</label>
      <input class="ds2-top-nav__search-input" id="{{ idPrefix }}-search-input" name="{{ search.inputName }}" type="search" placeholder="{{ search.placeholder }}">
      {% for field in search.hiddenFields %}
        <input type="hidden" name="{{ field.name }}" value="{{ field.value }}">
      {% endfor %}
      <button class="ds2-top-nav__search-submit" type="submit">
        <svg class="ds2-top-nav__search-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" aria-hidden="true" focusable="false">
          <path d="M20.67,18.67H19.61l-.37-.36a8.66,8.66,0,1,0-.93.93l.36.37v1.06l6.66,6.65,2-2Zm-8,0a6,6,0,1,1,6-6A6,6,0,0,1,12.67,18.67Z"></path>
        </svg>
        <span class="ds2-top-nav__visually-hidden">{{ search.submitLabel }}</span>
      </button>
    </form>
    </div>

    {# shows the search box and focuses the input, on every screen size. It never opens the menu #}
    <button class="ds2-top-nav__search-toggle" type="button" aria-controls="{{ idPrefix }}-search">
      <svg class="ds2-top-nav__search-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" aria-hidden="true" focusable="false">
        <path d="M20.67,18.67H19.61l-.37-.36a8.66,8.66,0,1,0-.93.93l.36.37v1.06l6.66,6.65,2-2Zm-8,0a6,6,0,1,1,6-6A6,6,0,0,1,12.67,18.67Z"></path>
      </svg>
      <span class="ds2-top-nav__visually-hidden">{{ search.openLabel }}</span>
    </button>

    {# comes before the nav in the source so focus order matches the visual order on mobile and tablet (WCAG 2.4.3) #}
    {# the old component put the burger after the nav, so tabbing from it left the header entirely #}
    {# aria-expanded on this and the other disclosure buttons is added by the JS, so without JS nothing claims to be collapsed while its links are showing #}
    <button class="ds2-top-nav__menu-toggle" type="button" aria-controls="{{ idPrefix }}-menu">
      {# staggered lines taken from the mobile design reference, currentColor keeps it visible in forced colours #}
      <svg class="ds2-top-nav__menu-icon ds2-top-nav__menu-icon--burger" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 12" aria-hidden="true" focusable="false">
        <rect x="0" y="0" width="14" height="2"></rect>
        <rect x="9" y="5" width="15" height="2"></rect>
        <rect x="1" y="10" width="16" height="2"></rect>
      </svg>
      {# shown instead of the burger while the menu is open #}
      <svg class="ds2-top-nav__menu-icon ds2-top-nav__menu-icon--close" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
        <path d="M19 6.41 17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"></path>
      </svg>
      <span class="ds2-top-nav__visually-hidden">{{ menuLabel }}</span>
    </button>
  </div>

  {# a named navigation landmark, with the levels as nested lists so their structure is announced (WCAG 1.3.1) #}
  <nav class="ds2-top-nav__nav" id="{{ idPrefix }}-menu" aria-label="{{ navLabel }}">
    <ul class="ds2-top-nav__list ds2-top-nav__list--level-1">
      {% for level1Option in level1Options %}
        {% if level1Option.level2Options %}
          {# items with children are buttons, not links: they open a panel rather than going anywhere (WCAG 4.1.2, 2.1.1) #}
          {# the arrow is decorative, so it's hidden and the button is named by its text alone (1.1.1) #}
          <li class="ds2-top-nav__item ds2-top-nav__item--level-1 ds2-top-nav__item--has-children">
            <button class="ds2-top-nav__link ds2-top-nav__link--button" type="button">
              <span class="ds2-top-nav__link-text">{{ level1Option.title }}</span>
              <span class="ds2-top-nav__arrow" aria-hidden="true"></span>
            </button>

            {# the panel is the button's next sibling, and that's how the JS finds it. It gives the
               panel an id and the button an aria-controls on init, along with aria-expanded, so
               there are no ids to number here. Don't wrap either of them in anything #}
            <div class="ds2-top-nav__panel ds2-top-nav__panel--level-2">
              {# the back button shows only an arrow, so hidden text says where it goes (WCAG 1.1.1, 2.4.6) #}
              <button class="ds2-top-nav__back" type="button">
                <span class="ds2-top-nav__back-arrow" aria-hidden="true"></span>
                <span class="ds2-top-nav__visually-hidden">Back to main menu</span>
              </button>
              <ul class="ds2-top-nav__list ds2-top-nav__list--level-2">
                {% for level2Option in level1Option.level2Options %}
                  {% if level2Option.level3Options %}
                    <li class="ds2-top-nav__item ds2-top-nav__item--level-2 ds2-top-nav__item--has-children">
                      <button class="ds2-top-nav__link ds2-top-nav__link--button" type="button">
                        <span class="ds2-top-nav__link-text">{{ level2Option.title }}</span>
                        <span class="ds2-top-nav__arrow" aria-hidden="true"></span>
                      </button>

                      <div class="ds2-top-nav__panel ds2-top-nav__panel--level-3">
                        {# named after its destination, e.g. "Back to Study", so it makes sense out of context (WCAG 2.4.6) #}
                        <button class="ds2-top-nav__back" type="button">
                          <span class="ds2-top-nav__back-arrow" aria-hidden="true"></span>
                          <span class="ds2-top-nav__visually-hidden">Back to {{ level1Option.title }}</span>
                        </button>
                        <ul class="ds2-top-nav__list ds2-top-nav__list--level-3">
                          {% for level3Option in level2Option.level3Options %}
                            <li class="ds2-top-nav__item ds2-top-nav__item--level-3">
                              <a class="ds2-top-nav__link" href="{{ level3Option.url }}">{{ level3Option.title }}</a>
                            </li>
                          {% endfor %}
                        </ul>
                      </div>
                    </li>
                  {% else %}
                    <li class="ds2-top-nav__item ds2-top-nav__item--level-2">
                      <a class="ds2-top-nav__link" href="{{ level2Option.url }}">{{ level2Option.title }}</a>
                    </li>
                  {% endif %}
                {% endfor %}
              </ul>
            </div>
          </li>
        {% else %}
          <li class="ds2-top-nav__item ds2-top-nav__item--level-1">
            <a class="ds2-top-nav__link" href="{{ level1Option.url }}">{{ level1Option.title }}</a>
          </li>
        {% endif %}
      {% endfor %}
    </ul>
  </nav>
</header>
<header class="ds2-top-nav" data-desktop-hover="false">
    <div class="ds2-top-nav__bar">

        <a class="ds2-top-nav__logo" href="https://www.leeds.ac.uk/">
            <svg class="ds2-top-nav__logo-crest" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261 261.2" aria-hidden="true" focusable="false">
                <polygon points="142,168.4 146.6,168.4 146.6,126.8 156.8,126.8 156.8,119.2 142,119.2"></polygon>
                <polygon points="162.4,168.4 167,168.4 167,126.8 177.2,126.8 177.2,119.2 162.4,119.2"></polygon>
                <polygon points="169,83.4 173.2,83.4 173.2,79.6 166.4,79.6 166.4,91 169,91"></polygon>
                <polygon points="157,83.4 161.2,83.4 161.2,79.6 154.4,79.6 154.4,91 157,91"></polygon>
                <polygon points="182.8,168.4 187.4,168.4 187.4,126.8 197.6,126.8 197.6,119.2 182.8,119.2"></polygon>
                <path d="M184.2,206.3c0-8.4-6.6-14.2-14.4-14.2s-14.4,5.8-14.4,14.2c0,8.4,6.6,14.2,14.4,14.2
          S184.2,214.7,184.2,206.3z M160.8,206.3c0-5.4,4.2-9,9-9s9,3.6,9,9c0,5.4-4.2,9-9,9S160.8,211.7,160.8,206.3z"></path>
                <polygon points="137.2,112.2 202.4,112.2 205.2,108 134.4,108"></polygon>
                <path d="M0,0.1v261h114.2V192H142v69.1h4.6v-77.7H193v77.7h4.6V192h27.8v69.1H261V0.1H0z M169.6,30.1h0.4l26.8,16.4
          h-54L169.6,30.1z M142.4,49.7h54.8l2.8,19.8h-60.4L142.4,49.7z M219.6,187.4h-22v-8.6H142v8.6h-22v-19.2h3.8v-62.8h6V72.8h80v32.6
          h6v62.8h3.8V187.4z"></path>
                <polygon points="181,83.4 185.2,83.4 185.2,79.6 178.4,79.6 178.4,91 181,91"></polygon>
            </svg>

            <svg class="ds2-top-nav__logo-wordmark" xmlns="http://www.w3.org/2000/svg" viewBox="261 0 1662 261.2" aria-hidden="true" focusable="false">
                <path d="M1851.2,174c7.6,5,18.4,9.8,33.4,9.8c22,0,38.4-12.2,38.4-31.6c0-33-45.2-30.6-45.2-48.6
          c0-6.2,6.2-9.6,13.8-9.6c8.8,0,16.2,3,25,7.8l0.4-19c-6-3.6-15.4-6.4-26.2-6.4c-23.2,0-35.6,14-35.6,30c0,8,2.4,14,6.4,18.6
          c13,14.4,38.4,16.8,38.4,30.6c0,8-8.6,10.6-16.4,10.6c-9.4,0-17.4-3.8-24-8L1851.2,174z M1769.6,164.2V95.8h13.4
          c21.4,0,34.6,13.2,34.6,34.2c0,20.6-13.2,34.2-34.6,34.2H1769.6z M1748,182.4h34.6c34,0,57.6-21.6,57.6-53
          c0-30.8-21.6-51.6-57.2-51.6h-35V182.4z M1666.4,182.4h60.6v-18h-39V138h33.6v-18.2H1688V95.6h38.8V77.8h-60.4V182.4z
          M1584.8,182.4h60.6v-18h-39V138h33.4v-18.2h-33.4V95.6h38.6V77.8h-60.2V182.4z M1509.4,182.4h58.4v-18h-36.6V77.8h-21.8V182.4z
          M1401,182.4h21.8v-44h32.4v-18h-32.4V95.6h38V77.8H1401V182.4z M1292.6,130c0-21.6,14.2-35.8,33-35.8s33.2,14.2,33.2,35.8
          c0,21.8-14.4,35.8-33.2,35.8S1292.6,151.8,1292.6,130 M1269.8,130c0,31,24,53.8,55.8,53.8c32,0,55.8-22.8,55.8-53.8
          s-23.8-53.8-55.8-53.8C1293.8,76.2,1269.8,99,1269.8,130 M1142.8,77.8l36.4,63.2v41.4h21.8v-41.2l36-63.4h-22.6l-16,29.4
          c-2.4,4.4-5.4,10-7.6,14.8h-0.2c-2.4-4.8-5.2-10.6-7.8-15l-16-29.2H1142.8z M1047.8,95.6h31.4v86.8h21.6V95.6h31.4V77.8h-84.4V95.6
          z M1007.6,182.4h21.6V77.8h-21.6V182.4z M915.2,174c7.4,5,18.2,9.8,33.2,9.8c22.4,0,38.4-11.6,38.4-31.6c0-32.4-45.2-31-45.2-48.6
          c0-6.2,6.2-9.6,14-9.6c8.6,0,16,3,25,7.8l0.2-19c-5.8-3.6-15.2-6.4-26.2-6.4c-23.2,0-35.6,14-35.6,30c0,8,2.4,14,6.6,18.6
          c12.8,14.4,38.2,16.8,38.2,30.6c0,8-8.4,10.6-16.4,10.6c-9.4,0-17.2-3.8-24-8L915.2,174z M847.4,126V95.6h9.2
          c10.4,0,16.2,6.2,16.2,14.4c0,9-7,16-16.2,16H847.4z M825.6,182.4h21.8V142h8.2c3.6,1.4,10,13.2,26,40.4h24.8
          c-15.8-26.8-25.8-44.4-31.2-47.4v-0.2c9.6-4.4,19-13.4,19-27c0-16.2-10.2-30-38-30h-30.6V182.4z M744,182.4h60.8v-18h-39V138h33.4
          v-18.2h-33.4V95.6h38.6V77.8H744V182.4z M624,77.8l43.4,105.4h17.4l44.6-105.4h-22.2l-22.8,55.8c-2.8,6.6-5,13.2-7.2,19.4h-0.4
          c-2-6.2-4.4-12.6-7-19.4l-22-55.8H624z M587.6,182.4h21.6V77.8h-21.6V182.4z M465,182.4h21.4V125c0-4.8,0-11.2-0.4-14.6h0.4
          c3,4.6,6,8.6,10,13.8l45.4,58.2H562V77.8h-21.4V131c0,4.4,0,11.4,0.2,16.4h-0.2c-3-4.4-5.8-8.6-10-13.8l-44-55.8H465V182.4z
          M351.2,141.6c0,27.6,15.4,42.2,44.6,42.2c28,0,45.6-14.2,45.6-42.6V77.8h-21.8V141c0,16.2-8,24.6-23.4,24.6
          c-14.6,0-23.2-8.8-23.2-24.6V77.8h-21.8V141.6z"></path>
            </svg>
            <span class="ds2-top-nav__visually-hidden">University of Leeds homepage</span>
        </a>

        <div class="ds2-top-nav__search-group">

            <button class="ds2-top-nav__search-close" type="button">
                <span class="ds2-top-nav__back-arrow" aria-hidden="true"></span>
                <span class="ds2-top-nav__visually-hidden">Close search</span>
            </button>

            <form class="ds2-top-nav__search" id="ds2-top-nav-search" role="search" action="https://www.leeds.ac.uk/search" method="get">
                <label class="ds2-top-nav__search-label ds2-top-nav__visually-hidden" for="ds2-top-nav-search-input">Search leeds.ac.uk</label>
                <input class="ds2-top-nav__search-input" id="ds2-top-nav-search-input" name="q" type="search" placeholder="Search">

                <input type="hidden" name="searchOption" value="searchSite">

                <button class="ds2-top-nav__search-submit" type="submit">
                    <svg class="ds2-top-nav__search-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" aria-hidden="true" focusable="false">
                        <path d="M20.67,18.67H19.61l-.37-.36a8.66,8.66,0,1,0-.93.93l.36.37v1.06l6.66,6.65,2-2Zm-8,0a6,6,0,1,1,6-6A6,6,0,0,1,12.67,18.67Z"></path>
                    </svg>
                    <span class="ds2-top-nav__visually-hidden">Search</span>
                </button>
            </form>
        </div>

        <button class="ds2-top-nav__search-toggle" type="button" aria-controls="ds2-top-nav-search">
            <svg class="ds2-top-nav__search-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" aria-hidden="true" focusable="false">
                <path d="M20.67,18.67H19.61l-.37-.36a8.66,8.66,0,1,0-.93.93l.36.37v1.06l6.66,6.65,2-2Zm-8,0a6,6,0,1,1,6-6A6,6,0,0,1,12.67,18.67Z"></path>
            </svg>
            <span class="ds2-top-nav__visually-hidden">Search</span>
        </button>

        <button class="ds2-top-nav__menu-toggle" type="button" aria-controls="ds2-top-nav-menu">

            <svg class="ds2-top-nav__menu-icon ds2-top-nav__menu-icon--burger" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 12" aria-hidden="true" focusable="false">
                <rect x="0" y="0" width="14" height="2"></rect>
                <rect x="9" y="5" width="15" height="2"></rect>
                <rect x="1" y="10" width="16" height="2"></rect>
            </svg>

            <svg class="ds2-top-nav__menu-icon ds2-top-nav__menu-icon--close" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
                <path d="M19 6.41 17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"></path>
            </svg>
            <span class="ds2-top-nav__visually-hidden">Menu</span>
        </button>
    </div>

    <nav class="ds2-top-nav__nav" id="ds2-top-nav-menu" aria-label="Main">
        <ul class="ds2-top-nav__list ds2-top-nav__list--level-1">

            <li class="ds2-top-nav__item ds2-top-nav__item--level-1 ds2-top-nav__item--has-children">
                <button class="ds2-top-nav__link ds2-top-nav__link--button" type="button">
                    <span class="ds2-top-nav__link-text">Study</span>
                    <span class="ds2-top-nav__arrow" aria-hidden="true"></span>
                </button>

                <div class="ds2-top-nav__panel ds2-top-nav__panel--level-2">

                    <button class="ds2-top-nav__back" type="button">
                        <span class="ds2-top-nav__back-arrow" aria-hidden="true"></span>
                        <span class="ds2-top-nav__visually-hidden">Back to main menu</span>
                    </button>
                    <ul class="ds2-top-nav__list ds2-top-nav__list--level-2">

                        <li class="ds2-top-nav__item ds2-top-nav__item--level-2">
                            <a class="ds2-top-nav__link" href="https://courses.leeds.ac.uk/">Course search</a>
                        </li>

                        <li class="ds2-top-nav__item ds2-top-nav__item--level-2 ds2-top-nav__item--has-children">
                            <button class="ds2-top-nav__link ds2-top-nav__link--button" type="button">
                                <span class="ds2-top-nav__link-text">Undergraduate</span>
                                <span class="ds2-top-nav__arrow" aria-hidden="true"></span>
                            </button>

                            <div class="ds2-top-nav__panel ds2-top-nav__panel--level-3">

                                <button class="ds2-top-nav__back" type="button">
                                    <span class="ds2-top-nav__back-arrow" aria-hidden="true"></span>
                                    <span class="ds2-top-nav__visually-hidden">Back to Study</span>
                                </button>
                                <ul class="ds2-top-nav__list ds2-top-nav__list--level-3">

                                    <li class="ds2-top-nav__item ds2-top-nav__item--level-3">
                                        <a class="ds2-top-nav__link" href="https://www.leeds.ac.uk/undergraduate">Undergraduate home page</a>
                                    </li>

                                    <li class="ds2-top-nav__item ds2-top-nav__item--level-3">
                                        <a class="ds2-top-nav__link" href="https://www.leeds.ac.uk/clearing">Clearing 2026</a>
                                    </li>

                                    <li class="ds2-top-nav__item ds2-top-nav__item--level-3">
                                        <a class="ds2-top-nav__link" href="https://courses.leeds.ac.uk/course-search/undergraduate-courses">Undergraduate course search</a>
                                    </li>

                                    <li class="ds2-top-nav__item ds2-top-nav__item--level-3">
                                        <a class="ds2-top-nav__link" href="https://www.leeds.ac.uk/undergraduate-fees/doc/fees-undergraduate-fees">Fees and funding</a>
                                    </li>

                                    <li class="ds2-top-nav__item ds2-top-nav__item--level-3">
                                        <a class="ds2-top-nav__link" href="https://www.leeds.ac.uk/undergraduate-student-life/doc/students-union">Student life</a>
                                    </li>

                                </ul>
                            </div>
                        </li>

                        <li class="ds2-top-nav__item ds2-top-nav__item--level-2 ds2-top-nav__item--has-children">
                            <button class="ds2-top-nav__link ds2-top-nav__link--button" type="button">
                                <span class="ds2-top-nav__link-text">Online courses</span>
                                <span class="ds2-top-nav__arrow" aria-hidden="true"></span>
                            </button>

                            <div class="ds2-top-nav__panel ds2-top-nav__panel--level-3">

                                <button class="ds2-top-nav__back" type="button">
                                    <span class="ds2-top-nav__back-arrow" aria-hidden="true"></span>
                                    <span class="ds2-top-nav__visually-hidden">Back to Study</span>
                                </button>
                                <ul class="ds2-top-nav__list ds2-top-nav__list--level-3">

                                    <li class="ds2-top-nav__item ds2-top-nav__item--level-3">
                                        <a class="ds2-top-nav__link" href="https://www.leeds.ac.uk/online-courses">Overview</a>
                                    </li>

                                    <li class="ds2-top-nav__item ds2-top-nav__item--level-3">
                                        <a class="ds2-top-nav__link" href="https://www.leeds.ac.uk/online-masters-study">Online Masters study</a>
                                    </li>

                                    <li class="ds2-top-nav__item ds2-top-nav__item--level-3">
                                        <a class="ds2-top-nav__link" href="https://www.leeds.ac.uk/short-online-courses">Short online courses</a>
                                    </li>

                                    <li class="ds2-top-nav__item ds2-top-nav__item--level-3">
                                        <a class="ds2-top-nav__link" href="https://www.leeds.ac.uk/online-courses/doc/phd-distance-learning">PhD by distance learning</a>
                                    </li>

                                </ul>
                            </div>
                        </li>

                        <li class="ds2-top-nav__item ds2-top-nav__item--level-2 ds2-top-nav__item--has-children">
                            <button class="ds2-top-nav__link ds2-top-nav__link--button" type="button">
                                <span class="ds2-top-nav__link-text">Masters</span>
                                <span class="ds2-top-nav__arrow" aria-hidden="true"></span>
                            </button>

                            <div class="ds2-top-nav__panel ds2-top-nav__panel--level-3">

                                <button class="ds2-top-nav__back" type="button">
                                    <span class="ds2-top-nav__back-arrow" aria-hidden="true"></span>
                                    <span class="ds2-top-nav__visually-hidden">Back to Study</span>
                                </button>
                                <ul class="ds2-top-nav__list ds2-top-nav__list--level-3">

                                    <li class="ds2-top-nav__item ds2-top-nav__item--level-3">
                                        <a class="ds2-top-nav__link" href="https://www.leeds.ac.uk/masters">Masters home page</a>
                                    </li>

                                    <li class="ds2-top-nav__item ds2-top-nav__item--level-3">
                                        <a class="ds2-top-nav__link" href="https://www.leeds.ac.uk/postgraduate-events">Postgraduate open day</a>
                                    </li>

                                    <li class="ds2-top-nav__item ds2-top-nav__item--level-3">
                                        <a class="ds2-top-nav__link" href="https://courses.leeds.ac.uk/course-search/masters-courses?type=PGT&amp;query=">Masters course search</a>
                                    </li>

                                    <li class="ds2-top-nav__item ds2-top-nav__item--level-3">
                                        <a class="ds2-top-nav__link" href="https://www.leeds.ac.uk/masters-applying/doc/apply-masters-courses">Applying</a>
                                    </li>

                                    <li class="ds2-top-nav__item ds2-top-nav__item--level-3">
                                        <a class="ds2-top-nav__link" href="https://www.leeds.ac.uk/masters-offer/doc/masters-fees-funding">Fees and funding</a>
                                    </li>

                                </ul>
                            </div>
                        </li>

                        <li class="ds2-top-nav__item ds2-top-nav__item--level-2">
                            <a class="ds2-top-nav__link" href="https://www.leeds.ac.uk/research-degrees">Research degrees</a>
                        </li>

                        <li class="ds2-top-nav__item ds2-top-nav__item--level-2">
                            <a class="ds2-top-nav__link" href="https://www.leeds.ac.uk/international-students">International students</a>
                        </li>

                        <li class="ds2-top-nav__item ds2-top-nav__item--level-2">
                            <a class="ds2-top-nav__link" href="https://www.leeds.ac.uk/foundation-courses">Foundation courses</a>
                        </li>

                        <li class="ds2-top-nav__item ds2-top-nav__item--level-2">
                            <a class="ds2-top-nav__link" href="https://www.leeds.ac.uk/mature-students">Mature students</a>
                        </li>

                    </ul>
                </div>
            </li>

            <li class="ds2-top-nav__item ds2-top-nav__item--level-1 ds2-top-nav__item--has-children">
                <button class="ds2-top-nav__link ds2-top-nav__link--button" type="button">
                    <span class="ds2-top-nav__link-text">Faculties</span>
                    <span class="ds2-top-nav__arrow" aria-hidden="true"></span>
                </button>

                <div class="ds2-top-nav__panel ds2-top-nav__panel--level-2">

                    <button class="ds2-top-nav__back" type="button">
                        <span class="ds2-top-nav__back-arrow" aria-hidden="true"></span>
                        <span class="ds2-top-nav__visually-hidden">Back to main menu</span>
                    </button>
                    <ul class="ds2-top-nav__list ds2-top-nav__list--level-2">

                        <li class="ds2-top-nav__item ds2-top-nav__item--level-2">
                            <a class="ds2-top-nav__link" href="https://ahc.leeds.ac.uk/">Arts, Humanities and Cultures</a>
                        </li>

                        <li class="ds2-top-nav__item ds2-top-nav__item--level-2">
                            <a class="ds2-top-nav__link" href="https://biologicalsciences.leeds.ac.uk/">Biological Sciences</a>
                        </li>

                        <li class="ds2-top-nav__item ds2-top-nav__item--level-2">
                            <a class="ds2-top-nav__link" href="https://business.leeds.ac.uk/">Business School</a>
                        </li>

                        <li class="ds2-top-nav__item ds2-top-nav__item--level-2">
                            <a class="ds2-top-nav__link" href="https://eps.leeds.ac.uk/">Engineering and Physical Sciences</a>
                        </li>

                        <li class="ds2-top-nav__item ds2-top-nav__item--level-2">
                            <a class="ds2-top-nav__link" href="https://environment.leeds.ac.uk/">Environment</a>
                        </li>

                        <li class="ds2-top-nav__item ds2-top-nav__item--level-2">
                            <a class="ds2-top-nav__link" href="https://medicinehealth.leeds.ac.uk/">Medicine and Health</a>
                        </li>

                        <li class="ds2-top-nav__item ds2-top-nav__item--level-2">
                            <a class="ds2-top-nav__link" href="https://essl.leeds.ac.uk/">Social Sciences</a>
                        </li>

                        <li class="ds2-top-nav__item ds2-top-nav__item--level-2">
                            <a class="ds2-top-nav__link" href="https://www.leeds.ac.uk/about/doc/faculties-contacts">Faculty contacts</a>
                        </li>

                    </ul>
                </div>
            </li>

            <li class="ds2-top-nav__item ds2-top-nav__item--level-1 ds2-top-nav__item--has-children">
                <button class="ds2-top-nav__link ds2-top-nav__link--button" type="button">
                    <span class="ds2-top-nav__link-text">Research</span>
                    <span class="ds2-top-nav__arrow" aria-hidden="true"></span>
                </button>

                <div class="ds2-top-nav__panel ds2-top-nav__panel--level-2">

                    <button class="ds2-top-nav__back" type="button">
                        <span class="ds2-top-nav__back-arrow" aria-hidden="true"></span>
                        <span class="ds2-top-nav__visually-hidden">Back to main menu</span>
                    </button>
                    <ul class="ds2-top-nav__list ds2-top-nav__list--level-2">

                        <li class="ds2-top-nav__item ds2-top-nav__item--level-2">
                            <a class="ds2-top-nav__link" href="https://www.leeds.ac.uk/research-and-innovation">Research overview</a>
                        </li>

                        <li class="ds2-top-nav__item ds2-top-nav__item--level-2">
                            <a class="ds2-top-nav__link" href="https://www.leeds.ac.uk/research-impact">Research excellence and impact</a>
                        </li>

                        <li class="ds2-top-nav__item ds2-top-nav__item--level-2">
                            <a class="ds2-top-nav__link" href="https://www.leeds.ac.uk/research-strategy-environment">Strategy and environment</a>
                        </li>

                        <li class="ds2-top-nav__item ds2-top-nav__item--level-2">
                            <a class="ds2-top-nav__link" href="https://www.leeds.ac.uk/researcher-staff-development">Researcher and staff development</a>
                        </li>

                        <li class="ds2-top-nav__item ds2-top-nav__item--level-2">
                            <a class="ds2-top-nav__link" href="https://www.leeds.ac.uk/research-public-engagement">Public engagement</a>
                        </li>

                        <li class="ds2-top-nav__item ds2-top-nav__item--level-2">
                            <a class="ds2-top-nav__link" href="https://www.leeds.ac.uk/research-and-innovation/doc/research-innovation-contacts">Research and innovation contacts</a>
                        </li>

                    </ul>
                </div>
            </li>

            <li class="ds2-top-nav__item ds2-top-nav__item--level-1 ds2-top-nav__item--has-children">
                <button class="ds2-top-nav__link ds2-top-nav__link--button" type="button">
                    <span class="ds2-top-nav__link-text">Business</span>
                    <span class="ds2-top-nav__arrow" aria-hidden="true"></span>
                </button>

                <div class="ds2-top-nav__panel ds2-top-nav__panel--level-2">

                    <button class="ds2-top-nav__back" type="button">
                        <span class="ds2-top-nav__back-arrow" aria-hidden="true"></span>
                        <span class="ds2-top-nav__visually-hidden">Back to main menu</span>
                    </button>
                    <ul class="ds2-top-nav__list ds2-top-nav__list--level-2">

                        <li class="ds2-top-nav__item ds2-top-nav__item--level-2">
                            <a class="ds2-top-nav__link" href="https://www.leeds.ac.uk/business-partnerships">Overview</a>
                        </li>

                        <li class="ds2-top-nav__item ds2-top-nav__item--level-2">
                            <a class="ds2-top-nav__link" href="https://www.leeds.ac.uk/business-partnerships-access-expertise/doc/work-with-academics">Access our expertise</a>
                        </li>

                        <li class="ds2-top-nav__item ds2-top-nav__item--level-2">
                            <a class="ds2-top-nav__link" href="https://www.leeds.ac.uk/training-development/doc/cpd-professional-learning-businesses-employers">Training and development</a>
                        </li>

                        <li class="ds2-top-nav__item ds2-top-nav__item--level-2">
                            <a class="ds2-top-nav__link" href="https://www.leeds.ac.uk/business-partnerships/doc/recruit-students">Recruit our students and graduates</a>
                        </li>

                        <li class="ds2-top-nav__item ds2-top-nav__item--level-2">
                            <a class="ds2-top-nav__link" href="https://www.leeds.ac.uk/business-partnerships/doc/use-facilities">Use our facilities</a>
                        </li>

                        <li class="ds2-top-nav__item ds2-top-nav__item--level-2">
                            <a class="ds2-top-nav__link" href="https://www.leeds.ac.uk/business-partnerships/doc/support-small-businesses">Support for small businesses</a>
                        </li>

                        <li class="ds2-top-nav__item ds2-top-nav__item--level-2">
                            <a class="ds2-top-nav__link" href="https://www.leeds.ac.uk/business-partnerships/doc/business-partners">Our business partners</a>
                        </li>

                    </ul>
                </div>
            </li>

            <li class="ds2-top-nav__item ds2-top-nav__item--level-1 ds2-top-nav__item--has-children">
                <button class="ds2-top-nav__link ds2-top-nav__link--button" type="button">
                    <span class="ds2-top-nav__link-text">About</span>
                    <span class="ds2-top-nav__arrow" aria-hidden="true"></span>
                </button>

                <div class="ds2-top-nav__panel ds2-top-nav__panel--level-2">

                    <button class="ds2-top-nav__back" type="button">
                        <span class="ds2-top-nav__back-arrow" aria-hidden="true"></span>
                        <span class="ds2-top-nav__visually-hidden">Back to main menu</span>
                    </button>
                    <ul class="ds2-top-nav__list ds2-top-nav__list--level-2">

                        <li class="ds2-top-nav__item ds2-top-nav__item--level-2">
                            <a class="ds2-top-nav__link" href="https://www.leeds.ac.uk/campusmap">Campus map</a>
                        </li>

                        <li class="ds2-top-nav__item ds2-top-nav__item--level-2">
                            <a class="ds2-top-nav__link" href="https://www.leeds.ac.uk/about/doc/about-history">Our history</a>
                        </li>

                        <li class="ds2-top-nav__item ds2-top-nav__item--level-2">
                            <a class="ds2-top-nav__link" href="https://www.leeds.ac.uk/our-faculties">Faculties</a>
                        </li>

                        <li class="ds2-top-nav__item ds2-top-nav__item--level-2">
                            <a class="ds2-top-nav__link" href="https://www.leeds.ac.uk/structure-governance/doc/governance">Governance</a>
                        </li>

                        <li class="ds2-top-nav__item ds2-top-nav__item--level-2">
                            <a class="ds2-top-nav__link" href="https://www.leeds.ac.uk/structure-governance/doc/reports-accounts-policies">Reports, accounts and policies</a>
                        </li>

                        <li class="ds2-top-nav__item ds2-top-nav__item--level-2">
                            <a class="ds2-top-nav__link" href="https://www.leeds.ac.uk/about/doc/strategies-values">Strategies and values</a>
                        </li>

                        <li class="ds2-top-nav__item ds2-top-nav__item--level-2">
                            <a class="ds2-top-nav__link" href="https://equality.leeds.ac.uk/">Equality</a>
                        </li>

                        <li class="ds2-top-nav__item ds2-top-nav__item--level-2">
                            <a class="ds2-top-nav__link" href="https://www.leeds.ac.uk/university-executive">Executive team</a>
                        </li>

                        <li class="ds2-top-nav__item ds2-top-nav__item--level-2">
                            <a class="ds2-top-nav__link" href="https://www.leeds.ac.uk/university-news">News</a>
                        </li>

                        <li class="ds2-top-nav__item ds2-top-nav__item--level-2">
                            <a class="ds2-top-nav__link" href="https://www.leeds.ac.uk/events/">Events</a>
                        </li>

                    </ul>
                </div>
            </li>

            <li class="ds2-top-nav__item ds2-top-nav__item--level-1">
                <a class="ds2-top-nav__link" href="https://www.leeds.ac.uk/alumni">Alumni</a>
            </li>

        </ul>
    </nav>
</header>
  • Content:
    // Mobile first: the base is the mobile and tablet layout, then the JS-only states
    // under &--js, then one desktop media query at the bottom.
    // State and behaviour are in ds2-top-nav.module.js. Aiming at WCAG 2.2 AA.
    .ds2-top-nav {
      // replace with DS2 colour names when added
      $bgColour: #000;
      $fgColour: #fff;
      $highlightColour: #D9FF2E;
      $searchBorderColour: #999;
    
      // must match desktopMediaQuery in ds2-top-nav.module.js
      // in rem so it follows the reader's text size: 80rem is 1280px at the usual 16px text,
      // or 1600px if they've set 20px, because bigger text needs a wider screen before the row fits.
      // With a px breakpoint, bigger text made the desktop nav overflow between 1280 and about 1500px.
      // (in a media query rem goes by the browser's own setting, not by font-size on html)
      $desktopBreakpoint: 80rem;
    
      // spacing uses the global $spacing-* scale (see /docs/spacing) wherever a value lands on it.
      // what's still in px is either a size rather than spacing, a Figma measurement that isn't on the
      // scale (5px, 15px, 36px, 50px), or spacing that feeds Sass arithmetic alongside a px size:
      // $barHeight and $searchRowOthers add padding to the crest and the icon buttons, and Sass can't
      // add rem to px. Converting those means moving the sums into calc()
      //
      // **To confirm against a clean Figma export:** $linkPaddingInline (36px) is 4px off $spacing-6
      // and was left alone because it sets the text inset on every row and the child rows line up
      // against it. $rowHeight, $searchHeight and the icon sizes are near scale steps too, but they're
      // sizes rather than spacing, so they stay px whatever the number
      //
      // mobile values come from the Figma file, which uses a 390px wide frame
      // values marked (image) were measured from the mobile reference image scaled to 390px, refine them from cleaner exports
    
      // top row, its height comes from the padding and the logo rather than being fixed
      $barPaddingTop: 32px;
      $barPaddingBottom: 24px;
      $barPaddingInline: 24px;
      $logoPadding: 24px;
      $crestWidth: 43.49px;
      $crestHeight: 42.74px;
      // only used to size the open menu below the top row
      $barHeight: $barPaddingTop + $logoPadding * 2 + $crestHeight + $barPaddingBottom;
    
      // icon buttons, the menu button is wider so the burger sits 12px in from its edge like the Figma button group
      $iconButtonSize: 40px;
      $menuButtonSize: 48px;
      // the icon paths have space round them, these sizes give the Figma glyph sizes (search about 15px, close about 15px)
      $searchIconSize: 20px;
      $closeIconSize: 26px;
      $burgerWidth: 24px;
      $burgerHeight: 12px;
    
      // search box in the open menu, flexible width
      $searchHeight: 60px;
      $searchGapInline: 12px;
      $searchRadius: 4px;
      $searchSubmitInset: $spacing-2;
      // was 5px, snapped to the scale: a 1px difference measured off a scaled screenshot
      $searchInputPaddingStart: $spacing-1;
      $searchInputPaddingEnd: $spacing-3;
    
      // menu list: 60px rows with 4px between them, 50px below the top row
      // text in rem so it follows the browser's font size as well as page zoom (WCAG 1.4.4)
      // line height has no unit so it grows with the text
      $linkFontSize: 1rem;
      $linkLineHeight: 1.5;
      $rowHeight: 60px;
      $rowGap: $spacing-1;
      $linkPaddingInline: 36px; // (image)
      // was 50px, and Figma said "about 50", so the 2px to the nearest step is inside that
      $listPaddingTop: $spacing-7;
      $listPaddingBottom: $spacing-6;
    
      // drill-down panels (image), children line up with the level 1 text as the parent row they were indented under isn't built
      $panelPaddingTop: $spacing-5;
      $childRowHeight: 56px;
      $backHeight: 60px;
      $backPaddingInline: $spacing-5;
      $slideDuration: 0.3s;
    
      // tablet (481-1279) uses the mobile layout, these stop the search box and menu rows stretching across wide screens
      // both are wider than anything reached on mobile, so they need no breakpoint
      $searchMaxWidth: 480px;
      $listMaxWidth: 480px;
    
      // when the menu opens the search box slides out leftwards from where the search icon was, like the old component
      // it animates max-width, as a flexible width can't be transitioned; the end value is the box's real width,
      // the screen minus everything else in the open top row, so the slide takes the full time at every screen size
      $searchSlideDuration: 0.4s;
      // what shares the row with the open box: the bar's padding, the close arrow, the box's own margins and the burger.
      // the logo is hidden while the box is open, so it isn't counted
      $searchRowOthers: $barPaddingInline * 2 + $iconButtonSize + $searchGapInline * 2 + $menuButtonSize;
      // the open box's cap. It only binds from 690px up (530 + $searchRowOthers); below that the row
      // is the limit, so a phone is unaffected by this number and there's no tablet media query.
      // 305px was the value from the old design, but it only ever bound from 465px up, which is tablet
      $searchMaxWidthOpen: 530px;
      $searchSlideEndWidth: unquote("min(#{$searchMaxWidthOpen}, calc(100vw - #{$searchRowOthers}))");
    
      // arrow drawn with borders so it stays visible in forced colours, 13 x 11 in Figma
      $arrowWidth: 13px;
      // was 15px measured from the image, so 1px off the scale is inside the measurement error
      $arrowGap: $spacing-4;
      $arrowStroke: 2px;
      $arrowHeadSize: 8px;
    
      // desktop (1280 and up), values from the old ds2-top-nav desktop SCSS and screenshots
      $desktopBarHeight: 80px;
      $desktopMaxWidth: 1400px;
      $desktopPaddingInline: $spacing-4;
      $desktopLogoHeight: 34px;
      $desktopLinkPaddingInline: $spacing-4;
      // narrows towards 1280 (16px at 1440, 12px at 1280) so the nav still fits beside the open
      // search box when a user applies their own text spacing (WCAG 1.4.12), or when labels are longer
      $desktopLinkPaddingFluid: unquote("clamp(10px, calc(2.5vw - 20px), #{$desktopLinkPaddingInline})");
      // between the logo, nav and search; also the smallest gap left between the logo and the nav when search is open
      $desktopColumnGap: $spacing-4;
      $desktopRowHeight: 52px;
      $desktopPanelPaddingTop: $spacing-3;
      $desktopPanelPaddingBottom: $spacing-7;
      $desktopLevel2Width: 33%;
      $desktopLevel2ArrowWidth: 20px;
      $desktopArrowToLevel3Gap: $spacing-5;
      $desktopSearchWidth: 250px;
      // the open box fills the space the links leave, up to this
      $desktopSearchMaxWidth: 800px;
      $desktopSearchHeight: 44px;
      $arrowGrowDuration: 0.4s;
      $arrowTurnDuration: 0.2s;
    
      $focusOutlineWidth: 2px;
      $focusOutlineOffset: 2px;
      $menuZIndex: 100;
      $seamOverlap: 1px;
      $noJsIndent: 24px;
    
      position: relative;
      font-family: uolInter, Arial, Helvetica, sans-serif;
      background: $bgColour;
      color: $fgColour;
    
      // the site doesn't set border-box globally, so full width rows would be their padding wider than the screen
      &,
      *,
      *::before,
      *::after {
        box-sizing: border-box;
      }
    
      // one focus style for the whole header, keyboard focus only (WCAG 2.4.7)
      // the ring is the highlight colour, about 18:1 against the black bar (WCAG 1.4.11 wants 3:1)
      // the search input is left out: its ring goes round the whole search box, see &__search
      // :where keeps the specificity low, so full width rows can move the ring inside themselves
      :where(a, button):focus-visible {
        outline: $focusOutlineWidth solid $highlightColour;
        outline-offset: $focusOutlineOffset;
      }
    
      // top row
    
      &__bar {
        display: flex;
        align-items: center;
        // the controls that fade out while the search box opens are positioned against this
        position: relative;
        // the logo is the tallest thing in the row, so the bar would shrink whenever it's hidden
        // (opening the search box). This holds the height the logo would have given it.
        // $barHeight, not the logo's own height, because box-sizing is border-box here so this
        // has to include the bar's padding
        min-height: $barHeight;
        padding: $barPaddingTop $barPaddingInline $barPaddingBottom;
      }
    
      &__logo {
        display: inline-flex;
        align-items: center;
        margin-right: auto;
        padding: $logoPadding;
        color: inherit;
        fill: currentColor;
      }
    
      &__logo-crest {
        width: $crestWidth;
        height: $crestHeight;
      }
    
      // wordmark only has room on desktop, the crest alone is used below that
      &__logo-wordmark {
        display: none;
        height: $crestHeight;
    
        @media (min-width: $desktopBreakpoint) {
          display: block;
        }
      }
    
      // shared reset for the icon buttons
      &__search-toggle,
      &__search-close,
      &__menu-toggle,
      &__search-submit {
        align-items: center;
        justify-content: center;
        flex: none;
        width: $iconButtonSize;
        height: $iconButtonSize;
        padding: 0;
        border: 0;
        background: none;
        color: inherit;
        cursor: pointer;
    
        // hover colour for mouse users only, so it doesn't stick after a tap on a touch screen
        @media (hover: hover) {
          &:hover {
            color: $highlightColour;
          }
        }
      }
    
      &__menu-toggle {
        width: $menuButtonSize;
        height: $menuButtonSize;
      }
    
      // only shown once the JS is running, see &--js below
      &__search-toggle,
      &__menu-toggle {
        display: none;
      }
    
      // shown only while the search box is open, see &--search-open below
      // it's the one visible way to close the box, which matters on a touch screen where there's no Escape
      &__search-close {
        display: none;
    
        .ds2-top-nav__back-arrow {
          // the arrow is the whole button here, rather than sitting after a label
          margin: 0;
        }
      }
    
      &__search-icon {
        width: $searchIconSize;
        height: $searchIconSize;
        fill: currentColor;
      }
    
      &__menu-icon {
        fill: currentColor;
    
        &--burger {
          width: $burgerWidth;
          height: $burgerHeight;
        }
    
        &--close {
          display: none;
          width: $closeIconSize;
          height: $closeIconSize;
        }
      }
    
      // icon follows the button state rather than a class, so it can't disagree with what is announced
      &__menu-toggle[aria-expanded="true"] {
        .ds2-top-nav__menu-icon--burger {
          display: none;
        }
    
        .ds2-top-nav__menu-icon--close {
          display: block;
        }
      }
    
      // the arrow and the box are one unit on desktop, so they slide together. Everywhere else this
      // wrapper gets out of the way and they behave as direct children of the bar
      &__search-group {
        display: contents;
      }
    
      // search box, shown in the open menu (or always when there is no JS)
    
      &__search {
        display: flex;
        align-items: center;
        flex: 1;
        min-width: 0;
        max-width: $searchMaxWidth;
        height: $searchHeight;
        margin-inline: $searchGapInline;
        border: 1px solid $searchBorderColour;
        border-radius: $searchRadius;
    
        // the input's focus ring goes round the whole box instead of inside it.
        // :has() is the newest thing this component uses: Firefox only got it in December 2023.
        // if it ever isn't supported the ring goes round the input instead, which is only cosmetic
        &:has(.ds2-top-nav__search-input:focus-visible) {
          outline: $focusOutlineWidth solid $highlightColour;
          outline-offset: $focusOutlineOffset;
        }
      }
    
      &__search-input {
        flex: 1;
        min-width: 0;
        height: 100%;
        padding: 0 $searchInputPaddingEnd 0 $searchInputPaddingStart;
        border: 0;
        background: transparent;
        color: inherit;
        font: inherit;
        // 16px or more stops iOS zooming in when the input is focused, 1rem is 16px unless the user has chosen larger
        font-size: $linkFontSize;
    
        &::placeholder {
          color: inherit;
          opacity: 1;
        }
    
        &:focus {
          outline: none;
        }
      }
    
      // submit comes after the input in the source so it's announced in the usual order, but sits on the left as in the design
      &__search-submit {
        display: inline-flex;
        order: -1;
        margin-left: $searchSubmitInset;
      }
    
      // menu
    
      &__list {
        display: flex;
        flex-direction: column;
        gap: $rowGap;
        max-width: $listMaxWidth;
        margin: 0;
        padding: 0;
        list-style: none;
      }
    
      &__list--level-1 {
        padding-block: $listPaddingTop $listPaddingBottom;
      }
    
      &__link {
        display: flex;
        align-items: center;
        width: 100%;
        // min-height, not height, so rows grow rather than clip if the text needs more room
        // and they're well over the 24px minimum tap target (WCAG 2.5.8)
        min-height: $rowHeight;
        padding: 0 $linkPaddingInline;
        border: 0;
        background: none;
        color: inherit;
        font: inherit;
        font-size: $linkFontSize;
        line-height: $linkLineHeight;
        text-align: left;
        text-decoration: none;
        cursor: pointer;
    
        // rows are full width, so the focus ring sits inside them rather than being cut off
        // at the screen edge, where part of it would be invisible (WCAG 2.4.7)
        &:focus-visible {
          outline-offset: -$focusOutlineWidth * 2;
        }
    
        @media (hover: hover) {
          &:hover {
            color: $highlightColour;
          }
        }
      }
    
      // rows inside panels are a little shorter than level 1 rows
      &__panel &__link {
        min-height: $childRowHeight;
      }
    
      &__list--level-2,
      &__list--level-3 {
        padding-bottom: $listPaddingBottom;
      }
    
      // back buttons need the JS, they're shown in &--js below
      &__back {
        display: none;
        align-items: center;
        height: $backHeight;
        padding: 0 $backPaddingInline;
        border: 0;
        background: none;
        color: $highlightColour;
        cursor: pointer;
    
        // sits against the screen edge, so the focus ring goes inside it
        &:focus-visible {
          outline-offset: -$focusOutlineWidth * 2;
        }
      }
    
      // arrows are drawn with borders in currentColor, not backgrounds: Windows high contrast
      // removes backgrounds but keeps borders, which is what left the old component with a
      // stray chevron and no line (supports WCAG 1.4.11)
      &__arrow,
      &__back-arrow {
        position: relative;
        flex: none;
        width: $arrowWidth;
        border-top: $arrowStroke solid currentColor;
    
        // head is a square with two borders turned 45deg, its corner lands on the end of the line
        &::after {
          content: '';
          position: absolute;
          top: -($arrowStroke / 2) - ($arrowHeadSize / 2);
          right: $arrowStroke / 2;
          width: $arrowHeadSize;
          height: $arrowHeadSize;
          border-top: $arrowStroke solid currentColor;
          border-right: $arrowStroke solid currentColor;
          transform: rotate(45deg);
        }
      }
    
      &__arrow {
        margin-left: $arrowGap;
      }
    
      // same arrow, pointing left
      &__back-arrow {
        transform: scaleX(-1);
      }
    
      // without JS every level is shown as one long list: the arrows would suggest a drill-down that can't happen,
      // so they're hidden, and child lists are indented so the structure is still clear
      &:not(.ds2-top-nav--js) {
        .ds2-top-nav__arrow {
          display: none;
        }
    
        .ds2-top-nav__list--level-2,
        .ds2-top-nav__list--level-3 {
          padding-left: $noJsIndent;
        }
      }
    
      // with JS the menu starts closed: search and nav are hidden behind the icon buttons
      // without JS they stay visible so the links and search still work
      &--js {
        .ds2-top-nav__search-toggle,
        .ds2-top-nav__menu-toggle {
          display: inline-flex;
        }
    
        .ds2-top-nav__search,
        .ds2-top-nav__nav {
          display: none;
        }
    
        // menu covers the page below the top row, at least to the bottom of the screen
        // it starts 1px up, over the top row's empty bottom padding, so no line of the page shows through where the two meet
        // on scaled displays (125%, 150%), padding makes up the 1px so the content doesn't move
        .ds2-top-nav__nav {
          position: absolute;
          top: calc(100% - #{$seamOverlap});
          left: 0;
          right: 0;
          z-index: $menuZIndex;
          min-height: calc(100vh - #{$barHeight - $seamOverlap});
          min-height: calc(100dvh - #{$barHeight - $seamOverlap});
          padding-top: $seamOverlap;
          background: $bgColour;
          // clip rather than hidden, so off-screen panels can't cause sideways scrolling
          // and the page can still scroll down to reach a long menu, including at 400% zoom (WCAG 1.4.10).
          // Safari only supports clip from 16. Falling back to visible would let the closed panels
          // off to the right cause sideways scrolling
          overflow-x: clip;
        }
    
        .ds2-top-nav__back {
          display: inline-flex;
        }
    
        // drill-down: each panel slides in from the right over the list it belongs to
        // level 2 panels are positioned against the nav, level 3 panels against their level 2 panel
        .ds2-top-nav__panel {
          position: absolute;
          // arrows are positioned too, without this the arrows of later items in the list paint on top of the panel
          z-index: 1;
          top: 0;
          left: 0;
          width: 100%;
          // closed panels are cut to the menu's height so their content doesn't make the page scroll further
          height: 100%;
          overflow: hidden;
          padding-top: $panelPaddingTop;
          background: $bgColour;
          transform: translateX(100%);
          // hidden once it has slid out, so nothing in it can be focused or read out
          // (visibility, not just moving it off screen, which would leave it reachable) (WCAG 2.4.11, 4.1.2)
          visibility: hidden;
          transition: transform $slideDuration ease, visibility 0s linear $slideDuration;
        }
    
        .ds2-top-nav__panel--open {
          height: auto;
          min-height: 100%;
          overflow: visible;
          transform: none;
          visibility: visible;
          transition: transform $slideDuration ease;
        }
    
        // panels appear without sliding for anyone who has asked for less motion
        // (WCAG 2.3.3 is AAA, but this costs nothing and helps with vestibular disorders)
        @media (prefers-reduced-motion: reduce) {
          .ds2-top-nav__panel,
          .ds2-top-nav__panel--open {
            transition: none;
          }
        }
      }
    
      // open menu, mobile and tablet
      // the open box spans the bar: the close arrow sits where the logo was, the burger stays at the right
      &--menu-open {
        .ds2-top-nav__nav {
          display: block;
        }
      }
    
      // the search box belongs to the search icon, not the menu: the burger opens the menu with the icon still showing
      // --search-closing holds this layout for the length of the animation on the way out, because a
      // closed box is display: none and so has nothing to animate from. The JS takes the class off at the end
      &--search-open,
      &--search-closing {
    
        // the arrow belongs to the box: it sits against its left edge and moves with it as it grows,
        // rather than floating alone at the edge of the bar. This auto margin and the box's one
        // split the free space, so the pair is centred and the burger keeps the right-hand end
        .ds2-top-nav__search-close {
          display: inline-flex;
          margin-left: auto;
        }
    
    
        .ds2-top-nav__search {
          display: flex;
          max-width: $searchSlideEndWidth;
          // the gap to the arrow stays fixed, the space to the burger is free, so the arrow and box
          // stay together and the pair grows outwards from the middle
          margin-inline: $searchGapInline auto;
          // the input and icon stay inside the box while it's narrow
          overflow: hidden;
        }
      }
    
      // settled open: the logo and the icon are gone for good
      // display: none, not visibility, so they leave the tab order with it
      &--search-open {
        .ds2-top-nav__logo,
        .ds2-top-nav__search-toggle {
          display: none;
        }
    
        .ds2-top-nav__search {
          animation: ds2-top-nav-search-grow $searchSlideDuration ease;
    
          @media (prefers-reduced-motion: reduce) {
            animation: none;
          }
        }
      }
    
      // while the box is opening or closing, the logo and the search icon cross-fade with the arrow.
      // position: absolute with no offsets leaves them exactly where they sit but takes them out of the
      // flow, so the box has its room straight away and nothing jumps as they go.
      // these rules come after &--search-open so they win where both classes are on the header
      &--search-opening,
      &--search-closing {
        .ds2-top-nav__logo,
        .ds2-top-nav__search-toggle {
          display: inline-flex;
          // each one needs its own offsets: an absolutely positioned child of a flex container is put
          // at the container's start corner, not where it would have sat, so without these the search
          // icon jumps across the bar and lands on top of the logo.
          // the block offsets plus auto margins centre them the way align-items did
          position: absolute;
          top: $barPaddingTop;
          bottom: $barPaddingBottom;
          margin-block: auto;
          // they're on their way out or not properly back yet, so they take no clicks
          pointer-events: none;
        }
    
        .ds2-top-nav__logo {
          left: $barPaddingInline;
        }
    
        // its right edge lands where the burger's left edge is, which is where it sits in the flow
        .ds2-top-nav__search-toggle {
          right: $barPaddingInline + $menuButtonSize;
        }
      }
    
      &--search-opening {
        .ds2-top-nav__logo,
        .ds2-top-nav__search-toggle {
          animation: ds2-top-nav-fade-out $searchSlideDuration ease forwards;
        }
    
        .ds2-top-nav__search-close {
          animation: ds2-top-nav-fade-in $searchSlideDuration ease;
        }
      }
    
      // the same animation backwards. Nothing in here is interactive: focus has already gone back to
      // the icon and the box is on its way out, so a click or a tab must not land in it
      &--search-closing {
        .ds2-top-nav__logo,
        .ds2-top-nav__search-toggle {
          animation: ds2-top-nav-fade-in $searchSlideDuration ease;
        }
    
        .ds2-top-nav__search-close {
          pointer-events: none;
          animation: ds2-top-nav-fade-out $searchSlideDuration ease forwards;
        }
    
        .ds2-top-nav__search {
          pointer-events: none;
          // its own keyframe rather than the grow one reversed: same name means the browser treats it
          // as the animation that has already finished and never restarts it.
          // forwards holds the end state until the JS takes the class off, so it can't flash back to full width
          animation: ds2-top-nav-search-shrink $searchSlideDuration ease forwards;
        }
      }
    
      // desktop, 1280 and up: one row with the logo, nav and search, and a mega-menu dropdown
      // only with JS, as without it the dropdowns couldn't open, so the plain stacked lists are kept instead
      @media (min-width: $desktopBreakpoint) {
        &--js {
          // content lines up to a centred column up to 1400px wide, the background stays full width
          $contentInset: unquote("max(#{$desktopPaddingInline}, calc((100% - #{$desktopMaxWidth}) / 2))");
    
          // the top row's children join this grid (the bar uses display: contents)
          // the nav is right-aligned against the search column, which is only as wide as the icon or the open search box,
          // so opening search pushes the links along towards the logo
          display: grid;
          // equal side columns, so the links are centred on the page rather than in whatever space is
          // left between the logo and the search. The logo is much wider than the search icon, so
          // centring them in the leftover space would sit them noticeably right of centre
          grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
          column-gap: $desktopColumnGap;
          align-items: center;
          min-height: $desktopBarHeight;
          padding-inline: $contentInset;
    
          .ds2-top-nav__bar {
            display: contents;
          }
    
          .ds2-top-nav__logo {
            grid-column: 1;
            grid-row: 1;
            justify-self: start;
            margin: 0;
            padding: 0;
          }
    
          .ds2-top-nav__logo-crest {
            width: $desktopLogoHeight;
            height: $desktopLogoHeight;
          }
    
          .ds2-top-nav__logo-wordmark {
            height: $desktopLogoHeight;
          }
    
          .ds2-top-nav__menu-toggle {
            display: none;
          }
    
          // search: an icon at the right that expands into the search box
          .ds2-top-nav__search-toggle {
            grid-column: 3;
            grid-row: 1;
            justify-self: end;
          }
    
          .ds2-top-nav__search {
            display: none;
            grid-column: 3;
            grid-row: 1;
            justify-self: end;
            width: $desktopSearchWidth;
            max-width: none;
            height: $desktopSearchHeight;
            margin: 0;
          }
    
          // nav: level 1 in a row, centred in its own column
          .ds2-top-nav__nav {
            display: block;
            position: static;
            grid-column: 2;
            grid-row: 1;
            justify-self: center;
            min-height: 0;
            padding: 0;
            background: none;
            overflow: visible;
          }
    
          .ds2-top-nav__list--level-1 {
            flex-direction: row;
            gap: 0;
            max-width: none;
            padding: 0;
    
            > .ds2-top-nav__item > .ds2-top-nav__link {
              width: auto;
              // full height of the bar so the whole bar is the hover area, and the dropdown starts right below it
              min-height: $desktopBarHeight;
              padding: 0 $desktopLinkPaddingFluid;
    
              &[aria-expanded="true"] {
                color: $highlightColour;
              }
    
              // points down when closed and up when open
              > .ds2-top-nav__arrow {
                transform: rotate(90deg);
                transition: transform $arrowTurnDuration ease;
    
                @media (prefers-reduced-motion: reduce) {
                  transition: none;
                }
              }
    
              &[aria-expanded="true"] > .ds2-top-nav__arrow {
                transform: rotate(-90deg);
              }
            }
          }
    
          // no back buttons on desktop, level 2 and 3 show side by side
          .ds2-top-nav__back {
            display: none;
          }
    
          // level 2: a full width dropdown below the bar, positioned against the header
          .ds2-top-nav__panel--level-2 {
            display: none;
            position: absolute;
            z-index: $menuZIndex;
            top: 100%;
            left: 0;
            right: 0;
            width: auto;
            height: auto;
            min-height: 0;
            overflow: visible;
            padding: $desktopPanelPaddingTop $contentInset $desktopPanelPaddingBottom;
            transform: none;
            visibility: visible;
            transition: none;
    
            // in forced colours the black background becomes the page colour, so the dropdown
            // would merge into the page below it. This border gives it a visible edge (supports WCAG 1.4.11)
            @media (forced-colors: active) {
              border-bottom: 1px solid CanvasText;
            }
          }
    
          // level 3: beside the level 2 column, positioned against the level 2 list
          .ds2-top-nav__panel--level-3 {
            display: none;
            position: absolute;
            top: 0;
            left: 100%;
            width: 100%;
            height: auto;
            min-height: 0;
            overflow: visible;
            padding: 0;
            background: none;
            transform: none;
            visibility: visible;
            transition: none;
          }
    
          .ds2-top-nav__panel--open {
            display: block;
          }
    
          .ds2-top-nav__list--level-2 {
            position: relative;
            gap: 0;
            width: $desktopLevel2Width;
            max-width: none;
            padding: 0;
          }
    
          .ds2-top-nav__list--level-3 {
            gap: 0;
            max-width: none;
            padding: 0;
          }
    
          .ds2-top-nav__panel .ds2-top-nav__link {
            min-height: $desktopRowHeight;
            padding: 0;
    
            // the dropdown has room round it, so the focus ring can go outside the row like everywhere else
            &:focus-visible {
              outline-offset: $focusOutlineOffset;
            }
    
            &[aria-expanded="true"] {
              color: $highlightColour;
            }
          }
    
          // level 2 items with children: a highlight coloured arrow whose line grows across to the level 3 column when open,
          // and on hover too unless hover has been turned off, when it would point at a panel that isn't going to open
          .ds2-top-nav__list--level-2 > .ds2-top-nav__item > .ds2-top-nav__link {
            padding-right: $desktopArrowToLevel3Gap;
    
            .ds2-top-nav__link-text {
              flex: none;
            }
    
            > .ds2-top-nav__arrow {
              flex: 0 1 $desktopLevel2ArrowWidth;
              color: $highlightColour;
              transition: flex-grow $arrowGrowDuration ease;
    
              @media (prefers-reduced-motion: reduce) {
                transition: none;
              }
            }
    
            &[aria-expanded="true"] > .ds2-top-nav__arrow {
              flex-grow: 1;
            }
    
          }
    
          // the growth on hover follows the same attribute the JS reads, and has to sit out here so
          // the :not() lands on the header itself. Nested inside the rule above it would compile to a
          // descendant selector, and since --js is on that same element it could never match.
          // with hover off the arrow grows only for an open panel, never towards empty space
          &:not([data-desktop-hover="false"]) {
            @media (hover: hover) {
              .ds2-top-nav__list--level-2 > .ds2-top-nav__item > .ds2-top-nav__link:hover > .ds2-top-nav__arrow {
                flex-grow: 1;
              }
            }
          }
    
        }
    
        // desktop: the open box fills the space the nav links leave, so it grows by max-width
        // display is set again here because the rule hiding it above sits later in the file than the shared search-open rule
        &--search-open,
        &--search-closing {
          // logo | everything else, replacing logo | nav | search. The arrow and the box share that
          // second column as one flex row, so they move together.
          // this rule sits after &--js, which sets the three columns, and both classes are on the
          // same element
          grid-template-columns: auto minmax(0, 1fr);
    
          .ds2-top-nav__search-toggle {
            display: none;
          }
    
          // the logo stays on desktop: there's room for it, and it's the nav links that make way
          .ds2-top-nav__logo {
            display: inline-flex;
          }
    
          // the links give up their column to the box
          // display: none, not visibility, so they leave the tab order rather than being invisible and still reachable
          .ds2-top-nav__nav {
            display: none;
          }
    
          // the arrow and the box share one flex row, spanning the slack column and the box's own,
          // and it's packed to the right. That's what makes the box open leftwards out of the icon
          // and the arrow travel with its left edge instead of waiting at a fixed column boundary
          .ds2-top-nav__search-group {
            display: flex;
            align-items: center;
            justify-content: flex-end;
            gap: $desktopColumnGap;
            grid-column: 2;
            grid-row: 1;
          }
    
          .ds2-top-nav__search-close {
            // it's a flex item here, not a grid one
            grid-column: auto;
            justify-self: auto;
          }
    
          // takes the room the links left, up to its cap, and no further
          .ds2-top-nav__search {
            display: flex;
            flex: 1;
            width: auto;
            max-width: $desktopSearchMaxWidth;
            justify-self: auto;
            margin-inline: 0;
          }
        }
    
        &--search-open .ds2-top-nav__search {
          animation: ds2-top-nav-desktop-search-grow $searchSlideDuration ease;
    
          @media (prefers-reduced-motion: reduce) {
            animation: none;
          }
        }
    
        &--search-closing .ds2-top-nav__search {
          animation: ds2-top-nav-desktop-search-shrink $searchSlideDuration ease forwards;
        }
    
        // desktop takes no part in the cross-fade: the offsets above are measured from the mobile bar,
        // and here the bar is display: contents so there is nothing to position against
        &--search-opening,
        &--search-closing {
          .ds2-top-nav__logo {
            position: static;
            inset: auto;
            margin-block: 0;
            animation: none;
            pointer-events: auto;
          }
        }
    
        // on the way out the icon comes back straight away, in the same cell as the shrinking box and
        // against the same right edge, so the box appears to collapse into it.
        // it has to be focusable by then: closing returns focus here, and a display: none element
        // can't take focus, which left focus on the body
        &--search-closing {
          .ds2-top-nav__search-toggle {
            display: inline-flex;
            grid-column: 2;
            grid-row: 1;
            justify-self: end;
          }
        }
    
        // opening is the other way round: it goes as the box arrives
        &--search-opening {
          .ds2-top-nav__search-toggle {
            display: none;
          }
        }
      }
    
      // search box slides, see $searchSlideDuration
      // the build's CSS minifier renames keyframes (e.g. to "j"), so search the compiled CSS for the animation rule, not these names
      // separate names on purpose: an animation with the same name on the same element isn't restarted
      @keyframes ds2-top-nav-fade-out {
        from {
          opacity: 1;
        }
    
        to {
          opacity: 0;
        }
      }
    
      @keyframes ds2-top-nav-fade-in {
        from {
          opacity: 0;
        }
    
        to {
          opacity: 1;
        }
      }
    
      // the way back out. It can't be the grow keyframe reversed: an animation with the same name
      // on the same element isn't restarted, so nothing would happen
      @keyframes ds2-top-nav-search-shrink {
        from {
          max-width: $searchSlideEndWidth;
          opacity: 1;
        }
    
        67% {
          opacity: 1;
        }
    
        to {
          max-width: $iconButtonSize;
          opacity: 0;
        }
      }
    
      // grows outwards from the middle of the bar, because the box is centred by its auto margins.
      // the fade is over the first third, so it doesn't start life as a bare sliver of border
      @keyframes ds2-top-nav-search-grow {
        from {
          max-width: $iconButtonSize;
          opacity: 0;
        }
    
        33% {
          opacity: 1;
        }
    
        to {
          max-width: $searchSlideEndWidth;
          opacity: 1;
        }
      }
    
      // desktop: the box has a fixed width, so its width can be animated directly
      // the open desktop box has no fixed width to animate to, so it grows by max-width like the mobile one
      @keyframes ds2-top-nav-desktop-search-grow {
        from {
          max-width: $iconButtonSize;
        }
    
        to {
          max-width: $desktopSearchMaxWidth;
        }
      }
    
      @keyframes ds2-top-nav-desktop-search-shrink {
        from {
          max-width: $desktopSearchMaxWidth;
        }
    
        to {
          max-width: $iconButtonSize;
        }
      }
    
      // text for screen readers only: it names the icon buttons and says where each back button goes
      // display: none would hide it from them too, so it stays in the page (WCAG 1.1.1, 2.4.6)
      // kept inside the component so it has no global dependency
      &__visually-hidden {
        position: absolute !important;
        width: 1px;
        height: 1px;
        margin: -1px;
        padding: 0;
        overflow: hidden;
        clip-path: inset(50%);
        white-space: nowrap;
        border: 0;
      }
    }
    
  • URL: /components/raw/ds2-top-nav/_ds2-top-nav.scss
  • Filesystem Path: src/library/02-ds2-components/ds2-top-nav/_ds2-top-nav.scss
  • Size: 34.5 KB
  • Content:
    // Burger menu with drill-down panels up to 80rem, mega-menu above it.
    // This file holds the state; how each state looks is in _ds2-top-nav.scss.
    // The buttons are disclosures with aria-expanded, not ARIA menus, and hover is
    // always an extra on top of click and keyboard. Aiming at WCAG 2.2 AA.
    
    // must match $desktopBreakpoint in the SCSS
    // in rem so it moves with the reader's text size: 80rem is 1280px at the usual 16px text
    const desktopMediaQuery = '(min-width: 80rem)';
    
    const openMenuClass = 'ds2-top-nav--menu-open';
    // the search box shows only in this state, on every screen size
    const openSearchClass = 'ds2-top-nav--search-open';
    const openPanelClass = 'ds2-top-nav__panel--open';
    // held for the length of the closing animation, because a closed search box is display: none
    // and so has nothing to animate out of
    const closingSearchClass = 'ds2-top-nav--search-closing';
    // its opposite number: held while the box opens, so the logo and the search icon can fade out
    // instead of blinking away
    const openingSearchClass = 'ds2-top-nav--search-opening';
    
    // desktop hover timings in ms. The open delay stops dropdowns flashing as the pointer passes,
    // and level 2 waits longer so moving diagonally to the level 3 links doesn't switch item.
    // The close delay lets the pointer slip off and come back, which also keeps hover content
    // hoverable and persistent (WCAG 1.4.13).
    const hoverOpenDelay = 100;
    const hoverOpenDelayLevel2 = 200;
    const hoverCloseDelay = 300;
    
    // must match $searchSlideDuration in the SCSS
    const searchSlideDuration = 400;
    
    export const ds2Header = () => {
      const desktopQuery = window.matchMedia(desktopMediaQuery);
    
      document.querySelectorAll('.ds2-top-nav').forEach((header) => {
        // don't set up the same header twice if the init function is called again
        if (header.classList.contains('ds2-top-nav--js')) return;
    
        initTopNav(header, desktopQuery);
      });
    };
    
    const initTopNav = (header, desktopQuery) => {
      const nav = header.querySelector('.ds2-top-nav__nav');
      if (!nav) return;
    
      const menuToggle = header.querySelector('.ds2-top-nav__menu-toggle');
      const searchToggle = header.querySelector('.ds2-top-nav__search-toggle');
      const searchClose = header.querySelector('.ds2-top-nav__search-close');
    
      // desktop dropdowns open on hover as well as click unless the markup says otherwise.
      // the attribute is only written when it's off, so anything without it keeps hover.
      // it's read per header, so two headers on a page can differ
      const hoverOpens = header.dataset.desktopHover !== 'false';
    
      // where the nav sits in the markup, so it can be put back exactly as it was below the breakpoint.
      // it's a child of the header, while the logo and the search sit inside the bar, so moving it
      // between them means moving it into the bar
      const bar = header.querySelector('.ds2-top-nav__bar');
      const navHome = nav.nextElementSibling;
      const searchGroup = header.querySelector('.ds2-top-nav__search-group');
      const searchForm = header.querySelector('.ds2-top-nav__search');
      const searchInput = header.querySelector('.ds2-top-nav__search-input');
    
      // each disclosure button in the nav controls one panel: the panel is the button's next sibling.
      // pairing them here, rather than in the markup, keeps every id and aria-controls out of the
      // template, so an implementation in another templating language has only the structure to
      // reproduce, with nothing to number. The markup carries no ARIA for these buttons at all,
      // which is also the honest state without JS: nothing can open, so nothing claims it can
      const panelFor = new Map();
      const buttonFor = new Map();
    
      // ids have to be unique on the page. idPrefix in the config is meant to keep two headers apart,
      // but two left on the default would share every id, which breaks aria-controls and the label,
      // and is a duplicate-id failure in its own right. Rather than rely on remembering, anything
      // already taken is given a suffix here.
      //
      // which of the two keeps the markup's id isn't fixed: whichever is set up first sees the other's
      // copy and moves off it. That's fine, because nothing outside the component depends on these ids,
      // and it only happens at all when they were already clashing
      const freeId = (wanted) => {
        if (!document.getElementById(wanted)) return wanted;
    
        let n = 2;
        while (document.getElementById(`${wanted}-${n}`)) n += 1;
    
        return `${wanted}-${n}`;
      };
    
      // an id the markup set, which another element on the page has already claimed
      const isTaken = (el) => el && el.id
        && Array.from(document.querySelectorAll(`[id="${el.id}"]`)).some((other) => other !== el);
    
      // rename it and mend whatever pointed at it
      const rename = (el, pointers) => {
        if (!isTaken(el)) return;
    
        const to = freeId(el.id);
        el.id = to;
        pointers.forEach(({ element, attribute }) => {
          if (element) element.setAttribute(attribute, to);
        });
      };
    
      rename(nav, [{ element: menuToggle, attribute: 'aria-controls' }]);
      rename(searchForm, [{ element: searchToggle, attribute: 'aria-controls' }]);
      rename(searchInput, [{ element: header.querySelector('.ds2-top-nav__search-label'), attribute: 'for' }]);
    
      // the panels are numbered from the nav's id, so this reads it after any renaming above
      const idBase = nav.id || `${header.id || 'ds2-top-nav'}-menu`;
      let panelNumber = 0;
    
      Array.from(nav.querySelectorAll('.ds2-top-nav__item > button')).forEach((button) => {
        const panel = button.nextElementSibling;
    
        if (panel && panel.classList.contains('ds2-top-nav__panel')) {
          panelFor.set(button, panel);
          buttonFor.set(panel, button);
    
          panelNumber += 1;
          if (!panel.id) panel.id = freeId(`${idBase}-panel-${panelNumber}`);
          button.setAttribute('aria-controls', panel.id);
    
          // the state lives on the button, not the panel (WCAG 4.1.2)
          // it isn't in the markup, so without JS nothing is announced as collapsed while its links show
          button.setAttribute('aria-expanded', 'false');
        }
      });
    
      // panels opened by hovering, so only these close again when the pointer leaves
      // a clicked panel stays open until it's clicked again, Escape, or a click outside
      const openedByHover = new Set();
      const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
      let searchClosingTimer;
      const hoverTimers = new Map();
    
      const isDesktop = () => desktopQuery.matches;
      const isMenuOpen = () => header.classList.contains(openMenuClass);
      const isSearchOpen = () => header.classList.contains(openSearchClass);
      const isExpanded = (button) => button.getAttribute('aria-expanded') === 'true';
      const isTopLevel = (button) => !button.closest('.ds2-top-nav__panel');
      const openButtons = () => Array.from(panelFor.keys()).filter(isExpanded);
    
      // the disclosure button that belongs directly to an item, not one in a nested panel
      const itemButton = (item) => {
        const button = item.querySelector(':scope > button');
        return button && panelFor.has(button) ? button : null;
      };
    
      const clearHoverTimer = (button) => {
        clearTimeout(hoverTimers.get(button));
        hoverTimers.delete(button);
      };
    
      // on mobile and tablet a panel slides over the list it came from, so what it covers is made inert:
      // the other items, the button that opened it, and the back button of the panel above.
      // Otherwise keyboard and screen reader users reach items hidden behind the panel (WCAG 2.4.11, 4.1.2).
      // inert goes on the siblings because a descendant can't undo it.
      // inert is the one modern feature here with an accessibility consequence rather than a cosmetic
      // one: without it, a keyboard or screen reader user can reach the links behind an open panel.
      // Firefox only got it in April 2023, so it's the thing to check if that ever starts happening
      const setCoveredInert = (button, covered) => {
        const item = button.closest('.ds2-top-nav__item');
        if (!item || !item.parentElement) return;
    
        Array.from(item.parentElement.children).forEach((sibling) => {
          if (sibling !== item) sibling.inert = covered;
        });
        button.inert = covered;
    
        const parentPanel = item.closest('.ds2-top-nav__panel');
        const parentBack = parentPanel && parentPanel.querySelector(':scope > .ds2-top-nav__back');
        if (parentBack) parentBack.inert = covered;
      };
    
      const closePanel = (button) => {
        const panel = panelFor.get(button);
    
        // close nested panels first so nothing is left open inside a closed panel
        Array.from(panel.querySelectorAll('button[aria-expanded="true"]')).forEach((nestedButton) => {
          if (panelFor.has(nestedButton)) closePanel(nestedButton);
        });
    
        clearHoverTimer(button);
        openedByHover.delete(button);
        button.setAttribute('aria-expanded', 'false');
        panel.classList.remove(openPanelClass);
        setCoveredInert(button, false);
      };
    
      const closeAllPanels = () => {
        openButtons().forEach(closePanel);
      };
    
      const openPanel = (button) => {
        const panel = panelFor.get(button);
        const item = button.closest('.ds2-top-nav__item');
    
        // only one panel open per list
        if (item && item.parentElement) {
          Array.from(item.parentElement.children).forEach((sibling) => {
            const siblingButton = itemButton(sibling);
    
            if (sibling !== item && siblingButton && isExpanded(siblingButton)) {
              closePanel(siblingButton);
            }
          });
        }
    
        button.setAttribute('aria-expanded', 'true');
        panel.classList.add(openPanelClass);
    
        // the panel slides over the button, so focus moves into it (WCAG 2.4.3)
        // leaving focus on the covered button would strand keyboard users behind the panel
        if (!isDesktop()) {
          setCoveredInert(button, true);
    
          // fall back to the back button, so focus never lands on the button just made inert
          const firstItem = panel.querySelector(':scope > .ds2-top-nav__list a[href], :scope > .ds2-top-nav__list button')
            || panel.querySelector(':scope > .ds2-top-nav__back');
    
          if (firstItem) firstItem.focus();
        }
      };
    
      // the icon's aria-expanded says whether the box is showing (WCAG 4.1.2)
      const syncSearchToggle = () => {
        if (searchToggle) searchToggle.setAttribute('aria-expanded', String(isSearchOpen()));
      };
    
      // the icon expands into the search box
      // on desktop the box takes the place of the nav links, so any open dropdown goes with them
      const setSearchOpen = (open) => {
        if (open && isDesktop()) closeAllPanels();
    
        const wasOpen = isSearchOpen();
    
        header.classList.toggle(openSearchClass, open);
        syncSearchToggle();
    
        // play the opening animation backwards on the way out, then let the box go back to display: none.
        // reduced motion skips it, so the box doesn't linger for no reason
        clearTimeout(searchClosingTimer);
    
        header.classList.remove(openingSearchClass);
    
        if (!open && wasOpen && !reducedMotion.matches) {
          header.classList.add(closingSearchClass);
          // it's on its way out, so nothing in it should take focus in the meantime
          if (searchForm) searchForm.inert = true;
    
          searchClosingTimer = setTimeout(() => {
            header.classList.remove(closingSearchClass);
            if (searchForm) searchForm.inert = false;
          }, searchSlideDuration);
        } else if (open && !wasOpen && !reducedMotion.matches) {
          header.classList.remove(closingSearchClass);
          if (searchForm) searchForm.inert = false;
    
          header.classList.add(openingSearchClass);
          searchClosingTimer = setTimeout(() => {
            header.classList.remove(openingSearchClass);
          }, searchSlideDuration);
        } else {
          header.classList.remove(closingSearchClass);
          if (searchForm) searchForm.inert = false;
        }
      };
    
      // the menu and the search box are independent: the burger opens and closes the menu and nothing else.
      // closing the menu used to put the search box away too, which was left over from when the search icon
      // opened the menu as well. It hasn't since 2026-09-14
      const setMenuOpen = (open) => {
        header.classList.toggle(openMenuClass, open);
        if (menuToggle) menuToggle.setAttribute('aria-expanded', String(open));
    
        if (!open) closeAllPanels();
      };
    
      // clicking or tabbing away only closes search if nothing has been typed, so text isn't hidden
      const closeSearchIfEmpty = () => {
        if (isSearchOpen() && !(searchInput && searchInput.value)) setSearchOpen(false);
      };
    
      // close everything, used for clicking outside and focus leaving the header
      const closeAll = () => {
        hoverTimers.forEach((timer) => clearTimeout(timer));
        hoverTimers.clear();
        closeAllPanels();
        setMenuOpen(false);
        closeSearchIfEmpty();
      };
    
      panelFor.forEach((panel, button) => {
        button.addEventListener('click', () => {
          clearHoverTimer(button);
    
          if (isExpanded(button)) {
            // a click on an open panel always closes it, however it was opened
            // (a hover-opened panel used to stay open when clicked; removed 2026-09-21)
            closePanel(button);
          } else {
            openPanel(button);
          }
        });
    
        // desktop hover is mouse only, so a tap is always a click and nothing needs hover to work
        // (pointerenter fires for touch too, hence the check)
        const item = button.closest('.ds2-top-nav__item');
        if (!item) return;
    
        // the hover handlers aren't attached at all when hover is off, rather than bailing out inside
        // them, so nothing can half-happen and openedByHover is simply never filled
        if (!hoverOpens) return;
    
        item.addEventListener('pointerenter', (event) => {
          if (event.pointerType !== 'mouse' || !isDesktop()) return;
    
          clearHoverTimer(button);
          if (isExpanded(button)) return;
    
          hoverTimers.set(button, setTimeout(() => {
            hoverTimers.delete(button);
            openPanel(button);
            openedByHover.add(button);
          }, isTopLevel(button) ? hoverOpenDelay : hoverOpenDelayLevel2));
        });
    
        item.addEventListener('pointerleave', (event) => {
          if (event.pointerType !== 'mouse' || !isDesktop()) return;
    
          clearHoverTimer(button);
    
          // level 3 stays open after the pointer leaves its level 2 item, until another item is chosen or the dropdown closes
          if (isTopLevel(button) && openedByHover.has(button)) {
            hoverTimers.set(button, setTimeout(() => {
              hoverTimers.delete(button);
              if (openedByHover.has(button)) closePanel(button);
            }, hoverCloseDelay));
          }
        });
      });
    
      // back buttons return to the panel above and focus the button that opened this one,
      // so people carry on from where they were rather than at the top of the menu (WCAG 2.4.3)
      Array.from(nav.querySelectorAll('.ds2-top-nav__back')).forEach((backButton) => {
        const panel = backButton.closest('.ds2-top-nav__panel');
        const button = panel && buttonFor.get(panel);
        if (!button) return;
    
        backButton.addEventListener('click', () => {
          closePanel(button);
          button.focus();
        });
      });
    
      if (menuToggle) {
        menuToggle.addEventListener('click', () => {
          setMenuOpen(!isMenuOpen());
        });
      }
    
      if (searchToggle) {
        searchToggle.addEventListener('click', () => {
          // the icon only opens the search box, it never opens the menu
          setSearchOpen(true);
    
          if (searchInput) searchInput.focus();
        });
      }
    
      // closing puts focus back on the icon, which is where the user started and is visible again by now
      if (searchClose) {
        searchClose.addEventListener('click', () => {
          setSearchOpen(false);
          if (searchToggle) searchToggle.focus();
        });
      }
    
      // Escape closes things and puts focus back on the control that opened them (WCAG 2.1.1)
      // it's on the document, not the header, so a hover-opened dropdown can be dismissed
      // without moving the pointer or having focus in the header (WCAG 1.4.13)
      document.addEventListener('keydown', (event) => {
        if (event.key !== 'Escape') return;
    
        // let escape clear a search input that has text in it first
        if (event.target === searchInput && searchInput.value) return;
    
        const focusInHeader = header.contains(document.activeElement);
    
        // the search box collapses back to its icon first, on every screen size
        if (isSearchOpen()) {
          setSearchOpen(false);
          if (focusInHeader && searchToggle) searchToggle.focus();
          return;
        }
    
        if (isDesktop()) {
          // desktop: close the open dropdown and return focus to its top level button
          const topButton = openButtons().find(isTopLevel);
          if (!topButton) return;
    
          closeAllPanels();
          if (focusInHeader) topButton.focus();
        } else if (isMenuOpen()) {
          // mobile and tablet: close the whole menu and return focus to the menu button
          closeAll();
          if (focusInHeader && menuToggle) menuToggle.focus();
        }
      });
    
      document.addEventListener('click', (event) => {
        if (!header.contains(event.target)) closeAll();
      });
    
      header.addEventListener('focusin', (event) => {
        if (!isDesktop()) return;
    
        // desktop: tabbing to another part of the header closes dropdowns focus has left
        openButtons().forEach((button) => {
          const item = button.closest('.ds2-top-nav__item');
    
          if (isExpanded(button) && item && !item.contains(event.target)) closePanel(button);
        });
    
        // the close button is left out: focusing it would shut an empty box before the click landed,
        // taking the button out of the page mid-click and leaving focus nowhere
        const movingToClose = searchClose && searchClose.contains(event.target);
    
        if (searchForm && !searchForm.contains(event.target) && !movingToClose) closeSearchIfEmpty();
      });
    
      header.addEventListener('focusout', (event) => {
        // relatedTarget is null when the window loses focus, keep the menu open in that case
        if (event.relatedTarget && !header.contains(event.relatedTarget)) closeAll();
      });
    
      // ACCESSIBILITY: tab order follows the markup, and one markup order can't suit both layouts.
      // the burger has to come before the menu it opens on mobile, which puts the search ahead of the
      // links on desktop, where you'd then tab from the logo out to the search icon and back to the
      // middle for the nav. So the nav is moved ahead of the search on desktop and put back below the
      // breakpoint (WCAG 2.4.3). Moving a node keeps its listeners, and the nav is positioned against
      // the header on mobile, so nothing shifts on screen either way
      const placeNav = () => {
        if (!bar || !searchGroup) return;
    
        if (isDesktop()) {
          // into the bar, between the logo and the search. The bar is display: contents up here,
          // so the nav still takes its place in the header's grid
          if (nav.parentNode !== bar || nav.nextElementSibling !== searchGroup) {
            bar.insertBefore(nav, searchGroup);
          }
        } else if (nav.parentNode !== header || nav.nextElementSibling !== navHome) {
          header.insertBefore(nav, navHome);
        }
      };
    
      // start each layout clean when crossing the desktop breakpoint
      desktopQuery.addEventListener('change', () => {
        // closing first, so focus is never inside the nav while it's being moved
        closeAll();
        setSearchOpen(false);
        placeNav();
      });
    
      // matchMedia only fires on a change, so the starting width has to be handled here
      placeNav();
    
      setMenuOpen(false);
      header.classList.add('ds2-top-nav--js');
    };
  • URL: /components/raw/ds2-top-nav/ds2-top-nav.module.js
  • Filesystem Path: src/library/02-ds2-components/ds2-top-nav/ds2-top-nav.module.js
  • Size: 19.4 KB
{
  "idPrefix": "ds2-top-nav",
  "desktopHover": false,
  "navLabel": "Main",
  "menuLabel": "Menu",
  "logo": {
    "url": "https://www.leeds.ac.uk/",
    "label": "University of Leeds homepage"
  },
  "search": {
    "action": "https://www.leeds.ac.uk/search",
    "inputName": "q",
    "label": "Search leeds.ac.uk",
    "placeholder": "Search",
    "submitLabel": "Search",
    "openLabel": "Search",
    "closeLabel": "Close search",
    "hiddenFields": [
      {
        "name": "searchOption",
        "value": "searchSite"
      }
    ]
  },
  "level1Options": [
    {
      "title": "Study",
      "level2Options": [
        {
          "title": "Course search",
          "url": "https://courses.leeds.ac.uk/"
        },
        {
          "title": "Undergraduate",
          "level3Options": [
            {
              "title": "Undergraduate home page",
              "url": "https://www.leeds.ac.uk/undergraduate"
            },
            {
              "title": "Clearing 2026",
              "url": "https://www.leeds.ac.uk/clearing"
            },
            {
              "title": "Undergraduate course search",
              "url": "https://courses.leeds.ac.uk/course-search/undergraduate-courses"
            },
            {
              "title": "Fees and funding",
              "url": "https://www.leeds.ac.uk/undergraduate-fees/doc/fees-undergraduate-fees"
            },
            {
              "title": "Student life",
              "url": "https://www.leeds.ac.uk/undergraduate-student-life/doc/students-union"
            }
          ]
        },
        {
          "title": "Online courses",
          "level3Options": [
            {
              "title": "Overview",
              "url": "https://www.leeds.ac.uk/online-courses"
            },
            {
              "title": "Online Masters study",
              "url": "https://www.leeds.ac.uk/online-masters-study"
            },
            {
              "title": "Short online courses",
              "url": "https://www.leeds.ac.uk/short-online-courses"
            },
            {
              "title": "PhD by distance learning",
              "url": "https://www.leeds.ac.uk/online-courses/doc/phd-distance-learning"
            }
          ]
        },
        {
          "title": "Masters",
          "level3Options": [
            {
              "title": "Masters home page",
              "url": "https://www.leeds.ac.uk/masters"
            },
            {
              "title": "Postgraduate open day",
              "url": "https://www.leeds.ac.uk/postgraduate-events"
            },
            {
              "title": "Masters course search",
              "url": "https://courses.leeds.ac.uk/course-search/masters-courses?type=PGT&query="
            },
            {
              "title": "Applying",
              "url": "https://www.leeds.ac.uk/masters-applying/doc/apply-masters-courses"
            },
            {
              "title": "Fees and funding",
              "url": "https://www.leeds.ac.uk/masters-offer/doc/masters-fees-funding"
            }
          ]
        },
        {
          "title": "Research degrees",
          "url": "https://www.leeds.ac.uk/research-degrees"
        },
        {
          "title": "International students",
          "url": "https://www.leeds.ac.uk/international-students"
        },
        {
          "title": "Foundation courses",
          "url": "https://www.leeds.ac.uk/foundation-courses"
        },
        {
          "title": "Mature students",
          "url": "https://www.leeds.ac.uk/mature-students"
        }
      ]
    },
    {
      "title": "Faculties",
      "level2Options": [
        {
          "title": "Arts, Humanities and Cultures",
          "url": "https://ahc.leeds.ac.uk/"
        },
        {
          "title": "Biological Sciences",
          "url": "https://biologicalsciences.leeds.ac.uk/"
        },
        {
          "title": "Business School",
          "url": "https://business.leeds.ac.uk/"
        },
        {
          "title": "Engineering and Physical Sciences",
          "url": "https://eps.leeds.ac.uk/"
        },
        {
          "title": "Environment",
          "url": "https://environment.leeds.ac.uk/"
        },
        {
          "title": "Medicine and Health",
          "url": "https://medicinehealth.leeds.ac.uk/"
        },
        {
          "title": "Social Sciences",
          "url": "https://essl.leeds.ac.uk/"
        },
        {
          "title": "Faculty contacts",
          "url": "https://www.leeds.ac.uk/about/doc/faculties-contacts"
        }
      ]
    },
    {
      "title": "Research",
      "level2Options": [
        {
          "title": "Research overview",
          "url": "https://www.leeds.ac.uk/research-and-innovation"
        },
        {
          "title": "Research excellence and impact",
          "url": "https://www.leeds.ac.uk/research-impact"
        },
        {
          "title": "Strategy and environment",
          "url": "https://www.leeds.ac.uk/research-strategy-environment"
        },
        {
          "title": "Researcher and staff development",
          "url": "https://www.leeds.ac.uk/researcher-staff-development"
        },
        {
          "title": "Public engagement",
          "url": "https://www.leeds.ac.uk/research-public-engagement"
        },
        {
          "title": "Research and innovation contacts",
          "url": "https://www.leeds.ac.uk/research-and-innovation/doc/research-innovation-contacts"
        }
      ]
    },
    {
      "title": "Business",
      "level2Options": [
        {
          "title": "Overview",
          "url": "https://www.leeds.ac.uk/business-partnerships"
        },
        {
          "title": "Access our expertise",
          "url": "https://www.leeds.ac.uk/business-partnerships-access-expertise/doc/work-with-academics"
        },
        {
          "title": "Training and development",
          "url": "https://www.leeds.ac.uk/training-development/doc/cpd-professional-learning-businesses-employers"
        },
        {
          "title": "Recruit our students and graduates",
          "url": "https://www.leeds.ac.uk/business-partnerships/doc/recruit-students"
        },
        {
          "title": "Use our facilities",
          "url": "https://www.leeds.ac.uk/business-partnerships/doc/use-facilities"
        },
        {
          "title": "Support for small businesses",
          "url": "https://www.leeds.ac.uk/business-partnerships/doc/support-small-businesses"
        },
        {
          "title": "Our business partners",
          "url": "https://www.leeds.ac.uk/business-partnerships/doc/business-partners"
        }
      ]
    },
    {
      "title": "About",
      "level2Options": [
        {
          "title": "Campus map",
          "url": "https://www.leeds.ac.uk/campusmap"
        },
        {
          "title": "Our history",
          "url": "https://www.leeds.ac.uk/about/doc/about-history"
        },
        {
          "title": "Faculties",
          "url": "https://www.leeds.ac.uk/our-faculties"
        },
        {
          "title": "Governance",
          "url": "https://www.leeds.ac.uk/structure-governance/doc/governance"
        },
        {
          "title": "Reports, accounts and policies",
          "url": "https://www.leeds.ac.uk/structure-governance/doc/reports-accounts-policies"
        },
        {
          "title": "Strategies and values",
          "url": "https://www.leeds.ac.uk/about/doc/strategies-values"
        },
        {
          "title": "Equality",
          "url": "https://equality.leeds.ac.uk/"
        },
        {
          "title": "Executive team",
          "url": "https://www.leeds.ac.uk/university-executive"
        },
        {
          "title": "News",
          "url": "https://www.leeds.ac.uk/university-news"
        },
        {
          "title": "Events",
          "url": "https://www.leeds.ac.uk/events/"
        }
      ]
    },
    {
      "title": "Alumni",
      "url": "https://www.leeds.ac.uk/alumni"
    }
  ]
}