Jump to content

User:Gibcus/common.js: Difference between revisions

From BioMicro Center
 
No edit summary
 
(19 intermediate revisions by the same user not shown)
Line 1: Line 1:
/**
/**
  * User:Gibcus/common.js
  * BioMicro Center Wiki Wrapper Full DOM Replacement
* MIT BioMicro Center — Modern Wiki Theme Engine
  *
  *
  * Paste this entire file into:
  * Strategy:
  * https://bmcwiki.mit.edu/index.php/User:Gibcus/common.js
  * 1. Extract wiki article content from MediaWiki's DOM
  *
  * 2. Inject the complete shell structure
  * It applies to YOUR login session only.
  * 3. Place article content inside the shell
  * To deploy for all users, an admin with editinterface rights
  * MediaWiki still runs underneath for editing, search, login, etc.
* must paste it into MediaWiki:Common.js instead.
  */
  */


Line 14: Line 12:
   'use strict';
   'use strict';


   /* ============================================================
   /* ── Extract nav from MediaWiki's sidebar DOM ─────────────── */
    0. WAIT FOR DOM
   function extractNavFromDom() {
  ============================================================ */
     var SKIP_IDS = {
   mw.hook( 'wikipage.content' ).add( function () {
      'p-tb': 1, 'p-personal': 1, 'p-search': 1,
     BMC.init();
      'p-logo': 1, 'p-cactions': 1, 'p-views': 1,
  } );
      'p-lang': 1, 'p-namespaces': 1
    };


  /* ============================================================
    /* Scope to the sidebar container to exclude Vector 2022 header user-menu portlets
    1. DESIGN TOKENS
      (p-vector-user-menu-userpage, p-user-menu-logout, p-personal-sticky-header, etc.) */
  ============================================================ */
    var navContainer = document.getElementById( 'vector-main-menu' ) ||
  var TOKENS = {
                      document.getElementById( 'mw-panel' )        ||
    black:    '#0d0d0d',
                      document.getElementById( 'mw-navigation' )    ||
    darkGray: '#1a1a1a',
                      document.body;
    midGray:  '#2e2e2e',
    border:  '#e2ddd6',
    bg:      '#f5f3ef',
    bgCard:  '#ffffff',
    gold:    '#b8913a',
    goldHov:  '#d4a84b',
    red:      '#8b1a1a',  // MIT red accent
    text:    '#1c1c1c',
    textMid:  '#555555',
    textLight:'#888888',
    fontBody: "'DM Sans', 'Helvetica Neue', Arial, sans-serif",
    fontMono: "'DM Mono', 'Courier New', monospace",
  };


  /* ============================================================
    var portlets = Array.prototype.slice.call(
    2. GLOBAL CSS — injected once, applies to every BMC page
      navContainer.querySelectorAll( '[id^="p-"]' )
  ============================================================ */
    );
  var GLOBAL_CSS = [
    "@import url('https://fonts.googleapis.com/css2?family=DM+Sans:ital,wght@0,300;0,400;0,500;0,600;1,400&family=DM+Mono:wght@400;500&display=swap');",


     /* --- Reset MediaWiki chrome --- */
     var nav = [];
    '#mw-navigation, #mw-head, #mw-panel, #mw-sidebar-button,',
     var currentUrl = window.location.href.split( '?' )[0];
    '#mw-sidebar-checkbox, .mw-footer, #footer, #p-logo,',
     '.vector-menu-tabs, .vector-header-start .vector-header-logo,',
    '.mw-portlet-lang, #p-cactions, .vector-page-toolbar { display: none !important; }',


     '#content, #mw-content-text, .mw-body, #mw-body-content,',
     portlets.forEach( function ( portlet ) {
    '.mw-body-content { margin: 0 !important; padding: 0 !important;',
      if ( SKIP_IDS[ portlet.id ] ) return;
    'max-width: none !important; border: none !important; }',


    'body { background: ' + TOKENS.bg + ' !important;',
      var links = Array.prototype.slice.call(
    'font-family: ' + TOKENS.fontBody + ' !important;',
        portlet.querySelectorAll( 'li a' )
    'color: ' + TOKENS.text + ' !important; margin: 0 !important; padding: 0 !important; }',
      ).map( function ( a ) {
        return { label: a.textContent.trim(), href: a.href };
      } ).filter( function ( item ) { return item.label && item.href; } );


    /* hide edit pencil icons in headings */
      if ( !links.length ) return;
    '.mw-editsection { display: none !important; }',


    /* --- BMC Top Nav Bar --- */
      var headingEl = portlet.querySelector( '[id$="-label"], h3, h2' );
    '#bmc-topnav {',
      var sectionLabel = headingEl ? headingEl.textContent.trim() : '';
    '  position: sticky; top: 0; z-index: 1000;',
    '  background: ' + TOKENS.black + ';',
    '  display: flex; align-items: center;',
    '  padding: 0 32px; height: 56px;',
    ' box-shadow: 0 1px 0 rgba(255,255,255,0.06);',
    '  gap: 0;',
    '}',
    '#bmc-topnav .bmc-nav-logo {',
    '  font-family: ' + TOKENS.fontBody + ';',
    '  font-size: 0.95em; font-weight: 600;',
    '  color: #fff; text-decoration: none;',
    '  letter-spacing: -0.01em; white-space: nowrap;',
    '  padding-right: 32px;',
    '  border-right: 1px solid #333;',
    '  margin-right: 8px;',
    '  display: flex; align-items: center; gap: 10px;',
    '}',
    '#bmc-topnav .bmc-nav-logo span.accent { color: ' + TOKENS.gold + '; }',
    '#bmc-topnav .bmc-nav-logo img { height: 28px; width: 28px; border-radius: 4px; }',
    '#bmc-nav-links {',
    '  display: flex; align-items: center;',
    '  gap: 2px; flex: 1; overflow-x: auto;',
    '}',
    '#bmc-nav-links a {',
    '  color: #aaa; text-decoration: none;',
    ' font-size: 0.8em; font-weight: 500;',
    '  letter-spacing: 0.03em;',
    '  padding: 6px 13px; border-radius: 6px;',
    '  white-space: nowrap;',
    '  transition: color 0.15s, background 0.15s;',
    '}',
    '#bmc-nav-links a:hover { color: #fff; background: #2a2a2a; }',
    '#bmc-nav-links a.bmc-nav-active { color: ' + TOKENS.gold + '; }',


    '#bmc-nav-right {',
      var isActive = links.some( function ( link ) {
    '  display: flex; align-items: center; gap: 12px; margin-left: auto;',
        return link.href.split( '?' )[0] === currentUrl;
    '}',
      } );
    '#bmc-nav-right a.bmc-ilabs-btn {',
    '  background: ' + TOKENS.gold + ';',
    '  color: ' + TOKENS.black + '; font-weight: 600;',
    '  font-size: 0.75em; letter-spacing: 0.05em;',
    '  padding: 6px 14px; border-radius: 6px;',
    '  text-decoration: none; white-space: nowrap;',
    '  transition: background 0.15s;',
    '}',
    '#bmc-nav-right a.bmc-ilabs-btn:hover { background: ' + TOKENS.goldHov + '; }',
    '#bmc-nav-right a.bmc-nav-search {',
    '  color: #888; font-size: 1.1em;',
    '  text-decoration: none;',
    '  padding: 4px 8px; border-radius: 6px;',
    ' transition: color 0.15s;',
    '}',
    '#bmc-nav-right a.bmc-nav-search:hover { color: #fff; }',


    /* --- Page wrapper --- */
      if ( links.length === 1 ) {
    '#bmc-page-wrap {',
        nav.push( {
     '  max-width: 1140px; margin: 0 auto;',
          label:    links[0].label,
    '  padding: 40px 32px 80px;',
          href:     links[0].href,
    '}',
          isActive: links[0].href.split( '?' )[0] === currentUrl
    '@media (max-width: 768px) {',
        } );
     '  #bmc-page-wrap { padding: 24px 16px 60px; }',
      } else {
    '  #bmc-topnav { padding: 0 16px; }',
        nav.push( {
     '}',
          label:   sectionLabel || links[0].label,
          href:     links[0].href,
          items:   links,
          isActive: isActive
        } );
      }
     } );


     /* --- Page title --- */
     return nav;
    '#bmc-page-title {',
  }
    '  margin-bottom: 32px;',
    '}',
    '#bmc-page-title h1 {',
    '  font-size: 2em; font-weight: 600;',
    '  color: ' + TOKENS.black + '; letter-spacing: -0.025em;',
    '  line-height: 1.1; margin: 0 0 6px;',
    '}',
    '#bmc-page-title .bmc-subtitle {',
    '  font-size: 0.92em; color: ' + TOKENS.textMid + ';',
    '  font-weight: 400;',
    '}',


    /* --- Section headings --- */
  /* ── Build topbar HTML ─────────────────────────────────────── */
    '#bmc-page-wrap h2 {',
  function buildTopbar() {
     '  font-size: 1.1em; font-weight: 600;',
     var d = document.createElement( 'div' );
    '  color: ' + TOKENS.black + ';',
     d.className = 'bmc-topbar';
     ' letter-spacing: 0.06em; text-transform: uppercase;',
     d.innerHTML =
     '  margin: 40px 0 16px;',
      '<div class="inner">' +
    ' padding-bottom: 8px;',
        '<a href="mailto:biomicro@mit.edu">biomicro@mit.edu</a>' +
    ' border-bottom: 1px solid ' + TOKENS.border + ';',
        '<span>|</span>' +
    '}',
        '617-715-4533' +
    '#bmc-page-wrap h3 {',
        '<span>|</span>' +
    ' font-size: 0.95em; font-weight: 600;',
        'Building 68-322' +
    ' color: ' + TOKENS.textMid + ';',
      '</div>';
    ' letter-spacing: 0.04em; text-transform: uppercase;',
     return d;
     '  margin: 28px 0 12px;',
  }
    '}',


    /* --- Generic body content prose --- */
  /* ── Build nav dropdown item ───────────────────────────────── */
     '#bmc-page-wrap p {',
  function buildDropdownMenu( items ) {
     '  font-size: 0.95em; line-height: 1.7;',
    var menu = document.createElement( 'div' );
    ' color: ' + TOKENS.textMid + '; margin-bottom: 12px;',
     menu.className = 'bmc-dropdown-menu';
    '}',
     items.forEach( function ( item ) {
    '#bmc-page-wrap a {',
      if ( item.divider ) {
    '  color: ' + TOKENS.gold + '; text-decoration: none;',
        var div = document.createElement( 'div' );
    ' border-bottom: 1px solid transparent;',
        div.className = 'bmc-divider';
    '  transition: border-color 0.15s;',
        menu.appendChild( div );
     '}',
      } else if ( item.groupLabel ) {
     '#bmc-page-wrap a:hover { border-bottom-color: ' + TOKENS.gold + '; }',
        var gl = document.createElement( 'div' );
        gl.className = 'bmc-group-label';
        gl.textContent = item.groupLabel;
        menu.appendChild( gl );
      } else {
        var a = document.createElement( 'a' );
        a.href = item.page ? mw.util.getUrl( item.page ) : item.href;
        a.textContent = item.label;
        if ( item.ext ) a.target = '_blank';
        menu.appendChild( a );
      }
     } );
     return menu;
  }


    /* --- Generic list styling --- */
  /* ── Build full header ─────────────────────────────────────── */
    '#bmc-page-wrap ul, #bmc-page-wrap ol {',
  function buildHeader( currentPage, nav ) {
     ' padding-left: 1.4em; margin-bottom: 12px;',
     /* Logo */
     '}',
    var logo = document.createElement( 'a' );
     '#bmc-page-wrap li {',
     logo.href = mw.util.getUrl( 'BioMicroCenter' );
     ' font-size: 0.92em; line-height: 1.7;',
     logo.className = 'bmc-logo';
    '  color: ' + TOKENS.textMid + '; margin-bottom: 3px;',
     logo.innerHTML =
    '}',
      '<img class="bmc-logo-img" src="https://bmcwiki.mit.edu/images/c/c9/Logo.png" alt="MIT BioMicro Center" />';


     /* --- Generic wiki tables → clean style --- */
     /* Nav */
     '#bmc-page-wrap table.wikitable, #bmc-page-wrap table {',
     var ul = document.createElement( 'ul' );
    ' border-collapse: collapse; width: 100%;',
     nav.forEach( function ( item ) {
     '  font-size: 0.87em; margin-bottom: 24px;',
      var li = document.createElement( 'li' );
    '  background: ' + TOKENS.bgCard + ';',
      if ( item.isActive ) li.classList.add( 'bmc-active' );
    '  border-radius: 10px; overflow: hidden;',
      if ( item.items ) li.classList.add( 'bmc-dropdown' );
    '  border: 1px solid ' + TOKENS.border + ';',
    '  box-shadow: 0 1px 4px rgba(0,0,0,0.05);',
    '}',
    '#bmc-page-wrap table th {',
    '  background: ' + TOKENS.darkGray + ';',
    '  color: #ddd; font-weight: 500;',
    '  padding: 10px 14px; text-align: left;',
    '  font-size: 0.82em; letter-spacing: 0.04em;',
    '  text-transform: uppercase;',
    '}',
    '#bmc-page-wrap table td {',
    '  padding: 9px 14px;',
    '  border-bottom: 1px solid ' + TOKENS.border + ';',
    '  color: ' + TOKENS.text + '; vertical-align: top;',
    '}',
    '#bmc-page-wrap table tr:last-child td { border-bottom: none; }',
    '#bmc-page-wrap table tr:hover td { background: #faf8f5; }',


    /* --- Card component --- */
      var a = document.createElement( 'a' );
    '.bmc-card {',
      a.href = item.page ? mw.util.getUrl( item.page ) : ( item.href || '#' );
    ' background: ' + TOKENS.bgCard + ';',
      a.textContent = item.label;
    '  border: 1px solid ' + TOKENS.border + ';',
      li.appendChild( a );
    '  border-radius: 12px;',
    '  padding: 24px;',
    '  box-shadow: 0 1px 4px rgba(0,0,0,0.05);',
    '}',
    '.bmc-card-grid {',
    '  display: grid;',
    '  grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));',
    '  gap: 16px; margin-bottom: 32px;',
    '}',


    /* --- Inline badge --- */
      if ( item.items ) {
    '.bmc-badge {',
        li.appendChild( buildDropdownMenu( item.items ) );
    '  display: inline-block;',
      }
    '  font-size: 0.7em; font-weight: 600;',
      ul.appendChild( li );
    '  letter-spacing: 0.06em; text-transform: uppercase;',
     } );
    '  padding: 2px 7px; border-radius: 4px;',
    '  background: ' + TOKENS.gold + '22;',
     '  color: ' + TOKENS.gold + '; margin-left: 6px;',
    '  vertical-align: middle;',
    '}',


     /* --- TOC hide (we build nav instead) --- */
     var navEl = document.createElement( 'nav' );
     '#toc, .toc { display: none !important; }',
    navEl.className = 'bmc-nav';
     navEl.appendChild( ul );


     /* --- Breadcrumb strip under nav --- */
     /* Search */
     '#bmc-breadcrumb {',
     var searchForm = document.createElement( 'form' );
    ' background: #fff;',
     searchForm.className = 'bmc-search-form';
     ' border-bottom: 1px solid ' + TOKENS.border + ';',
     searchForm.method = 'get';
     ' padding: 10px 32px;',
     searchForm.action = '/index.php';
     ' font-size: 0.78em;',
     searchForm.innerHTML =
     ' color: ' + TOKENS.textLight + ';',
      '<input type="hidden" name="title" value="Special:Search">' +
    ' display: flex; align-items: center; gap: 6px;',
      '<input type="search" name="search" placeholder="Search wiki…" aria-label="Search">' +
    '}',
      '<button type="submit" aria-label="Search"><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg></button>';
    '#bmc-breadcrumb a { color: ' + TOKENS.textMid + '; text-decoration: none; }',
    '#bmc-breadcrumb a:hover { color: ' + TOKENS.gold + '; }',
    '#bmc-breadcrumb .sep { color: #ccc; }',


     /* --- Pricing-specific styles (reused from earlier) --- */
     /* User info — shown only when logged in */
     '#bmc-pricing-tier-toggle {',
     var userName = mw.config.get( 'wgUserName' );
    '  display: flex; gap: 4px;',
     var userEl = null;
    '  background: ' + TOKENS.darkGray + ';',
     if ( userName ) {
    '  border-radius: 10px; padding: 4px;',
      userEl = document.createElement( 'span' );
     '  margin-bottom: 24px; width: fit-content;',
      userEl.className = 'bmc-user-info';
     '}',
      userEl.textContent = userName;
    '#bmc-pricing-tier-toggle button {',
     }
    '  padding: 7px 18px; border: none;',
    '  border-radius: 7px; font-size: 0.8em;',
    ' font-weight: 600; cursor: pointer;',
    '  letter-spacing: 0.04em;',
    '  transition: all 0.15s;',
    '  font-family: ' + TOKENS.fontBody + ';',
    '  background: transparent; color: #777;',
    '}',
    '#bmc-pricing-tier-toggle button.active-core  { background: #1a6b3c; color: #fff; }',
    '#bmc-pricing-tier-toggle button.active-mit  { background: #8b1a1a; color: #fff; }',
     '#bmc-pricing-tier-toggle button.active-nonmit{ background: #1a3b8b; color: #fff; }',


     '.bmc-price-section {',
     var right = document.createElement( 'div' );
    '  background: ' + TOKENS.bgCard + ';',
     right.className = 'bmc-header-right';
     '  border: 1px solid ' + TOKENS.border + ';',
     right.appendChild( navEl );
    '  border-radius: 12px; overflow: hidden;',
     right.appendChild( searchForm );
    '  margin-bottom: 12px;',
     var editBar = buildEditBar( currentPage );
    '  box-shadow: 0 1px 3px rgba(0,0,0,0.04);',
     if ( editBar ) { right.appendChild( editBar ); }
    '}',
     if ( userEl ) { right.appendChild( userEl ); }
    '.bmc-price-section-header {',
    '  background: #faf9f7;',
    '  border-bottom: 1px solid ' + TOKENS.border + ';',
     '  padding: 13px 20px;',
    '  font-size: 0.88em; font-weight: 600;',
     '  color: #333; cursor: pointer;',
    '  display: flex; justify-content: space-between; align-items: center;',
    '  user-select: none;',
    '}',
    '.bmc-price-section-header:hover { background: #f2f0ec; }',
     '.bmc-price-section-header .chevron { color: #aaa; font-size: 0.8em; transition: transform 0.2s; }',
     '.bmc-price-section-header.collapsed .chevron { transform: rotate(-90deg); }',
    '.bmc-price-row {',
    '  display: flex; justify-content: space-between; align-items: flex-start;',
    '  padding: 11px 20px;',
    '  border-bottom: 1px solid #f5f2ee;',
    '  transition: background 0.1s; gap: 16px;',
    '}',
     '.bmc-price-row:last-child { border-bottom: none; }',
    '.bmc-price-row:hover { background: #faf8f5; }',
    '.bmc-price-row-name {',
    '  font-size: 0.87em; color: #222;',
    '}',
    '.bmc-price-row-unit {',
    '  font-size: 0.74em; color: #aaa;',
    '  font-family: ' + TOKENS.fontMono + '; margin-top: 2px;',
    '}',
    '.bmc-price-row-note {',
    '  font-size: 0.74em; color: #999;',
    '  font-style: italic; margin-top: 3px;',
    '}',
    '.bmc-price-amount {',
    '  font-size: 1em; font-weight: 600;',
    '  font-family: ' + TOKENS.fontMono + ';',
    '  white-space: nowrap; text-align: right;',
    '  min-width: 90px;',
    '}',
    '.bmc-price-amount.tier-core  { color: #1a6b3c; }',
    '.bmc-price-amount.tier-mit    { color: #8b1a1a; }',
    '.bmc-price-amount.tier-nonmit { color: #1a3b8b; }',
    '.bmc-price-amount.tier-na    { color: #ccc; font-style: italic; font-size: 0.82em; }',


     /* search bar */
     /* Inner wrapper */
     '#bmc-pricing-search-wrap {',
     var inner = document.createElement( 'div' );
    '  position: relative; margin-bottom: 20px; width: 260px;',
     inner.className = 'bmc-header-inner';
     '}',
     inner.appendChild( logo );
    '#bmc-pricing-search {',
     inner.appendChild( right );
    ' width: 100%; padding: 9px 14px 9px 36px;',
     '  border: 1.5px solid #ddd; border-radius: 8px;',
    '  font-size: 0.85em; font-family: ' + TOKENS.fontBody + ';',
    '  background: #fff; outline: none;',
     '}',
    '#bmc-pricing-search:focus { border-color: ' + TOKENS.gold + '; }',
    '#bmc-pricing-search-icon {',
    '  position: absolute; left: 12px; top: 50%;',
    '  transform: translateY(-50%); color: #aaa; pointer-events: none;',
    '}',


     /* tier indicator pill */
     var header = document.createElement( 'header' );
    '#bmc-tier-pill {',
     header.className = 'bmc-header';
     ' display: inline-flex; align-items: center; gap: 8px;',
     header.appendChild( inner );
    ' padding: 5px 12px; border-radius: 6px;',
     return header;
     '  font-size: 0.78em; font-weight: 600;',
  }
     '  letter-spacing: 0.04em; margin-bottom: 20px;',
    '}',


    /* homepage card grid */
  /* ── Build page hero ───────────────────────────────────────── */
    '.bmc-home-grid {',
  function buildHero( titleText ) {
    '  display: grid;',
     var crumb = document.createElement( 'div' );
    '  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));',
     crumb.className = 'bmc-breadcrumb';
    '  gap: 16px; margin-top: 24px;',
     crumb.innerHTML =
    '}',
      '<a href="' + mw.util.getUrl( 'BioMicroCenter' ) + '">Home</a>' +
    '.bmc-home-card {',
      '<span class="sep">›</span>' +
    '  background: ' + TOKENS.bgCard + ';',
      document.createTextNode( titleText ).textContent; // plain text, no XSS
    '  border: 1px solid ' + TOKENS.border + ';',
    '  border-radius: 14px; padding: 22px 24px;',
    '  box-shadow: 0 1px 4px rgba(0,0,0,0.05);',
    '  transition: box-shadow 0.2s, transform 0.2s;',
    '}',
     '.bmc-home-card:hover {',
    '  box-shadow: 0 4px 16px rgba(0,0,0,0.1);',
    ' transform: translateY(-2px);',
    '}',
    '.bmc-home-card .card-icon {',
    '  font-size: 1.6em; margin-bottom: 12px;',
    '}',
    '.bmc-home-card .card-title {',
    '  font-size: 0.95em; font-weight: 600;',
     '  color: ' + TOKENS.black + '; margin-bottom: 8px;',
    '}',
    '.bmc-home-card .card-links {',
    ' list-style: none; padding: 0; margin: 0;',
     '}',
    '.bmc-home-card .card-links li {',
    ' font-size: 0.84em; line-height: 1.8;',
    '  color: ' + TOKENS.textMid + ';',
    '}',
    '.bmc-home-card .card-links li a {',
    ' color: ' + TOKENS.textMid + '; text-decoration: none;',
    '  border-bottom: none;',
    '  transition: color 0.15s;',
    '}',
    '.bmc-home-card .card-links li a:hover { color: ' + TOKENS.gold + '; }',
    '.bmc-home-card .card-links li::before {',
    '  content: ""; color: ' + TOKENS.gold + '; font-size: 0.85em;',
    '}',


     /* contact strip */
     var h1 = document.createElement( 'h1' );
    '#bmc-contact-strip {',
     h1.textContent = titleText;
    '  background: ' + TOKENS.darkGray + ';',
    '  color: #ccc; border-radius: 12px;',
    '  padding: 24px 28px; margin-top: 32px;',
    '  display: flex; gap: 32px; flex-wrap: wrap; align-items: center;',
    '}',
    '#bmc-contact-strip .contact-item {',
    ' font-size: 0.87em; display: flex; gap: 8px; align-items: center;',
    '}',
     '#bmc-contact-strip a { color: ' + TOKENS.gold + '; text-decoration: none; }',
    '#bmc-contact-strip .contact-label { color: #666; font-size: 0.8em; text-transform: uppercase; letter-spacing: 0.08em; }',


  ].join('\n');
    var inner = document.createElement( 'div' );
    inner.className = 'inner';
    inner.appendChild( crumb );
    inner.appendChild( h1 );


  /* ============================================================
    var hero = document.createElement( 'div' );
    3. NAV STRUCTURE
     hero.className = 'bmc-page-hero';
  ============================================================ */
     hero.appendChild( inner );
  var NAV_LINKS = [
     return hero;
    { label: 'Home',        href: '/index.php/BioMicroCenter',                  match: 'BioMicroCenter$' },
  }
     { label: 'Sequencing',  href: '/index.php/BioMicroCenter:Sequencing',        match: 'Sequencing' },
     { label: 'Library Prep', href: '/index.php/BioMicroCenter:Illumina_Library_Preparation', match: 'Library' },
     { label: 'Single Cell',  href: '/index.php/BioMicroCenter:SingleCell',        match: 'SingleCell|SpTx|Visium' },
    { label: 'Long Read',    href: '/index.php/BioMicroCenter:Oxford_Nanopore_Technologies', match: 'Nanopore|PacBio' },
    { label: 'Computing',    href: '/index.php/BioMicroCenter:Computing',        match: 'Computing' },
    { label: 'QC',          href: '/index.php/BioMicroCenter:QC',              match: ':QC' },
    { label: 'Pricing',      href: '/index.php/BioMicroCenter:Pricing',          match: 'Pricing' },
    { label: 'People',      href: '/index.php/BioMicroCenter:People',          match: 'People' },
    { label: 'FAQ',          href: '/index.php/BioMicroCenter:FAQ',              match: 'FAQ' },
  ];


   /* ============================================================
   /* ── Build sidebar TOC from MediaWiki #toc or heading scan ─── */
    4. MAIN BMC OBJECT
   function buildSidebarToc( mwToc, contentEl ) {
  ============================================================ */
    var toc = document.createElement( 'div' );
   var BMC = {
    toc.className = 'bmc-toc';


     page: mw.config.get( 'wgPageName' ) || '',
     var h3 = document.createElement( 'h3' );
    h3.textContent = 'On this page';
    toc.appendChild( h3 );


     /* ---- 4a. INIT ---- */
     var list;
    init: function () {
      this.injectCSS();
      this.injectNav();
      this.injectBreadcrumb();
      this.wrapContent();


       // Page-specific transforms
    if ( mwToc ) {
       if ( /^BioMicroCenter$/.test( this.page ) ) {
       /* Clone only the list from the MediaWiki TOC */
        this.transformHomepage();
      var mwList = mwToc.querySelector( 'ul' );
       } else if ( /Pricing/.test( this.page ) ) {
       if ( mwList ) {
         this.transformPricing();
        list = mwList.cloneNode( true );
      } else {
        /* Mark sub-items */
        this.transformGeneric();
        list.querySelectorAll( 'li li' ).forEach( function ( li ) {
          li.classList.add( 'bmc-sub' );
        } );
       }
    } else if ( contentEl ) {
      /* Fall back to scanning h2/h3 headings directly */
      var headings = Array.prototype.slice.call(
        contentEl.querySelectorAll( 'h2, h3' )
      );
      if ( headings.length > 0 ) {
         list = document.createElement( 'ul' );
        headings.forEach( function ( heading ) {
          var anchor = heading.querySelector( '.mw-headline' );
          if ( !anchor || !anchor.id ) return;
          var li = document.createElement( 'li' );
          if ( heading.tagName === 'H3' ) li.classList.add( 'bmc-sub' );
          var a = document.createElement( 'a' );
          a.href = '#' + anchor.id;
          a.textContent = anchor.textContent.trim();
          li.appendChild( a );
          list.appendChild( li );
        } );
       }
       }
     },
     }


     /* ---- 4b. INJECT GLOBAL CSS ---- */
     if ( !list || list.childNodes.length === 0 ) return null;
    injectCSS: function () {
      var style = document.createElement( 'style' );
      style.id = 'bmc-theme-css';
      style.textContent = GLOBAL_CSS;
      document.head.appendChild( style );
    },


     /* ---- 4c. INJECT TOP NAV ---- */
     toc.appendChild( list );
    injectNav: function () {
      var page = this.page;
      var logoImg = document.querySelector( '#p-logo img, .mw-wiki-logo' );
      var logoSrc = logoImg ? logoImg.src : '';


      var linksHtml = NAV_LINKS.map( function ( l ) {
    var aside = document.createElement( 'aside' );
        var active = new RegExp( l.match ).test( page ) ? ' bmc-nav-active' : '';
    aside.className = 'bmc-sidebar';
        return '<a href="' + l.href + '" class="' + active + '">' + l.label + '</a>';
    aside.appendChild( toc );
      } ).join( '' );
     return aside;
 
  }
      var nav = document.createElement( 'div' );
      nav.id = 'bmc-topnav';
      nav.innerHTML =
        '<a href="/index.php/BioMicroCenter" class="bmc-nav-logo">' +
          ( logoSrc ? '<img src="' + logoSrc + '" alt="BMC">' : '' ) +
          'BioMicro <span class="accent">Center</span>' +
        '</a>' +
        '<div id="bmc-nav-links">' + linksHtml + '</div>' +
        '<div id="bmc-nav-right">' +
          '<a href="/index.php/Special:Search" class="bmc-nav-search" title="Search">⌕</a>' +
          '<a href="https://mit-ki.ilabsolutions.com/sc/3381/ki-genomics-core-mit-biomicro-center?tab=about"' +
          ' class="bmc-ilabs-btn" target="_blank">Submit via iLabs ↗</a>' +
        '</div>';
 
      // Insert before body content
      var body = document.body;
      body.insertBefore( nav, body.firstChild );
    },
 
     /* ---- 4d. BREADCRUMB ---- */
    injectBreadcrumb: function () {
      var page = this.page;
      if ( /^BioMicroCenter$/.test( page ) ) return; // no breadcrumb on homepage
 
      var parts = page.split( ':' );
      var crumb = document.createElement( 'div' );
      crumb.id = 'bmc-breadcrumb';
      crumb.innerHTML =
        '<a href="/index.php/BioMicroCenter">Home</a>' +
        '<span class="sep">›</span>' +
        '<span>' + ( parts[1] || parts[0] ).replace( /_/g, ' ' ) + '</span>';
 
      var nav = document.getElementById( 'bmc-topnav' );
      if ( nav ) nav.after( crumb );
    },


    /* ---- 4e. WRAP MAIN CONTENT ---- */
    wrapContent: function () {
      var content = document.getElementById( 'mw-content-text' )
        || document.getElementById( 'content' )
        || document.querySelector( '.mw-body-content' );
      if ( !content ) return;


      // Get the first heading
  /* ── Smooth scroll for anchor links ───────────────────────── */
      var h1 = document.querySelector( '#firstHeading, h1.firstHeading' );
  function initSmoothScroll( container ) {
       var titleText = h1 ? h1.textContent.replace( /^BioMicroCenter:?/, '' ).trim() : '';
    container.addEventListener( 'click', function ( e ) {
 
       var a = e.target.closest( 'a[href^="#"]' );
      // Build page title block
      if ( !a ) return;
       var titleBlock = document.createElement( 'div' );
       var id = decodeURIComponent( a.getAttribute( 'href' ).slice( 1 ) );
       titleBlock.id = 'bmc-page-title';
       var target = document.getElementById( id );
       if ( titleText && !/^BioMicroCenter$/.test( this.page ) ) {
       if ( target ) {
         titleBlock.innerHTML =
        e.preventDefault();
          '<h1>' + titleText + '</h1>';
        target.scrollIntoView( { behavior: 'smooth', block: 'start' } );
         history.pushState( null, '', '#' + id );
       }
       }
      if ( h1 ) h1.style.display = 'none';
    } );
  }


      // Wrap in page container
  /* ── Add a separator line at the END of each rowspan group, on every
      var wrap = document.createElement( 'div' );
      cell whose own span also ends at that row. This marks the boundary
       wrap.id = 'bmc-page-wrap';
      between e.g. "5M" and "25M" without ever drawing a line through
       content.parentNode.insertBefore( wrap, content );
      the inside of a single rowspan cell.                              */
       wrap.appendChild( titleBlock );
  function fixRowspanSeparators( container ) {
      wrap.appendChild( content );
    container.querySelectorAll( '.wikitable' ).forEach( function ( table ) {
    },
       var tbody = table.querySelector( 'tbody' ) || table;
       var rows = Array.from( tbody.querySelectorAll( 'tr' ) );
       if ( !rows.length ) return;


    /* ---- 4f. GENERIC PAGE TRANSFORM ---- */
      /* Build a grid: for each row index, find which row index each
    transformGeneric: function () {
        rowspan group ENDS at, per starting cell. Then for every row
      // Just let the global CSS handle it tables, headings, prose all look better automatically.
        that is the last row of at least one rowspan group, draw a
       // Remove the old banner image if present (it's usually a 500px wide header)
        border-bottom on every td/th that is present in that row
      var banner = document.querySelector( '#mw-content-text img[src*="BMC_Header"]' );
        (cells whose own rowspan continues past this row are skipped,
      if ( banner ) {
        since the line would cut through them instead they get their
        var bannerParent = banner.closest( 'p, div' );
        own end-of-group border drawn at their own last row). */
        if ( bannerParent ) bannerParent.style.display = 'none';
       rows.forEach( function ( row, rowIdx ) {
      }
        row.querySelectorAll( 'td[rowspan], th[rowspan]' ).forEach( function ( cell ) {
    },
          var span = parseInt( cell.getAttribute( 'rowspan' ), 10 ) || 1;
          var endRowIdx = rowIdx + span - 1;
          var endRow = rows[ endRowIdx ];
          if ( !endRow ) return;


    /* ---- 4g. HOMEPAGE TRANSFORM ---- */
          /* Draw the boundary on every cell present in the END row,
    transformHomepage: function () {
            except cells whose OWN rowspan extends beyond this row
      var wrap = document.getElementById( 'bmc-page-wrap' );
            (those get their boundary drawn at their own end row). */
       if ( !wrap ) return;
          endRow.querySelectorAll( 'td, th' ).forEach( function ( endCell ) {
            var ownSpan = parseInt( endCell.getAttribute( 'rowspan' ), 10 ) || 1;
            if ( ownSpan > 1 ) return; /* this cell's own group hasn't ended yet */
            endCell.classList.add( 'bmc-rowspan-boundary' );
          } );
        } );
       } );
    } );
  }


      // Hide the old content and replace with hero + cards
      var oldContent = document.getElementById( 'mw-content-text' );


      // Extract links from existing wiki tables to preserve them
  /* ── Split <br>-separated items inside a <ul> into separate <li>s ── */
       var allLinks = {};
  function splitBrBullets( ul ) {
       if ( oldContent ) {
    Array.prototype.slice.call( ul.querySelectorAll( 'li' ) ).forEach( function ( li ) {
         oldContent.querySelectorAll( 'a' ).forEach( function ( a ) {
      if ( !li.querySelector( 'br' ) ) return;
           allLinks[a.textContent.trim()] = a.href;
       var nodes = Array.prototype.slice.call( li.childNodes );
      var segments = [[]];
       nodes.forEach( function ( node ) {
        if ( node.nodeName === 'BR' ) {
          segments.push( [] );
         } else {
          segments[ segments.length - 1 ].push( node );
        }
      } );
      /* Drop empty segments (only whitespace text nodes) */
      segments = segments.filter( function ( seg ) {
        return seg.some( function ( n ) {
           return n.nodeType === 1 ||
            ( n.nodeType === 3 && n.textContent.trim() !== '' );
         } );
         } );
        oldContent.style.display = 'none';
      }
      // Hero
      var hero = document.createElement( 'div' );
      hero.style.cssText = [
        'background: ' + TOKENS.black + ';',
        'border-radius: 16px; padding: 48px 40px;',
        'margin-bottom: 32px; position: relative; overflow: hidden;',
        'color: #fff;',
      ].join( '' );
      hero.innerHTML = [
        '<div style="position:relative;z-index:1">',
          '<div style="font-size:0.72em;letter-spacing:0.16em;color:#888;text-transform:uppercase;',
              'margin-bottom:12px;font-family:' + TOKENS.fontMono + '">',
              'Koch Institute · MIT Department of Biology · Biological Engineering',
          '</div>',
          '<h1 style="font-size:2.4em;font-weight:600;letter-spacing:-0.03em;',
              'line-height:1;margin:0 0 16px;color:#fff">',
              'MIT BioMicro<br><span style="color:' + TOKENS.gold + '">Center</span>',
          '</h1>',
          '<p style="max-width:560px;font-size:0.95em;line-height:1.7;color:#aaa;margin:0 0 24px">',
              'An integrated genomics core facility providing expertise and equipment for ',
              'systems biology — from sample prep through sequencing, single-cell, and bioinformatics.',
          '</p>',
          '<div style="display:flex;gap:12px;flex-wrap:wrap">',
            '<a href="https://mit-ki.ilabsolutions.com/sc/3381/ki-genomics-core-mit-biomicro-center?tab=about"',
              ' target="_blank"',
              ' style="background:' + TOKENS.gold + ';color:' + TOKENS.black + ';',
              'font-weight:600;font-size:0.85em;padding:10px 20px;border-radius:8px;',
              'text-decoration:none;letter-spacing:0.02em">',
              'Submit Samples via iLabs ↗',
            '</a>',
            '<a href="/index.php/BioMicroCenter:Pricing"',
              ' style="background:#222;color:#ccc;',
              'font-weight:500;font-size:0.85em;padding:10px 20px;border-radius:8px;',
              'text-decoration:none">',
              'View Pricing',
            '</a>',
          '</div>',
        '</div>',
        // subtle grid pattern overlay
        '<div style="position:absolute;inset:0;opacity:0.04;pointer-events:none;',
            'background-image:repeating-linear-gradient(0deg,#fff 0,#fff 1px,transparent 1px,transparent 40px),',
            'repeating-linear-gradient(90deg,#fff 0,#fff 1px,transparent 1px,transparent 40px)">',
        '</div>',
      ].join( '' );
      wrap.appendChild( hero );
      // Service cards
      var CARDS = [
        {
          icon: '🧬',
          title: 'Sequencing',
          links: [
            { label: 'NovaSeq X / NovaSeq 6000', href: '/index.php/BioMicroCenter:Illumina_Sequencing' },
            { label: 'Element AVITI24', href: '/index.php/BioMicroCenter:Element_Sequencing' },
            { label: 'Singular G4 / MiSeq', href: '/index.php/BioMicroCenter:Illumina_Sequencing' },
            { label: 'ONT PromethION', href: '/index.php/BioMicroCenter:Oxford_Nanopore_Technologies' },
            { label: 'PacBio Revio †', href: '/index.php/BioMicroCenter:PacBio' },
          ],
        },
        {
          icon: '📚',
          title: 'Library Preparation',
          links: [
            { label: 'Short Read — DNA', href: '/index.php/BioMicroCenter:DNA_LIB' },
            { label: 'Short Read — RNA', href: '/index.php/BioMicroCenter:RNA_LIB' },
            { label: 'High Throughput DNA', href: '/index.php/BioMicroCenter:DNA_HTL' },
            { label: 'High Throughput RNA', href: '/index.php/BioMicroCenter:RNA_HTL' },
            { label: 'Nanopore Library Prep', href: '/index.php/BioMicroCenter:NanoPore_Library_Prep' },
            { label: 'PacBio Library Prep', href: '/index.php/BioMicroCenter:PacBio_Library_Preparation' },
          ],
        },
        {
          icon: '⬡',
          title: 'Single Cell & Spatial',
          links: [
            { label: '10X Chromium (5′/3′ RNA, ATAC)', href: '/index.php/BioMicroCenter:SingleCell' },
            { label: '10X Visium (Standard + HD)', href: '/index.php/BioMicroCenter:SpTx' },
            { label: 'AVITI24 in situ', href: '/index.php/BioMicroCenter:Element_Sequencing' },
          ],
        },
        {
          icon: '🔬',
          title: 'Sample Services & QC',
          links: [
            { label: 'Fragment Analyzer / FemtoPulse', href: '/index.php/BioMicroCenter:QC' },
            { label: 'BioAnalyzer', href: '/index.php/BioMicroCenter:QC' },
            { label: 'Chemagic360 DNA/RNA Extraction', href: '/index.php/BioMicroCenter:Tecan_Freedom_Evo' },
            { label: 'Covaris Sonicator', href: '/index.php/BioMicroCenter:Covaris' },
            { label: 'RT-PCR (Roche LC480)', href: '/index.php/BioMicroCenter:RTPCR' },
          ],
        },
        {
          icon: '🤖',
          title: 'Automation',
          links: [
            { label: 'Tecan EVO 150 Liquid Handler', href: '/index.php/BioMicroCenter:Tecan_Freedom_Evo' },
            { label: 'SPT Mosquito HV', href: '/index.php/BioMicroCenter:Tecan_Freedom_Evo' },
            { label: 'Pippin Prep Electrophoresis', href: '/index.php/BioMicroCenter:PippinPrep' },
            { label: 'Oligo Synthesis', href: '/index.php/BioMicroCenter:Oligo_Synthesis' },
          ],
        },
        {
          icon: '💻',
          title: 'Bioinformatics & Computing',
          links: [
            { label: 'Bioinformatics Consulting (IGB)', href: 'https://igb.mit.edu/' },
            { label: 'Luria Computing Cluster', href: 'https://igb.mit.edu/computing-resources/luria-cluster' },
            { label: 'Active Data Storage', href: 'https://igb.mit.edu/computing-resources/active-data-storage' },
            { label: 'Data Transfer (Globus)', href: 'https://igb.mit.edu/data-management/globus' },
            { label: 'Training Sessions', href: 'https://igb.mit.edu/mini-courses' },
          ],
        },
        {
          icon: '📋',
          title: 'Get Started',
          links: [
            { label: 'Pricing', href: '/index.php/BioMicroCenter:Pricing' },
            { label: 'New User Signup', href: '/index.php/BioMicroCenter:Forms#NEW_USERS' },
            { label: 'External Submission Forms', href: '/index.php/BioMicroCenter:Forms' },
            { label: 'FAQs', href: '/index.php/BioMicroCenter:FAQ' },
            { label: 'Staff Directory', href: '/index.php/BioMicroCenter:People' },
            { label: 'News & Updates', href: '/index.php/BioMicroCenter:News' },
          ],
        },
        {
          icon: '📅',
          title: 'News & Resources',
          links: [
            { label: 'News & General Information', href: '/index.php/BioMicroCenter:News' },
            { label: 'Technology Seminar Series', href: '/index.php/BioMicroCenter:Technology_Seminar_Series' },
            { label: 'Biostuff Mailing List', href: 'http://mailman.mit.edu:/mailman/listinfo/biostuff' },
            { label: 'FAIR Data Management', href: 'https://koch-institute-mit.gitbook.io/mit-data-management-analysis-core/' },
          ],
        },
      ];
      var grid = document.createElement( 'div' );
      grid.className = 'bmc-home-grid';
      CARDS.forEach( function ( card ) {
        var li = card.links.map( function ( lnk ) {
          return '<li><a href="' + lnk.href + '">' + lnk.label + '</a></li>';
        } ).join( '' );
        var el = document.createElement( 'div' );
        el.className = 'bmc-home-card';
        el.innerHTML =
          '<div class="card-icon">' + card.icon + '</div>' +
          '<div class="card-title">' + card.title + '</div>' +
          '<ul class="card-links">' + li + '</ul>';
        grid.appendChild( el );
       } );
       } );
       wrap.appendChild( grid );
       if ( segments.length <= 1 ) return;
      var parent = li.parentNode;
      var ref    = li.nextSibling;
      segments.forEach( function ( seg ) {
        var newLi = document.createElement( 'li' );
        seg.forEach( function ( n ) { newLi.appendChild( n ); } );
        parent.insertBefore( newLi, ref );
      } );
      parent.removeChild( li );
    } );
  }


      // Contact strip
  /* ── Edit bar for logged-in users ─────────────────────────── */
      var contact = document.createElement( 'div' );
  function buildEditBar( currentPage ) {
      contact.id = 'bmc-contact-strip';
    if ( !mw.config.get( 'wgUserId' ) ) return null;
      contact.innerHTML = [
    var bar = document.createElement( 'div' );
        '<div><div class="contact-label">Location</div>',
    bar.className = 'bmc-edit-bar';
        '<div class="contact-item">Room 68-322, MIT</div></div>',
    var editHref = mw.util.getUrl( currentPage, { action: 'edit' } );
        '<div><div class="contact-label">Email</div>',
    var histHref = mw.util.getUrl( currentPage, { action: 'history' } );
        '<div class="contact-item"><a href="mailto:biomicro@mit.edu">biomicro@mit.edu</a></div></div>',
    var talkHref = mw.util.getUrl( 'Talk:' + currentPage );
        '<div><div class="contact-label">Phone</div>',
    bar.innerHTML =
        '<div class="contact-item">617-715-4533</div></div>',
      '<a href="' + editHref + '">Edit</a>' +
        '<div style="margin-left:auto">',
      '<a href="' + histHref + '">History</a>' +
        '<a href="https://mit-ki.ilabsolutions.com/sc/3381/ki-genomics-core-mit-biomicro-center?tab=about"',
      '<a href="' + talkHref + '">Talk</a>';
          ' target="_blank"',
    return bar;
          ' style="background:' + TOKENS.gold + ';color:#000;font-weight:600;',
  }
          'font-size:0.82em;padding:9px 18px;border-radius:7px;text-decoration:none">',
          'iLabs — Internal Submission ↗',
        '</a></div>',
      ].join( '' );
      wrap.appendChild( contact );
    },


    /* ---- 4h. PRICING PAGE TRANSFORM ---- */
  /* ── Main: build and inject the shell ─────────────────────── */
    transformPricing: function () {
  function buildShell( nav ) {
      var self = this;
    var currentPage = mw.config.get( 'wgPageName' ) || '';
      var oldContent = document.getElementById( 'mw-content-text' );
      if ( !oldContent ) return;


      // Current tier state
    /* 1. Extract wiki content BEFORE any DOM manipulation */
      var tier = 'mit';
    var mwContentText = document.getElementById( 'mw-content-text' );
      var tierColors = { core: '#1a6b3c', mit: '#8b1a1a', nonmit: '#1a3b8b' };
    if ( !mwContentText ) return; /* not a content page, bail */
      var tierBadges = { core: 'CORE', mit: 'MIT', nonmit: 'EXT' };
      var tierLabels = { core: 'Core Lab', mit: 'MIT', nonmit: 'Non-MIT' };


      // Parse all wiki tables from the existing rendered HTML
    var contentClone = mwContentText.cloneNode( true );
      // Each table = one pricing section
      var sections = self.parsePricingTables( oldContent );


      // Hide original content
    /* 2. Extract page title */
       oldContent.style.display = 'none';
    var titleEl = document.getElementById( 'firstHeading' ) ||
       document.querySelector( '.mw-first-heading' );
    var titleText = titleEl
      ? titleEl.textContent.trim()
      : ( mw.config.get( 'wgTitle' ) || 'BioMicro Center' );


      var wrap = document.getElementById( 'bmc-page-wrap' );
    /* 3. Extract MediaWiki TOC from the cloned content */
      if ( !wrap ) return;
    var mwToc = contentClone.querySelector( '#toc, .toc' );
    if ( mwToc ) mwToc.parentNode.removeChild( mwToc ); /* remove from content body */


      // Controls row
    /* 3b. Hide the BMC logo/header sticker by exact image filename.
       var controls = document.createElement( 'div' );
          The sticker is BMC_Header_2020_3.png (from the {{BioMicroCenter}} template)
       controls.style.cssText = 'display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:16px;margin-bottom:8px;';
          and bmc_logo_square.png. Target the nearest meaningful container so the
          surrounding content (nav links, table structure) is preserved. */
    contentClone.querySelectorAll( 'img' ).forEach( function ( img ) {
       var src = img.getAttribute( 'src' ) || '';
       if ( /BMC_Header|bmc_logo_square|Bmc_logo|\/Logo\.png/i.test( src ) ) {
        var container = img.closest( '.thumb' ) || img.closest( 'figure' ) || img.closest( 'td' );
        if ( container ) {
          container.style.display = 'none';
        } else {
          img.style.display = 'none';
        }
      }
    } );


      // Tier toggle
    /* 4. Build shell elements */
      var toggle = document.createElement( 'div' );
    var topbar = buildTopbar();
      toggle.id = 'bmc-pricing-tier-toggle';
    var header = buildHeader( currentPage, nav );
      [ 'core', 'mit', 'nonmit' ].forEach( function ( t ) {
    var hero  = buildHero( titleText );
        var btn = document.createElement( 'button' );
        btn.textContent = tierLabels[t];
        btn.dataset.tier = t;
        btn.className = t === tier ? 'active-' + t : '';
        btn.addEventListener( 'click', function () {
          tier = t;
          toggle.querySelectorAll( 'button' ).forEach( function ( b ) { b.className = ''; } );
          btn.className = 'active-' + t;
          pill.style.background = tierColors[t] + '22';
          pill.style.color = tierColors[t];
          pill.style.borderColor = tierColors[t] + '44';
          pillBadge.style.background = tierColors[t];
          pillBadge.textContent = tierBadges[t];
          pillLabel.textContent = 'Showing ' + tierLabels[t] + ' pricing';
          self.updatePricingTier( t );
        } );
        toggle.appendChild( btn );
      } );


      // Search
    /* Page layout grid */
      var searchWrap = document.createElement( 'div' );
    var layout = document.createElement( 'div' );
      searchWrap.id = 'bmc-pricing-search-wrap';
    layout.className = 'bmc-page-layout';
      searchWrap.innerHTML =
        '<span id="bmc-pricing-search-icon">⌕</span>' +
        '<input id="bmc-pricing-search" type="text" placeholder="Search services…">';
      searchWrap.querySelector( 'input' ).addEventListener( 'input', function () {
        self.filterPricingRows( this.value.toLowerCase() );
      } );


      controls.appendChild( toggle );
    /* Sidebar — skip on the home page */
       controls.appendChild( searchWrap );
    var isHome = ( currentPage === 'BioMicroCenter' ||
       wrap.appendChild( controls );
                  currentPage === 'Main_Page' );
    var sidebar = isHome ? null : buildSidebarToc( mwToc, contentClone );
    if ( sidebar ) {
       layout.appendChild( sidebar );
    } else {
      /* No TOC — collapse to single-column via inline style */
       layout.style.gridTemplateColumns = '1fr';
    }


      // Tier pill
    /* Main content area */
      var pill = document.createElement( 'div' );
    var main = document.createElement( 'main' );
      pill.id = 'bmc-tier-pill';
    main.className = 'bmc-content';
      pill.style.cssText = 'background:' + tierColors[tier] + '22;color:' + tierColors[tier] + ';border:1.5px solid ' + tierColors[tier] + '44;border-radius:6px;';
    main.appendChild( contentClone );
      var pillBadge = document.createElement( 'span' );
    layout.appendChild( main );
      pillBadge.style.cssText = 'background:' + tierColors[tier] + ';color:#fff;border-radius:3px;padding:1px 6px;font-size:0.82em;letter-spacing:0.08em;';
      pillBadge.textContent = tierBadges[tier];
      var pillLabel = document.createElement( 'span' );
      pillLabel.textContent = 'Showing ' + tierLabels[tier] + ' pricing';
      pill.appendChild( pillBadge );
      pill.appendChild( pillLabel );
      wrap.appendChild( pill );


      // Render sections
    /* 5. Build wrapper */
      var container = document.createElement( 'div' );
    var wrapper = document.createElement( 'div' );
      container.id = 'bmc-pricing-container';
    wrapper.id = 'bmc-wrapper';
      wrap.appendChild( container );
    wrapper.appendChild( topbar );
    wrapper.appendChild( header );
    wrapper.appendChild( hero );
    wrapper.appendChild( layout );


      sections.forEach( function ( section ) {
    var templateFooter = contentClone.querySelector( '#footer' );
        var card = document.createElement( 'div' );
    if ( templateFooter ) {
        card.className = 'bmc-price-section';
      templateFooter.remove();
      wrapper.appendChild( templateFooter );
    }


        var header = document.createElement( 'div' );
    /* 6. Inject into body (prepend so it appears first) */
        header.className = 'bmc-price-section-header';
    document.body.insertBefore( wrapper, document.body.firstChild );
        header.innerHTML = section.title + '<span class="chevron">▼</span>';
        header.addEventListener( 'click', function () {
          var body = card.querySelector( '.bmc-price-body' );
          var collapsed = header.classList.toggle( 'collapsed' );
          body.style.display = collapsed ? 'none' : '';
        } );


        var body = document.createElement( 'div' );
    /* 7. Add class immediately so the CSS rule kicks in and hides
        body.className = 'bmc-price-body';
          all original MW elements (works for any skin version) */
    document.body.classList.add( 'bmc-active' );
    if ( isHome ) {
      document.body.classList.add( 'bmc-home' );


        section.rows.forEach( function ( row ) {
      /* Remove page title from hero — logo already identifies the site */
          var rowEl = document.createElement( 'div' );
      var heroH1 = hero.querySelector( 'h1' );
          rowEl.className = 'bmc-price-row';
      if ( heroH1 ) { heroH1.remove(); }
          rowEl.dataset.rowName = row.name.toLowerCase();
      var heroCrumb = hero.querySelector( '.bmc-breadcrumb' );
      if ( heroCrumb ) { heroCrumb.remove(); }


          var amt = row.prices[tier];
      var parserOutput = contentClone.querySelector( '.mw-parser-output' ) || contentClone;
          var amtClass = ( amt === null || amt === undefined || amt === '' )
            ? 'tier-na' : 'tier-' + tier;
          var amtText = ( amt === null || amt === undefined || amt === '' )
            ? '—' : amt;


          rowEl.innerHTML =
      /* 7a. Remove the "Welcome to the MIT BioMicro Center!" tagline */
            '<div>' +
      var welcomeEl = null;
              '<div class="bmc-price-row-name">' + row.name + '</div>' +
      Array.prototype.forEach.call( parserOutput.children, function ( el ) {
              ( row.unit ? '<div class="bmc-price-row-unit">' + row.unit + '</div>' : '' ) +
        if ( !welcomeEl && /Welcome to the MIT BioMicro Center/i.test( el.textContent ) ) {
              ( row.notes ? '<div class="bmc-price-row-note">' + row.notes + '</div>' : '' ) +
          welcomeEl = el;
            '</div>' +
        }
            '<div class="bmc-price-amount ' + amtClass + '" data-prices=\'' + JSON.stringify( row.prices ) + '\'>' +
      } );
              amtText +
      if ( welcomeEl ) { welcomeEl.parentNode.removeChild( welcomeEl ); }
            '</div>';


           body.appendChild( rowEl );
      /* 7b. The first table on the page is the quick-links list (News,
            Pricing, Sample Submission, etc.) — pull its links up into
            the hero bar as pills so they don't eat vertical space, and
            remove the now-empty table. */
      var navLinksTable = parserOutput.querySelector( 'table' );
      if ( navLinksTable ) {
        var heroLinksBar = document.createElement( 'div' );
        heroLinksBar.className = 'bmc-hero-links';
        var seenHref = {};
        navLinksTable.querySelectorAll( 'a' ).forEach( function ( a ) {
          var label = a.textContent.trim();
          var href  = a.getAttribute( 'href' );
          if ( !label || !href || seenHref[ href ] ) return;
          seenHref[ href ] = true;
          var pill = document.createElement( 'a' );
          pill.className = 'bmc-hero-link';
          pill.setAttribute( 'href', href );
          pill.textContent = label;
           heroLinksBar.appendChild( pill );
         } );
         } );
        if ( heroLinksBar.children.length ) {
          hero.querySelector( '.inner' ).appendChild( heroLinksBar );
        }
        navLinksTable.parentNode.removeChild( navLinksTable );
      }


         card.appendChild( header );
      /* 7c. The remaining table is one single "services" mega-table (5 rows,
        card.appendChild( body );
            3 columns) where every cell — including the "Assisted Services" /
        container.appendChild( card );
            "Walkup Services" / "Informatics Services" headers — wraps its
      } );
            content in its own nested one-cell table, purely for old-school
            formatting. Left as-is, our .wikitable styling was being applied
            to those nested tables too, double-boxing every cell. Unwrap them
            first, then style the table as a whole. */
      var servicesTable = parserOutput.querySelector( 'table' );
      if ( servicesTable ) {
         Array.prototype.slice.call( servicesTable.querySelectorAll( 'table' ) ).forEach(
          function ( inner ) {
            var frag = document.createDocumentFragment();
            Array.prototype.forEach.call( inner.querySelectorAll( 'td' ), function ( td ) {
              while ( td.firstChild ) { frag.appendChild( td.firstChild ); }
            } );
            inner.parentNode.replaceChild( frag, inner );
          }
        );


      // Footer note
        servicesTable.classList.add( 'wikitable', 'bmc-home-table' );
      var note = document.createElement( 'div' );
      note.style.cssText = 'margin-top:24px;padding:14px 18px;background:#fff8e8;border-radius:8px;border:1px solid #e8d8a0;font-size:0.78em;color:#7a6a3a;line-height:1.6;';
      note.innerHTML = '✦ = Minimal inventory maintained &nbsp;·&nbsp; †† = Through collaboration with nearby academic shared resources &nbsp;·&nbsp; Pricing as of January 2026. Questions? <a href="mailto:biomicro@mit.edu" style="color:#8b6914">biomicro@mit.edu</a>';
      wrap.appendChild( note );
    },


    /* ---- 4i. PARSE PRICING TABLES FROM WIKI HTML ---- */
        /* 7d. A row whose cells are ALL just a heading with no <ul> of
    parsePricingTables: function ( container ) {
              items is a group-header row ("Assisted Services" / "Walkup
      var sections = [];
              Services", "Informatics/Bioinformatics Services") — style it
      // Walk h2/h3 + table pairs
              as a colored bar. Real content rows (heading + <ul>) get
      var nodes = container.querySelectorAll( 'h2, h3, table' );
              normal styling. Because this uses the table's own
      var currentTitle = 'Services';
              <tr>/<td>/colspan, the bar aligns exactly with the columns
      var currentRows = [];
              below it — no guesswork.


      var flush = function () {
              Color is chosen by matching the cell's own label text rather
        if ( currentRows.length > 0 ) {
              than its position, so "Walkup" / "Assisted" / "Bioinformatics"
           sections.push( { title: currentTitle, rows: currentRows } );
              each get their own distinct color regardless of column order.
           currentRows = [];
              Any header text that doesn't match one of these falls back to
              the neutral wide style, so new/renamed categories don't break. */
        function headerColorClass( label ) {
          var t = label.toLowerCase();
          if ( t.indexOf( 'walkup' ) !== -1 || t.indexOf( 'walk-up' ) !== -1 ) {
            return 'bmc-home-header-walkup';
           }
          if ( t.indexOf( 'bioinformatics' ) !== -1 || t.indexOf( 'informatics' ) !== -1 ) {
            return 'bmc-home-header-bioinformatics';
          }
          if ( t.indexOf( 'assisted' ) !== -1 ) {
            return 'bmc-home-header-assisted';
           }
          return 'bmc-home-header-wide';
         }
         }
      };


      nodes.forEach( function ( node ) {
        Array.prototype.forEach.call( servicesTable.querySelectorAll( 'tr' ), function ( tr ) {
        if ( node.tagName === 'H2' || node.tagName === 'H3' ) {
          var cells = Array.prototype.slice.call( tr.children ).filter( function ( c ) {
           flush();
            return c.tagName === 'TD' || c.tagName === 'TH';
           // Clean up heading text (remove [edit] spans)
           } );
           var clone = node.cloneNode( true );
           if ( !cells.length ) return;
          clone.querySelectorAll( '.mw-editsection' ).forEach( function ( e ) { e.remove(); } );
           var isHeaderRow = cells.every( function ( td ) { return !td.querySelector( 'ul' ); } );
           currentTitle = clone.textContent.trim();
           if ( isHeaderRow ) {
        } else if ( node.tagName === 'TABLE' ) {
            tr.classList.add( 'bmc-home-header-row' );
          // Find header row to determine column order
             cells.forEach( function ( td ) {
          var headers = [];
               td.classList.add( 'bmc-home-header-cell' );
          var headerRow = node.querySelector( 'tr' );
              td.classList.add( headerColorClass( td.textContent || '' ) );
          if ( headerRow ) {
             headerRow.querySelectorAll( 'th, td' ).forEach( function ( cell ) {
               headers.push( cell.textContent.trim().toLowerCase() );
             } );
             } );
           }
           }
        } );
      }


          // Find price column indices
     }
          var coreIdx  = -1, mitIdx = -1, nonmitIdx = -1;
          var nameIdx  = 0;
          var unitIdx  = -1, notesIdx = -1;
          headers.forEach( function ( h, i ) {
            if ( /core/.test( h ) )     coreIdx  = i;
            if ( /^mit$/.test( h ) )    mitIdx    = i;
            if ( /non.?mit/.test( h ) ) nonmitIdx = i;
            if ( /unit/.test( h ) )    unitIdx  = i;
            if ( /note/.test( h ) )    notesIdx  = i;
          } );


          // Parse data rows
    /* 8. Smooth scroll */
          var rows = node.querySelectorAll( 'tr' );
    initSmoothScroll( main );
          rows.forEach( function ( row, ri ) {
    initSmoothScroll( sidebar || main );
            if ( ri === 0 ) return; // skip header
            var cells = row.querySelectorAll( 'td, th' );
            if ( cells.length < 2 ) return;


            var getText = function ( idx ) {
    /* 9. Auto-style only genuine data tables — not layout/service-grid tables.
              if ( idx < 0 || idx >= cells.length ) return '';
          Real data tables have <th> header cells; layout tables use <td> only. */
              return cells[idx].textContent.trim();
    main.querySelectorAll(
            };
      '.mw-parser-output table:not(.wikitable):not(.infobox):not(.navbox)'
    ).forEach( function ( t ) {
      var firstRow = t.querySelector( 'tr' );
      if ( firstRow && firstRow.querySelector( 'th' ) ) {
        t.classList.add( 'wikitable' );
      }
    } );


            var name = getText( nameIdx );
    /* 9b. Old-school "card grid" tables (e.g. Alumni/Staff bio grids) lay out
            if ( !name ) return;
          N profile cells per row using a bare width="350" HTML attribute on
          each <td>, with narrow width="25" gap cells in between. Browsers
          only treat that width as a hint under automatic table layout, so
          columns end up sized by content instead of rendering equally.
          Force fixed layout via our own stylesheet (not user-editable
          wikitext, so nothing here is at risk of sanitizer stripping).


            var formatPrice = function ( idx ) {
          Only tag tables with this exact, unambiguous shape: every
              var raw = getText( idx );
          width-bearing cell in the table uses one of exactly two values —
              if ( !raw || raw === '' || /pricing in/i.test( raw ) ) return null;
          a "content" width and a much narrower "gap" width — with no other
              // Already has $ sign in wiki? Return as-is, else prefix
          width values mixed in. Rows are allowed to have a trailing/short
              return raw;
          row with fewer cells (e.g. 2 people instead of 3), so this checks
            };
          values used, not a fixed count per row. This keeps the rule from
 
          ever reaching colspan header-row tables, rowspan/pricing tables,
            currentRows.push( {
          or nested single-cell formatting tables — none of those share
              name: name,
          this bare width="n" alternating content/gap pattern. */
              unit: unitIdx >= 0 ? getText( unitIdx ) : '',
    main.querySelectorAll( 'table:not(.wikitable):not(.infobox):not(.navbox)' ).forEach(
              notes: notesIdx >= 0 ? getText( notesIdx ) : '',
      function ( t ) {
              prices: {
        var widthCells = t.querySelectorAll( 'td[width]' );
                core:  formatPrice( coreIdx ),
        if ( widthCells.length < 3 ) { return; }
                mit:    formatPrice( mitIdx ),
        var values = {};
                nonmit: formatPrice( nonmitIdx ),
        Array.prototype.forEach.call( widthCells, function ( td ) {
               }
          var v = parseInt( td.getAttribute( 'width' ), 10 );
             } );
          if ( !isNaN( v ) ) { values[ v ] = ( values[ v ] || 0 ) + 1; }
        } );
        var distinct = Object.keys( values ).map( Number ).sort( function ( a, b ) { return a - b; } );
        if ( distinct.length === 2 && distinct[ 0 ] * 3 <= distinct[ 1 ] ) {
          t.classList.add( 'bmc-card-grid-table' );
          var contentWidth = distinct[ 1 ];
          Array.prototype.forEach.call( widthCells, function ( td ) {
            if ( parseInt( td.getAttribute( 'width' ), 10 ) === contentWidth ) {
               td.classList.add( 'bmc-card-grid-content-cell' );
             }
           } );
           } );
         }
         }
       } );
       }
      flush();
    );
       return sections;
 
     },
    /* 10. Fix row separators on rowspan tables */
    requestAnimationFrame( function () {
       fixRowspanSeparators( main );
     } );


     /* ---- 4j. UPDATE PRICING TIER (live swap) ---- */
     /* 11. Pricing page — convert wikitables to flex layout so column
     updatePricingTier: function ( tier ) {
          borders and content stay perfectly aligned even with colspan rows.
       document.querySelectorAll( '.bmc-price-amount' ).forEach( function ( el ) {
          colspan cells get flex-grow = span so they span proportionally. */
         try {
     if ( currentPage.indexOf( 'Pricing' ) !== -1 ) {
           var prices = JSON.parse( el.dataset.prices );
       document.body.classList.add( 'bmc-pricing' );
          var amt = prices[tier];
      main.querySelectorAll( '.wikitable' ).forEach( function ( table ) {
          var isNA = ( amt === null || amt === undefined || amt === '' );
         table.querySelectorAll( 'tr' ).forEach( function ( row ) {
          el.textContent = isNA ? '' : amt;
           row.querySelectorAll( 'th, td' ).forEach( function ( cell ) {
          el.className = 'bmc-price-amount ' + ( isNA ? 'tier-na' : 'tier-' + tier );
            var span = parseInt( cell.getAttribute( 'colspan' ) || '1', 10 );
         } catch ( e ) {}
            cell.style.setProperty( 'flex', span + ' 1 0%', 'important' );
            cell.style.setProperty( 'min-width', '0', 'important' );
          } );
         } );
       } );
       } );
     },
     }


    /* ---- 4k. FILTER PRICING ROWS ---- */
  }
    filterPricingRows: function ( query ) {
      document.querySelectorAll( '.bmc-price-row' ).forEach( function ( row ) {
        var name = row.dataset.rowName || '';
        row.style.display = name.includes( query ) ? '' : 'none';
      } );
      // Show/hide empty sections
      document.querySelectorAll( '.bmc-price-section' ).forEach( function ( section ) {
        var visible = section.querySelectorAll( '.bmc-price-row:not([style*="none"])' );
        section.style.display = visible.length ? '' : 'none';
      } );
    },


   }; // end BMC
   /* ── Entry point ───────────────────────────────────────────── */
  mw.hook( 'wikipage.content' ).add( function () {
    /* Skip special pages (edit forms, history, etc.) to not break them */
    var ns    = mw.config.get( 'wgNamespaceNumber' );
    var action = mw.config.get( 'wgAction' );
    if ( action !== 'view' ) return;
    if ( ns < 0 ) return; /* Special: pages */


  /* ============================================================
    var nav = extractNavFromDom();
    5. KICK OFF
     buildShell( nav );
  ============================================================ */
   } );
  if ( document.readyState === 'loading' ) {
     document.addEventListener( 'DOMContentLoaded', function () { BMC.init(); } );
   } else {
    BMC.init();
  }


}() );
} )();

Latest revision as of 17:04, 13 July 2026

/**
 * BioMicro Center Wiki Wrapper — Full DOM Replacement
 *
 * Strategy:
 * 1. Extract wiki article content from MediaWiki's DOM
 * 2. Inject the complete shell structure
 * 3. Place article content inside the shell
 * MediaWiki still runs underneath for editing, search, login, etc.
 */

( function () {
  'use strict';

  /* ── Extract nav from MediaWiki's sidebar DOM ─────────────── */
  function extractNavFromDom() {
    var SKIP_IDS = {
      'p-tb': 1, 'p-personal': 1, 'p-search': 1,
      'p-logo': 1, 'p-cactions': 1, 'p-views': 1,
      'p-lang': 1, 'p-namespaces': 1
    };

    /* Scope to the sidebar container to exclude Vector 2022 header user-menu portlets
       (p-vector-user-menu-userpage, p-user-menu-logout, p-personal-sticky-header, etc.) */
    var navContainer = document.getElementById( 'vector-main-menu' ) ||
                       document.getElementById( 'mw-panel' )         ||
                       document.getElementById( 'mw-navigation' )    ||
                       document.body;

    var portlets = Array.prototype.slice.call(
      navContainer.querySelectorAll( '[id^="p-"]' )
    );

    var nav = [];
    var currentUrl = window.location.href.split( '?' )[0];

    portlets.forEach( function ( portlet ) {
      if ( SKIP_IDS[ portlet.id ] ) return;

      var links = Array.prototype.slice.call(
        portlet.querySelectorAll( 'li a' )
      ).map( function ( a ) {
        return { label: a.textContent.trim(), href: a.href };
      } ).filter( function ( item ) { return item.label && item.href; } );

      if ( !links.length ) return;

      var headingEl = portlet.querySelector( '[id$="-label"], h3, h2' );
      var sectionLabel = headingEl ? headingEl.textContent.trim() : '';

      var isActive = links.some( function ( link ) {
        return link.href.split( '?' )[0] === currentUrl;
      } );

      if ( links.length === 1 ) {
        nav.push( {
          label:    links[0].label,
          href:     links[0].href,
          isActive: links[0].href.split( '?' )[0] === currentUrl
        } );
      } else {
        nav.push( {
          label:    sectionLabel || links[0].label,
          href:     links[0].href,
          items:    links,
          isActive: isActive
        } );
      }
    } );

    return nav;
  }

  /* ── Build topbar HTML ─────────────────────────────────────── */
  function buildTopbar() {
    var d = document.createElement( 'div' );
    d.className = 'bmc-topbar';
    d.innerHTML =
      '<div class="inner">' +
        '<a href="mailto:biomicro@mit.edu">biomicro@mit.edu</a>' +
        '<span>|</span>' +
        '617-715-4533' +
        '<span>|</span>' +
        'Building 68-322' +
      '</div>';
    return d;
  }

  /* ── Build nav dropdown item ───────────────────────────────── */
  function buildDropdownMenu( items ) {
    var menu = document.createElement( 'div' );
    menu.className = 'bmc-dropdown-menu';
    items.forEach( function ( item ) {
      if ( item.divider ) {
        var div = document.createElement( 'div' );
        div.className = 'bmc-divider';
        menu.appendChild( div );
      } else if ( item.groupLabel ) {
        var gl = document.createElement( 'div' );
        gl.className = 'bmc-group-label';
        gl.textContent = item.groupLabel;
        menu.appendChild( gl );
      } else {
        var a = document.createElement( 'a' );
        a.href = item.page ? mw.util.getUrl( item.page ) : item.href;
        a.textContent = item.label;
        if ( item.ext ) a.target = '_blank';
        menu.appendChild( a );
      }
    } );
    return menu;
  }

  /* ── Build full header ─────────────────────────────────────── */
  function buildHeader( currentPage, nav ) {
    /* Logo */
    var logo = document.createElement( 'a' );
    logo.href = mw.util.getUrl( 'BioMicroCenter' );
    logo.className = 'bmc-logo';
    logo.innerHTML =
      '<img class="bmc-logo-img" src="https://bmcwiki.mit.edu/images/c/c9/Logo.png" alt="MIT BioMicro Center" />';

    /* Nav */
    var ul = document.createElement( 'ul' );
    nav.forEach( function ( item ) {
      var li = document.createElement( 'li' );
      if ( item.isActive ) li.classList.add( 'bmc-active' );
      if ( item.items ) li.classList.add( 'bmc-dropdown' );

      var a = document.createElement( 'a' );
      a.href = item.page ? mw.util.getUrl( item.page ) : ( item.href || '#' );
      a.textContent = item.label;
      li.appendChild( a );

      if ( item.items ) {
        li.appendChild( buildDropdownMenu( item.items ) );
      }
      ul.appendChild( li );
    } );

    var navEl = document.createElement( 'nav' );
    navEl.className = 'bmc-nav';
    navEl.appendChild( ul );

    /* Search */
    var searchForm = document.createElement( 'form' );
    searchForm.className = 'bmc-search-form';
    searchForm.method = 'get';
    searchForm.action = '/index.php';
    searchForm.innerHTML =
      '<input type="hidden" name="title" value="Special:Search">' +
      '<input type="search" name="search" placeholder="Search wiki…" aria-label="Search">' +
      '<button type="submit" aria-label="Search"><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg></button>';

    /* User info — shown only when logged in */
    var userName = mw.config.get( 'wgUserName' );
    var userEl = null;
    if ( userName ) {
      userEl = document.createElement( 'span' );
      userEl.className = 'bmc-user-info';
      userEl.textContent = userName;
    }

    var right = document.createElement( 'div' );
    right.className = 'bmc-header-right';
    right.appendChild( navEl );
    right.appendChild( searchForm );
    var editBar = buildEditBar( currentPage );
    if ( editBar ) { right.appendChild( editBar ); }
    if ( userEl ) { right.appendChild( userEl ); }

    /* Inner wrapper */
    var inner = document.createElement( 'div' );
    inner.className = 'bmc-header-inner';
    inner.appendChild( logo );
    inner.appendChild( right );

    var header = document.createElement( 'header' );
    header.className = 'bmc-header';
    header.appendChild( inner );
    return header;
  }

  /* ── Build page hero ───────────────────────────────────────── */
  function buildHero( titleText ) {
    var crumb = document.createElement( 'div' );
    crumb.className = 'bmc-breadcrumb';
    crumb.innerHTML =
      '<a href="' + mw.util.getUrl( 'BioMicroCenter' ) + '">Home</a>' +
      '<span class="sep">›</span>' +
      document.createTextNode( titleText ).textContent; // plain text, no XSS

    var h1 = document.createElement( 'h1' );
    h1.textContent = titleText;

    var inner = document.createElement( 'div' );
    inner.className = 'inner';
    inner.appendChild( crumb );
    inner.appendChild( h1 );

    var hero = document.createElement( 'div' );
    hero.className = 'bmc-page-hero';
    hero.appendChild( inner );
    return hero;
  }

  /* ── Build sidebar TOC from MediaWiki #toc or heading scan ─── */
  function buildSidebarToc( mwToc, contentEl ) {
    var toc = document.createElement( 'div' );
    toc.className = 'bmc-toc';

    var h3 = document.createElement( 'h3' );
    h3.textContent = 'On this page';
    toc.appendChild( h3 );

    var list;

    if ( mwToc ) {
      /* Clone only the list from the MediaWiki TOC */
      var mwList = mwToc.querySelector( 'ul' );
      if ( mwList ) {
        list = mwList.cloneNode( true );
        /* Mark sub-items */
        list.querySelectorAll( 'li li' ).forEach( function ( li ) {
          li.classList.add( 'bmc-sub' );
        } );
      }
    } else if ( contentEl ) {
      /* Fall back to scanning h2/h3 headings directly */
      var headings = Array.prototype.slice.call(
        contentEl.querySelectorAll( 'h2, h3' )
      );
      if ( headings.length > 0 ) {
        list = document.createElement( 'ul' );
        headings.forEach( function ( heading ) {
          var anchor = heading.querySelector( '.mw-headline' );
          if ( !anchor || !anchor.id ) return;
          var li = document.createElement( 'li' );
          if ( heading.tagName === 'H3' ) li.classList.add( 'bmc-sub' );
          var a = document.createElement( 'a' );
          a.href = '#' + anchor.id;
          a.textContent = anchor.textContent.trim();
          li.appendChild( a );
          list.appendChild( li );
        } );
      }
    }

    if ( !list || list.childNodes.length === 0 ) return null;

    toc.appendChild( list );

    var aside = document.createElement( 'aside' );
    aside.className = 'bmc-sidebar';
    aside.appendChild( toc );
    return aside;
  }


  /* ── Smooth scroll for anchor links ───────────────────────── */
  function initSmoothScroll( container ) {
    container.addEventListener( 'click', function ( e ) {
      var a = e.target.closest( 'a[href^="#"]' );
      if ( !a ) return;
      var id = decodeURIComponent( a.getAttribute( 'href' ).slice( 1 ) );
      var target = document.getElementById( id );
      if ( target ) {
        e.preventDefault();
        target.scrollIntoView( { behavior: 'smooth', block: 'start' } );
        history.pushState( null, '', '#' + id );
      }
    } );
  }

  /* ── Add a separator line at the END of each rowspan group, on every
       cell whose own span also ends at that row. This marks the boundary
       between e.g. "5M" and "25M" without ever drawing a line through
       the inside of a single rowspan cell.                              */
  function fixRowspanSeparators( container ) {
    container.querySelectorAll( '.wikitable' ).forEach( function ( table ) {
      var tbody = table.querySelector( 'tbody' ) || table;
      var rows = Array.from( tbody.querySelectorAll( 'tr' ) );
      if ( !rows.length ) return;

      /* Build a grid: for each row index, find which row index each
         rowspan group ENDS at, per starting cell. Then for every row
         that is the last row of at least one rowspan group, draw a
         border-bottom on every td/th that is present in that row
         (cells whose own rowspan continues past this row are skipped,
         since the line would cut through them — instead they get their
         own end-of-group border drawn at their own last row). */
      rows.forEach( function ( row, rowIdx ) {
        row.querySelectorAll( 'td[rowspan], th[rowspan]' ).forEach( function ( cell ) {
          var span = parseInt( cell.getAttribute( 'rowspan' ), 10 ) || 1;
          var endRowIdx = rowIdx + span - 1;
          var endRow = rows[ endRowIdx ];
          if ( !endRow ) return;

          /* Draw the boundary on every cell present in the END row,
             except cells whose OWN rowspan extends beyond this row
             (those get their boundary drawn at their own end row). */
          endRow.querySelectorAll( 'td, th' ).forEach( function ( endCell ) {
            var ownSpan = parseInt( endCell.getAttribute( 'rowspan' ), 10 ) || 1;
            if ( ownSpan > 1 ) return; /* this cell's own group hasn't ended yet */
            endCell.classList.add( 'bmc-rowspan-boundary' );
          } );
        } );
      } );
    } );
  }


  /* ── Split <br>-separated items inside a <ul> into separate <li>s ── */
  function splitBrBullets( ul ) {
    Array.prototype.slice.call( ul.querySelectorAll( 'li' ) ).forEach( function ( li ) {
      if ( !li.querySelector( 'br' ) ) return;
      var nodes = Array.prototype.slice.call( li.childNodes );
      var segments = [[]];
      nodes.forEach( function ( node ) {
        if ( node.nodeName === 'BR' ) {
          segments.push( [] );
        } else {
          segments[ segments.length - 1 ].push( node );
        }
      } );
      /* Drop empty segments (only whitespace text nodes) */
      segments = segments.filter( function ( seg ) {
        return seg.some( function ( n ) {
          return n.nodeType === 1 ||
            ( n.nodeType === 3 && n.textContent.trim() !== '' );
        } );
      } );
      if ( segments.length <= 1 ) return;
      var parent = li.parentNode;
      var ref    = li.nextSibling;
      segments.forEach( function ( seg ) {
        var newLi = document.createElement( 'li' );
        seg.forEach( function ( n ) { newLi.appendChild( n ); } );
        parent.insertBefore( newLi, ref );
      } );
      parent.removeChild( li );
    } );
  }

  /* ── Edit bar for logged-in users ─────────────────────────── */
  function buildEditBar( currentPage ) {
    if ( !mw.config.get( 'wgUserId' ) ) return null;
    var bar = document.createElement( 'div' );
    bar.className = 'bmc-edit-bar';
    var editHref = mw.util.getUrl( currentPage, { action: 'edit' } );
    var histHref = mw.util.getUrl( currentPage, { action: 'history' } );
    var talkHref = mw.util.getUrl( 'Talk:' + currentPage );
    bar.innerHTML =
      '<a href="' + editHref + '">Edit</a>' +
      '<a href="' + histHref + '">History</a>' +
      '<a href="' + talkHref + '">Talk</a>';
    return bar;
  }

  /* ── Main: build and inject the shell ─────────────────────── */
  function buildShell( nav ) {
    var currentPage = mw.config.get( 'wgPageName' ) || '';

    /* 1. Extract wiki content BEFORE any DOM manipulation */
    var mwContentText = document.getElementById( 'mw-content-text' );
    if ( !mwContentText ) return; /* not a content page, bail */

    var contentClone = mwContentText.cloneNode( true );

    /* 2. Extract page title */
    var titleEl = document.getElementById( 'firstHeading' ) ||
      document.querySelector( '.mw-first-heading' );
    var titleText = titleEl
      ? titleEl.textContent.trim()
      : ( mw.config.get( 'wgTitle' ) || 'BioMicro Center' );

    /* 3. Extract MediaWiki TOC from the cloned content */
    var mwToc = contentClone.querySelector( '#toc, .toc' );
    if ( mwToc ) mwToc.parentNode.removeChild( mwToc ); /* remove from content body */

    /* 3b. Hide the BMC logo/header sticker by exact image filename.
           The sticker is BMC_Header_2020_3.png (from the {{BioMicroCenter}} template)
           and bmc_logo_square.png. Target the nearest meaningful container so the
           surrounding content (nav links, table structure) is preserved. */
    contentClone.querySelectorAll( 'img' ).forEach( function ( img ) {
      var src = img.getAttribute( 'src' ) || '';
      if ( /BMC_Header|bmc_logo_square|Bmc_logo|\/Logo\.png/i.test( src ) ) {
        var container = img.closest( '.thumb' ) || img.closest( 'figure' ) || img.closest( 'td' );
        if ( container ) {
          container.style.display = 'none';
        } else {
          img.style.display = 'none';
        }
      }
    } );

    /* 4. Build shell elements */
    var topbar = buildTopbar();
    var header = buildHeader( currentPage, nav );
    var hero   = buildHero( titleText );

    /* Page layout grid */
    var layout = document.createElement( 'div' );
    layout.className = 'bmc-page-layout';

    /* Sidebar — skip on the home page */
    var isHome = ( currentPage === 'BioMicroCenter' ||
                   currentPage === 'Main_Page' );
    var sidebar = isHome ? null : buildSidebarToc( mwToc, contentClone );
    if ( sidebar ) {
      layout.appendChild( sidebar );
    } else {
      /* No TOC — collapse to single-column via inline style */
      layout.style.gridTemplateColumns = '1fr';
    }

    /* Main content area */
    var main = document.createElement( 'main' );
    main.className = 'bmc-content';
    main.appendChild( contentClone );
    layout.appendChild( main );

    /* 5. Build wrapper */
    var wrapper = document.createElement( 'div' );
    wrapper.id = 'bmc-wrapper';
    wrapper.appendChild( topbar );
    wrapper.appendChild( header );
    wrapper.appendChild( hero );
    wrapper.appendChild( layout );

    var templateFooter = contentClone.querySelector( '#footer' );
    if ( templateFooter ) {
      templateFooter.remove();
      wrapper.appendChild( templateFooter );
    }

    /* 6. Inject into body (prepend so it appears first) */
    document.body.insertBefore( wrapper, document.body.firstChild );

    /* 7. Add class immediately so the CSS rule kicks in and hides
          all original MW elements (works for any skin version) */
    document.body.classList.add( 'bmc-active' );
    if ( isHome ) {
      document.body.classList.add( 'bmc-home' );

      /* Remove page title from hero — logo already identifies the site */
      var heroH1 = hero.querySelector( 'h1' );
      if ( heroH1 ) { heroH1.remove(); }
      var heroCrumb = hero.querySelector( '.bmc-breadcrumb' );
      if ( heroCrumb ) { heroCrumb.remove(); }

      var parserOutput = contentClone.querySelector( '.mw-parser-output' ) || contentClone;

      /* 7a. Remove the "Welcome to the MIT BioMicro Center!" tagline */
      var welcomeEl = null;
      Array.prototype.forEach.call( parserOutput.children, function ( el ) {
        if ( !welcomeEl && /Welcome to the MIT BioMicro Center/i.test( el.textContent ) ) {
          welcomeEl = el;
        }
      } );
      if ( welcomeEl ) { welcomeEl.parentNode.removeChild( welcomeEl ); }

      /* 7b. The first table on the page is the quick-links list (News,
             Pricing, Sample Submission, etc.) — pull its links up into
             the hero bar as pills so they don't eat vertical space, and
             remove the now-empty table. */
      var navLinksTable = parserOutput.querySelector( 'table' );
      if ( navLinksTable ) {
        var heroLinksBar = document.createElement( 'div' );
        heroLinksBar.className = 'bmc-hero-links';
        var seenHref = {};
        navLinksTable.querySelectorAll( 'a' ).forEach( function ( a ) {
          var label = a.textContent.trim();
          var href  = a.getAttribute( 'href' );
          if ( !label || !href || seenHref[ href ] ) return;
          seenHref[ href ] = true;
          var pill = document.createElement( 'a' );
          pill.className = 'bmc-hero-link';
          pill.setAttribute( 'href', href );
          pill.textContent = label;
          heroLinksBar.appendChild( pill );
        } );
        if ( heroLinksBar.children.length ) {
          hero.querySelector( '.inner' ).appendChild( heroLinksBar );
        }
        navLinksTable.parentNode.removeChild( navLinksTable );
      }

      /* 7c. The remaining table is one single "services" mega-table (5 rows,
             3 columns) where every cell — including the "Assisted Services" /
             "Walkup Services" / "Informatics Services" headers — wraps its
             content in its own nested one-cell table, purely for old-school
             formatting. Left as-is, our .wikitable styling was being applied
             to those nested tables too, double-boxing every cell. Unwrap them
             first, then style the table as a whole. */
      var servicesTable = parserOutput.querySelector( 'table' );
      if ( servicesTable ) {
        Array.prototype.slice.call( servicesTable.querySelectorAll( 'table' ) ).forEach(
          function ( inner ) {
            var frag = document.createDocumentFragment();
            Array.prototype.forEach.call( inner.querySelectorAll( 'td' ), function ( td ) {
              while ( td.firstChild ) { frag.appendChild( td.firstChild ); }
            } );
            inner.parentNode.replaceChild( frag, inner );
          }
        );

        servicesTable.classList.add( 'wikitable', 'bmc-home-table' );

        /* 7d. A row whose cells are ALL just a heading with no <ul> of
               items is a group-header row ("Assisted Services" / "Walkup
               Services", "Informatics/Bioinformatics Services") — style it
               as a colored bar. Real content rows (heading + <ul>) get
               normal styling. Because this uses the table's own
               <tr>/<td>/colspan, the bar aligns exactly with the columns
               below it — no guesswork.

               Color is chosen by matching the cell's own label text rather
               than its position, so "Walkup" / "Assisted" / "Bioinformatics"
               each get their own distinct color regardless of column order.
               Any header text that doesn't match one of these falls back to
               the neutral wide style, so new/renamed categories don't break. */
        function headerColorClass( label ) {
          var t = label.toLowerCase();
          if ( t.indexOf( 'walkup' ) !== -1 || t.indexOf( 'walk-up' ) !== -1 ) {
            return 'bmc-home-header-walkup';
          }
          if ( t.indexOf( 'bioinformatics' ) !== -1 || t.indexOf( 'informatics' ) !== -1 ) {
            return 'bmc-home-header-bioinformatics';
          }
          if ( t.indexOf( 'assisted' ) !== -1 ) {
            return 'bmc-home-header-assisted';
          }
          return 'bmc-home-header-wide';
        }

        Array.prototype.forEach.call( servicesTable.querySelectorAll( 'tr' ), function ( tr ) {
          var cells = Array.prototype.slice.call( tr.children ).filter( function ( c ) {
            return c.tagName === 'TD' || c.tagName === 'TH';
          } );
          if ( !cells.length ) return;
          var isHeaderRow = cells.every( function ( td ) { return !td.querySelector( 'ul' ); } );
          if ( isHeaderRow ) {
            tr.classList.add( 'bmc-home-header-row' );
            cells.forEach( function ( td ) {
              td.classList.add( 'bmc-home-header-cell' );
              td.classList.add( headerColorClass( td.textContent || '' ) );
            } );
          }
        } );
      }

    }

    /* 8. Smooth scroll */
    initSmoothScroll( main );
    initSmoothScroll( sidebar || main );

    /* 9. Auto-style only genuine data tables — not layout/service-grid tables.
          Real data tables have <th> header cells; layout tables use <td> only. */
    main.querySelectorAll(
      '.mw-parser-output table:not(.wikitable):not(.infobox):not(.navbox)'
    ).forEach( function ( t ) {
      var firstRow = t.querySelector( 'tr' );
      if ( firstRow && firstRow.querySelector( 'th' ) ) {
        t.classList.add( 'wikitable' );
      }
    } );

    /* 9b. Old-school "card grid" tables (e.g. Alumni/Staff bio grids) lay out
           N profile cells per row using a bare width="350" HTML attribute on
           each <td>, with narrow width="25" gap cells in between. Browsers
           only treat that width as a hint under automatic table layout, so
           columns end up sized by content instead of rendering equally.
           Force fixed layout via our own stylesheet (not user-editable
           wikitext, so nothing here is at risk of sanitizer stripping).

           Only tag tables with this exact, unambiguous shape: every
           width-bearing cell in the table uses one of exactly two values —
           a "content" width and a much narrower "gap" width — with no other
           width values mixed in. Rows are allowed to have a trailing/short
           row with fewer cells (e.g. 2 people instead of 3), so this checks
           values used, not a fixed count per row. This keeps the rule from
           ever reaching colspan header-row tables, rowspan/pricing tables,
           or nested single-cell formatting tables — none of those share
           this bare width="n" alternating content/gap pattern. */
    main.querySelectorAll( 'table:not(.wikitable):not(.infobox):not(.navbox)' ).forEach(
      function ( t ) {
        var widthCells = t.querySelectorAll( 'td[width]' );
        if ( widthCells.length < 3 ) { return; }
        var values = {};
        Array.prototype.forEach.call( widthCells, function ( td ) {
          var v = parseInt( td.getAttribute( 'width' ), 10 );
          if ( !isNaN( v ) ) { values[ v ] = ( values[ v ] || 0 ) + 1; }
        } );
        var distinct = Object.keys( values ).map( Number ).sort( function ( a, b ) { return a - b; } );
        if ( distinct.length === 2 && distinct[ 0 ] * 3 <= distinct[ 1 ] ) {
          t.classList.add( 'bmc-card-grid-table' );
          var contentWidth = distinct[ 1 ];
          Array.prototype.forEach.call( widthCells, function ( td ) {
            if ( parseInt( td.getAttribute( 'width' ), 10 ) === contentWidth ) {
              td.classList.add( 'bmc-card-grid-content-cell' );
            }
          } );
        }
      }
    );

    /* 10. Fix row separators on rowspan tables */
    requestAnimationFrame( function () {
      fixRowspanSeparators( main );
    } );

    /* 11. Pricing page — convert wikitables to flex layout so column
           borders and content stay perfectly aligned even with colspan rows.
           colspan cells get flex-grow = span so they span proportionally. */
    if ( currentPage.indexOf( 'Pricing' ) !== -1 ) {
      document.body.classList.add( 'bmc-pricing' );
      main.querySelectorAll( '.wikitable' ).forEach( function ( table ) {
        table.querySelectorAll( 'tr' ).forEach( function ( row ) {
          row.querySelectorAll( 'th, td' ).forEach( function ( cell ) {
            var span = parseInt( cell.getAttribute( 'colspan' ) || '1', 10 );
            cell.style.setProperty( 'flex', span + ' 1 0%', 'important' );
            cell.style.setProperty( 'min-width', '0', 'important' );
          } );
        } );
      } );
    }

  }

  /* ── Entry point ───────────────────────────────────────────── */
  mw.hook( 'wikipage.content' ).add( function () {
    /* Skip special pages (edit forms, history, etc.) to not break them */
    var ns     = mw.config.get( 'wgNamespaceNumber' );
    var action = mw.config.get( 'wgAction' );
    if ( action !== 'view' ) return;
    if ( ns < 0 ) return; /* Special: pages */

    var nav = extractNavFromDom();
    buildShell( nav );
  } );

} )();