$sloprun.dev

Periodic Table

demo sandbox: allow-scripts · csp: default-src 'none'

sandboxed and isolated in your browser · never enter a real password in a demo

▶ 10 ran · ✓ 0 worked
i ran it — no login needed:
share: preview embed ↗
post it anywhere:
email the card:

0 reported it broke · the ▶ count is distinct visitors who ran it, counted once each, anonymously

All 118 elements, drawn so you can see what state each one is actually in: solids are filled squares, liquids are half full, gases are left as a dashed outline. It opens in a warm room, where exactly two elements are liquid — bromine and mercury — and the dial is the point of the thing, recomputing all 118 squares live against their real melting and boiling points. Drag it to boiling water and gallium and caesium turn to puddles; drag it to flowing lava and thirty-one elements are molten while tungsten has still not budged; drop it to liquid nitrogen and the air itself goes liquid. Click any square for its mass, group, period and a plain-English line on why it matters, filter by family from the legend, and note that search reads the dial too — type liquid and you get whatever is molten at the temperature you are on right now.

Embed this demo — put a runnable demo in your blog post

Paste this where you write. It renders a live, runnable demo inline anywhere raw HTML / iframes are allowed — Ghost, WordPress, Notion, Discourse, your own site.

<iframe src="https://sloprun.dev/embed/periodic-table" width="100%" height="640" loading="lazy" allow="" style="border:1px solid #E3E2DC;border-radius:10px" title="Periodic Table — a runnable demo on sloprun.dev"></iframe>
preview ↗

On Medium and dev.to the plain link becomes a rich preview card that links back here — they don't run third-party iframes, so paste the URL there and the reader clicks through to run it. https://sloprun.dev/p/periodic-table

Source

the code you see is the code that just ran raw ↗
Source — the code you see is the code that just ran 902 lines 48.9 KB index.html
demos/periodic-table/index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<meta name="color-scheme" content="light dark">
<title>Periodic Table — sloprun.dev</title>
<link rel="icon" href="data:,">
<style>
/* ── sloprun design tokens (verbatim from demos/tokens.css) ───────────────── */
:root {
  --bg: #F6F6F3;          /* neutral paper, not cream */
  --surface: #FFFFFF;
  --ink: #1A1D21;
  --muted: #5A6068;
  --line: #E3E2DC;
  --accent: #C05600;      /* phosphor amber, darkened for light bg */
  --accent-soft: #FFF3E6;
  --run: #1A7F37;         /* earned green */
  --run-soft: #E7F4EA;
  --danger: #C93C37;
  --radius: 10px;
  --font-sans: system-ui, -apple-system, "Segoe UI", sans-serif;
  --font-mono: ui-monospace, "SF Mono", "Cascadia Code", Menlo, Consolas, monospace;
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --bg: #14161A; --surface: #1C1F26; --ink: #E8E6E1; --muted: #9AA0A8;
    --line: #2A2E36; --accent: #FFB454; --accent-soft: #2A2118;
    --run: #3FB950; --run-soft: #16281B; --danger: #F47067;
  }
}
:root[data-theme="dark"] {
  --bg: #14161A; --surface: #1C1F26; --ink: #E8E6E1; --muted: #9AA0A8;
  --line: #2A2E36; --accent: #FFB454; --accent-soft: #2A2118;
  --run: #3FB950; --run-soft: #16281B; --danger: #F47067;
}
body { background: var(--bg); color: var(--ink); font-family: var(--font-sans); }
/* ── end tokens ───────────────────────────────────────────────────────────── */

/* demo-local: keep native chrome (scrollbars, controls) on the same theme as the
   tokens above — data-theme must win over the OS preference */
:root, :root[data-theme="light"] { color-scheme: light; }
@media (prefers-color-scheme: dark) { :root:not([data-theme="light"]) { color-scheme: dark; } }
:root[data-theme="dark"] { color-scheme: dark; }

/* demo-local: shadows need alpha, tokens are opaque hex */
:root { --shadow: 0 1px 2px rgba(26,29,33,.05); --ring: rgba(26,29,33,.30); }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) { --shadow: 0 1px 2px rgba(0,0,0,.35); --ring: rgba(232,230,225,.34); }
}
:root[data-theme="dark"] { --shadow: 0 1px 2px rgba(0,0,0,.35); --ring: rgba(232,230,225,.34); }

/* ── category ramp: extended from the amber/paper identity, green kept out
      (green is reserved for earned success) ─────────────────────────────── */
.c0 { --cf:#FBDCBE; --ci:#9E5310 }   /* alkali metal      — orange   */
.c1 { --cf:#F1F0BC; --ci:#75680E }   /* alkaline earth    — gold     */
.c2 { --cf:#DEE7F4; --ci:#2E5B96 }   /* transition metal  — steel    */
.c3 { --cf:#D3E9E9; --ci:#1B6A6C }   /* post-transition   — teal     */
.c4 { --cf:#E5DFF4; --ci:#5748A4 }   /* metalloid         — violet   */
.c5 { --cf:#F9DCEC; --ci:#A5397A }   /* reactive nonmetal — magenta  */
.c6 { --cf:#F7D2CB; --ci:#A94232 }   /* halogen           — rose     */
.c7 { --cf:#DEE0F6; --ci:#464CA2 }   /* noble gas         — indigo   */
.c8 { --cf:#EFDCEF; --ci:#853C82 }   /* lanthanide        — plum     */
.c9 { --cf:#EAE1D4; --ci:#755631 }   /* actinide          — umber    */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .c0 { --cf:#43260F; --ci:#F5A24E }
  :root:not([data-theme="light"]) .c1 { --cf:#333214; --ci:#DDCE5E }
  :root:not([data-theme="light"]) .c2 { --cf:#17263D; --ci:#7FB0F0 }
  :root:not([data-theme="light"]) .c3 { --cf:#113030; --ci:#5CC3C4 }
  :root:not([data-theme="light"]) .c4 { --cf:#241F3D; --ci:#A695F0 }
  :root:not([data-theme="light"]) .c5 { --cf:#351A2B; --ci:#EC8AC0 }
  :root:not([data-theme="light"]) .c6 { --cf:#3C1F19; --ci:#F09076 }
  :root:not([data-theme="light"]) .c7 { --cf:#1D2044; --ci:#9096F2 }
  :root:not([data-theme="light"]) .c8 { --cf:#321C33; --ci:#DE92DA }
  :root:not([data-theme="light"]) .c9 { --cf:#2E261A; --ci:#CFA772 }
}
:root[data-theme="dark"] .c0 { --cf:#43260F; --ci:#F5A24E }
:root[data-theme="dark"] .c1 { --cf:#333214; --ci:#DDCE5E }
:root[data-theme="dark"] .c2 { --cf:#17263D; --ci:#7FB0F0 }
:root[data-theme="dark"] .c3 { --cf:#113030; --ci:#5CC3C4 }
:root[data-theme="dark"] .c4 { --cf:#241F3D; --ci:#A695F0 }
:root[data-theme="dark"] .c5 { --cf:#351A2B; --ci:#EC8AC0 }
:root[data-theme="dark"] .c6 { --cf:#3C1F19; --ci:#F09076 }
:root[data-theme="dark"] .c7 { --cf:#1D2044; --ci:#9096F2 }
:root[data-theme="dark"] .c8 { --cf:#321C33; --ci:#DE92DA }
:root[data-theme="dark"] .c9 { --cf:#2E261A; --ci:#CFA772 }

* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
html { -webkit-text-size-adjust: 100%; }
body { min-height: 100%; overflow-x: hidden; }

.wrap {
  max-width: 1220px; margin: 0 auto;
  padding: 22px 16px 26px;
  display: flex; flex-direction: column; gap: 14px;
}

h1 { margin: 0 0 6px; font-size: clamp(21px, 3.6vw, 29px); letter-spacing: -.021em; line-height: 1.15; }
.lede { margin: 0; max-width: 74ch; color: var(--muted); font-size: 14.5px; line-height: 1.55; }
.lede b { color: var(--ink); font-weight: 600; }

.card {
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius); padding: 12px 13px; box-shadow: var(--shadow);
}
.card h2 {
  margin: 0 0 10px; font-family: var(--font-mono); font-size: 10.5px;
  letter-spacing: .1em; text-transform: uppercase; color: var(--muted); font-weight: 600;
}

/* ── toolbar ─────────────────────────────────────────────────────────────── */
.bar { display: grid; grid-template-columns: minmax(0,300px) minmax(0,1fr); gap: 14px 18px; align-items: start; }
@media (max-width: 720px) { .bar { grid-template-columns: minmax(0,1fr); } }

.field { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
.flabel {
  display: flex; justify-content: space-between; align-items: baseline; gap: 8px;
  font-family: var(--font-mono); font-size: 10.5px; letter-spacing: .1em;
  text-transform: uppercase; color: var(--muted); font-weight: 600;
}
.searchbox { position: relative; display: flex; }
input[type=search] {
  font: inherit; font-size: 14px; font-family: var(--font-sans);
  width: 100%; padding: 8px 30px 8px 11px; color: var(--ink);
  background: var(--bg); border: 1px solid var(--line); border-radius: 8px;
  -webkit-appearance: none; appearance: none;
}
input[type=search]::-webkit-search-cancel-button { display: none; }
input[type=search]:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
input[type=search]::placeholder { color: var(--muted); opacity: .85; }
.clr {
  position: absolute; right: 4px; top: 50%; transform: translateY(-50%);
  border: 0; background: transparent; color: var(--muted); cursor: pointer;
  font-size: 15px; line-height: 1; padding: 5px 6px; border-radius: 6px;
}
.clr:hover { color: var(--ink); }
.clr:focus-visible { outline: 2px solid var(--accent); outline-offset: 0; }
.clr[hidden] { display: none; }

.status {
  font-family: var(--font-mono); font-size: 11px; letter-spacing: .02em;
  color: var(--muted); min-height: 1.35em;
}
.status.ok { color: var(--run); }
.status.none { color: var(--danger); }

.tval { font-family: var(--font-mono); font-size: 11.5px; color: var(--ink); font-variant-numeric: tabular-nums; letter-spacing: 0; text-transform: none; }
.tval .u { color: var(--muted); }

input[type=range] {
  -webkit-appearance: none; appearance: none; display: block;
  width: 100%; height: 20px; margin: 0; background: transparent; cursor: pointer;
}
input[type=range]::-webkit-slider-runnable-track { height: 4px; border-radius: 2px; background: var(--line); }
input[type=range]::-webkit-slider-thumb {
  -webkit-appearance: none; width: 16px; height: 16px; margin-top: -6px;
  border-radius: 50%; background: var(--accent); border: 2px solid var(--surface);
  box-shadow: 0 0 0 1px var(--line);
}
input[type=range]::-moz-range-track { height: 4px; border-radius: 2px; background: var(--line); }
input[type=range]::-moz-range-thumb {
  width: 14px; height: 14px; border-radius: 50%; background: var(--accent);
  border: 2px solid var(--surface); box-shadow: 0 0 0 1px var(--line);
}
input[type=range]:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 6px; }

.chips { display: flex; flex-wrap: wrap; gap: 6px; }
.chip {
  font: inherit; font-size: 12px; color: var(--muted);
  background: var(--bg); border: 1px solid var(--line); border-radius: 999px;
  padding: 4px 10px; cursor: pointer; line-height: 1.25; white-space: nowrap;
  transition: color .12s ease, border-color .12s ease, background .12s ease;
}
.chip:hover { color: var(--accent); border-color: var(--accent); }
.chip:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.chip[aria-pressed="true"] { background: var(--accent-soft); color: var(--accent); border-color: var(--accent); font-weight: 600; }
.chip .k { font-family: var(--font-mono); font-size: 10.5px; opacity: .8; margin-left: 5px; }

/* ── stage ───────────────────────────────────────────────────────────────── */
.stage { display: grid; grid-template-columns: minmax(0,1fr) 296px; gap: 14px; align-items: start; }
.tip { margin: 9px 0 0; }
.fam { margin-top: 13px; }
/* stacked: the element card follows the table, so a tap has a visible result
   without scrolling past the whole legend */
@media (max-width: 940px) {
  .stage { grid-template-columns: minmax(0,1fr); }
  .col { display: contents; }
  .tablewrap { order: 1; }
  .tip { order: 2; margin: 0; }
  .panel { order: 3; }
  .fam { order: 4; margin-top: 0; }
}

.tablewrap { overflow-x: auto; overflow-y: hidden; padding: 3px; margin: -3px 0; scrollbar-width: thin; }
.tablewrap::-webkit-scrollbar { height: 8px; }
.tablewrap::-webkit-scrollbar-track { background: transparent; }
.tablewrap::-webkit-scrollbar-thumb { background: var(--line); border-radius: 4px; }
.tablewrap::-webkit-scrollbar-thumb:hover { background: var(--muted); }
.table {
  display: grid; grid-template-columns: repeat(18, minmax(0,1fr));
  grid-auto-rows: auto; gap: 3px; min-width: 620px;
}
.spacer { grid-column: 1 / -1; height: 8px; }

.el {
  position: relative; font: inherit; padding: 0; cursor: pointer;
  aspect-ratio: 1 / 1; min-width: 0;
  border: 1px solid transparent; border-radius: 5px;
  background: var(--cf); color: var(--ink);
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 1px; overflow: hidden;
  transition: opacity .16s ease, transform .12s ease;
}
.el .n {
  position: absolute; top: 2px; left: 3px;
  font-family: var(--font-mono); font-size: clamp(7px, .95vw, 9.5px);
  line-height: 1; color: var(--muted); font-variant-numeric: tabular-nums;
}
.el .s {
  font-size: clamp(11px, 1.75vw, 18px); font-weight: 650; line-height: 1;
  letter-spacing: -.01em; margin-top: 5px;
}
.el:hover { transform: translateY(-1px); }
.el:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; z-index: 3; }

/* phase = how much of the square is filled */
.el.ph-l { background: linear-gradient(to top, var(--cf) 52%, transparent 52%); border-color: var(--cf); }
.el.ph-g { background: transparent; border-color: var(--ci); border-style: dashed; color: var(--ci); }
.el.ph-g .n { color: var(--ci); opacity: .75; }
.el.ph-u {
  background-image: repeating-linear-gradient(45deg, var(--line) 0 2px, transparent 2px 5px);
  background-color: transparent; border-color: var(--line); color: var(--muted);
}
.el.dim { opacity: .13; pointer-events: none; }
/* the selected square is the one roving tab stop — never let a filter fade it
   to the point where you cannot see where the keyboard is */
.el.dim.sel { opacity: .55; pointer-events: auto; }
.el.hit { box-shadow: 0 0 0 2px var(--accent); }
.el.sel { box-shadow: 0 0 0 2px var(--ring); z-index: 2; }
.el.hit.sel { box-shadow: 0 0 0 2px var(--accent); }

.mark {
  aspect-ratio: 1 / 1; border-radius: 5px; border: 1px dashed var(--line);
  display: flex; align-items: center; justify-content: center; min-width: 0;
  font-family: var(--font-mono); font-size: clamp(7px, .9vw, 9px);
  color: var(--muted); letter-spacing: -.02em;
}

/* ── side panel ──────────────────────────────────────────────────────────── */
.panel { display: flex; flex-direction: column; gap: 14px; }

.dhead { display: flex; align-items: center; gap: 11px; margin-bottom: 10px; }
.dtile {
  flex: none; width: 54px; height: 54px; border-radius: 8px;
  border: 1px solid transparent; background: var(--cf);
  display: flex; align-items: center; justify-content: center; position: relative;
}
.dtile.ph-l { background: linear-gradient(to top, var(--cf) 52%, transparent 52%); border-color: var(--cf); }
.dtile.ph-g { background: transparent; border-color: var(--ci); border-style: dashed; }
.dtile.ph-u { background-image: repeating-linear-gradient(45deg, var(--line) 0 2px, transparent 2px 5px); border-color: var(--line); }
.dtile .s { font-size: 22px; font-weight: 650; letter-spacing: -.02em; }
.dtile.ph-g .s { color: var(--ci); }
.dtile .n {
  position: absolute; top: 3px; left: 5px; font-family: var(--font-mono);
  font-size: 9px; color: var(--muted); line-height: 1;
}
.dname { min-width: 0; }
.dname h3 { margin: 0; font-size: 18px; letter-spacing: -.015em; line-height: 1.15; }
.dcat {
  display: inline-flex; align-items: center; gap: 6px; margin-top: 5px;
  font-family: var(--font-mono); font-size: 10px; letter-spacing: .05em;
  text-transform: uppercase; color: var(--muted);
}
.dcat .sw { width: 9px; height: 9px; border-radius: 3px; background: var(--cf); border: 1px solid var(--ci); flex: none; }
.dfact { margin: 0 0 11px; font-size: 13px; line-height: 1.5; color: var(--muted); }
.dfact b { color: var(--ink); font-weight: 600; }

.dgrid { display: grid; grid-template-columns: repeat(3, minmax(0,1fr)); gap: 11px 8px; }
.stat { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.stat .v {
  font-family: var(--font-mono); font-size: 15px; line-height: 1.1;
  font-variant-numeric: tabular-nums; letter-spacing: -.02em;
}
.stat .v small { font-size: 10px; color: var(--muted); letter-spacing: 0; }
.stat .k {
  font-family: var(--font-mono); font-size: 9px; letter-spacing: .09em;
  text-transform: uppercase; color: var(--muted);
}
.stat.wide { grid-column: span 2; }
.stat .v.hi { color: var(--accent); }

.phasebar { display: flex; height: 8px; border-radius: 4px; overflow: hidden; background: var(--line); margin-bottom: 10px; }
.phasebar i { display: block; height: 100%; transition: width .18s ease; }
.phasebar .p-s { background: var(--ink); }
.phasebar .p-l { background: var(--accent); }
.phasebar .p-g { background: var(--muted); }
/* hatched, like the "never measured" swatch below it — a flat --line block on a
   --line track is invisible in both themes */
.phasebar .p-u {
  background-color: var(--surface);
  background-image: repeating-linear-gradient(45deg, var(--line) 0 2px, transparent 2px 5px);
}

.keys { display: flex; flex-direction: column; gap: 7px; }
.key { display: flex; align-items: center; gap: 9px; font-size: 12.5px; }
.key .box { flex: none; width: 17px; height: 17px; border-radius: 4px; border: 1px solid transparent; background: var(--ink); }
.key .box.l { background: linear-gradient(to top, var(--ink) 52%, transparent 52%); border-color: var(--ink); }
.key .box.g { background: transparent; border: 1px dashed var(--muted); }
.key .box.u { background-image: repeating-linear-gradient(45deg, var(--line) 0 2px, transparent 2px 5px); border: 1px solid var(--line); }
.key .cnt { margin-left: auto; font-family: var(--font-mono); font-size: 12px; font-variant-numeric: tabular-nums; color: var(--muted); }
.key .cnt b { color: var(--ink); font-weight: 500; }

.legend { display: flex; flex-wrap: wrap; gap: 4px 5px; }
.lg {
  display: flex; align-items: center; gap: 7px; text-align: left;
  font: inherit; font-size: 12.5px; color: var(--ink);
  background: transparent; border: 1px solid transparent; border-radius: 6px;
  padding: 4px 6px; cursor: pointer; line-height: 1.25;
}
.lg .sw { flex: none; width: 13px; height: 13px; border-radius: 4px; background: var(--cf); border: 1px solid var(--ci); }
.lg .ct { font-family: var(--font-mono); font-size: 10.5px; color: var(--muted); font-variant-numeric: tabular-nums; }
.lg:hover { background: var(--bg); }
.lg:focus-visible { outline: 2px solid var(--accent); outline-offset: -1px; }
.lg[aria-pressed="true"] { border-color: var(--ci); background: var(--bg); font-weight: 600; }
.lgnote { margin: 9px 0 0; font-size: 11.5px; line-height: 1.4; color: var(--muted); }

button {
  font: inherit; font-size: 13px; font-family: var(--font-sans);
  color: var(--ink); background: var(--surface);
  border: 1px solid var(--line); border-radius: 8px;
  padding: 7px 11px; cursor: pointer; line-height: 1.2;
  transition: border-color .12s ease, color .12s ease, background .12s ease;
}
button:hover:not(:disabled) { border-color: var(--accent); color: var(--accent); }
button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.reset { align-self: flex-start; }

.hint {
  font-family: var(--font-mono); font-size: 10.5px; color: var(--muted);
  letter-spacing: .02em;
}
.swipe { display: none; }
@media (max-width: 700px) { .swipe { display: inline; } }
footer {
  font-family: var(--font-mono); font-size: 11px; color: var(--muted);
  letter-spacing: .05em; padding-top: 2px;
}
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { transition-duration: .001ms !important; animation-duration: .001ms !important; }
  .el:hover { transform: none; }
}
</style>
</head>
<body>
<div class="wrap">

  <header>
    <h1>Periodic Table</h1>
    <p class="lede">
      All 118 elements, and the temperature they live at. The dial starts at a
      <b>warm room</b>: solid squares are <b>filled</b>, liquids are <b>half full</b>, and anything
      that has boiled away is left as an <b>outline</b>. Drag the temperature and watch the table melt.
    </p>
  </header>

  <section class="card bar" aria-label="Search and temperature">
    <div class="field">
      <div class="flabel"><label for="q">Search</label><span class="tval" id="qCount">118</span><span class="sr-only"> elements shown</span></div>
      <div class="searchbox">
        <input type="search" id="q" placeholder="try: noble, liquid, Fe, 79" autocomplete="off" spellcheck="false"
               aria-describedby="qStatus">
        <button class="clr" id="qClear" type="button" hidden aria-label="Clear search">&times;</button>
      </div>
      <div class="status" id="qStatus" role="status">Family, phase, symbol or number.</div>
    </div>

    <div class="field">
      <div class="flabel">
        <label for="temp">Temperature</label>
        <span class="tval" id="tVal">293 K<span class="u"> · 20 °C</span></span>
      </div>
      <input type="range" id="temp" min="0" max="1000" step="1" value="221"
             aria-valuetext="293 kelvin, 20 degrees Celsius">
      <div class="chips" id="tPresets" role="group" aria-label="Temperature presets">
        <button class="chip" type="button" data-k="0">absolute zero<span class="k">0 K</span></button>
        <button class="chip" type="button" data-k="77">liquid nitrogen<span class="k">77 K</span></button>
        <button class="chip" type="button" data-k="293" aria-pressed="true">warm room<span class="k">293 K</span></button>
        <button class="chip" type="button" data-k="373">boiling water<span class="k">373 K</span></button>
        <button class="chip" type="button" data-k="1500">flowing lava<span class="k">1500 K</span></button>
        <button class="chip" type="button" data-k="5772">surface of the sun<span class="k">5772 K</span></button>
      </div>
    </div>
  </section>

  <main class="stage">
    <div class="col">
      <div class="tablewrap">
        <div class="table" id="table" role="group" aria-label="Periodic table of the elements. Use the arrow keys to move between elements."></div>
      </div>
      <p class="hint tip">click an element · arrow keys move · home / end jump to the ends<span class="swipe"> · swipe the table sideways</span></p>

      <section class="card fam" aria-label="Families">
        <h2>Families</h2>
        <div class="legend" id="legend" role="group" aria-label="Filter by family"></div>
        <p class="lgnote">Click a family to isolate it. The fifteen heaviest have never been made in
          weighable amounts, so their melting points are blanks rather than zeros.</p>
      </section>
    </div>

    <div class="panel">
      <section class="card" aria-label="Selected element">
        <h2>Element</h2>
        <div class="dhead">
          <div class="dtile" id="dTile"><span class="n" id="dNum">80</span><span class="s" id="dSym">Hg</span></div>
          <div class="dname">
            <h3 id="dName">Mercury</h3>
            <span class="dcat" id="dCat"><span class="sw" id="dSw"></span><span id="dCatName">Transition metal</span></span>
          </div>
        </div>
        <p class="dfact" id="dFact"></p>
        <div class="dgrid">
          <div class="stat"><span class="v" id="dMass">—</span><span class="k">mass · u</span></div>
          <div class="stat"><span class="v" id="dGroup">—</span><span class="k" id="dGroupK">group</span></div>
          <div class="stat"><span class="v" id="dPeriod">—</span><span class="k">period</span></div>
          <div class="stat"><span class="v" id="dMelt">—</span><span class="k">melts at</span></div>
          <div class="stat"><span class="v" id="dBoil">—</span><span class="k">boils at</span></div>
          <div class="stat"><span class="v hi" id="dPhase">—</span><span class="k" id="dPhaseK">at 293 K</span></div>
        </div>
      </section>

      <section class="card" aria-label="Phases at this temperature">
        <h2 id="phHead">At 293 K</h2>
        <div class="phasebar" aria-hidden="true">
          <i class="p-s" id="barS"></i><i class="p-l" id="barL"></i><i class="p-g" id="barG"></i><i class="p-u" id="barU"></i>
        </div>
        <div class="keys">
          <div class="key"><span class="box"></span><span>solid</span><span class="cnt"><b id="cS">0</b> / 118</span></div>
          <div class="key"><span class="box l"></span><span>liquid</span><span class="cnt"><b id="cL">0</b> / 118</span></div>
          <div class="key"><span class="box g"></span><span>gas</span><span class="cnt"><b id="cG">0</b> / 118</span></div>
          <div class="key"><span class="box u"></span><span>never measured</span><span class="cnt"><b id="cU">0</b> / 118</span></div>
        </div>
      </section>

      <button class="reset" id="reset" type="button">Reset everything</button>
    </div>
  </main>

  <footer>demo · sloprun.dev</footer>
</div>
<div class="sr-only" id="live" role="status" aria-live="polite"></div>
<script>
(function () {
  "use strict";

  /* families: [id, human name] — index is the colour class c0…c9 */
  var CATS = [
    ["alkali",     "Alkali metal"],
    ["alkaline",   "Alkaline earth metal"],
    ["transition", "Transition metal"],
    ["post",       "Post-transition metal"],
    ["metalloid",  "Metalloid"],
    ["nonmetal",   "Reactive nonmetal"],
    ["halogen",    "Halogen"],
    ["noble",      "Noble gas"],
    ["lanthanide", "Lanthanide"],
    ["actinide",   "Actinide"]
  ];

  /* [symbol, name, mass (u), family, group (0 = f-block), period,
      melting point (K), boiling point (K), one plain-language fact]
     Melting / boiling points are at ordinary air pressure. null = never measured. */
  var RAW = [
["H","Hydrogen",1.008,5,1,1,13.99,20.27,"The lightest thing there is. Two of these plus one oxygen make water."],
["He","Helium",4.0026,7,18,1,0,4.22,"Lifts balloons and squeaks your voice. It never freezes at ordinary pressure."],
["Li","Lithium",6.94,0,1,2,453.65,1603,"Runs your phone and laptop batteries, and is light enough to float on water."],
["Be","Beryllium",9.0122,1,2,2,1560,2742,"A stiff, feather-light metal used in springs and precision instruments."],
["B","Boron",10.81,4,13,2,2349,4200,"Goes into heat-proof kitchen glass and the borax under the sink."],
["C","Carbon",12.011,5,14,2,3823,4098,"The backbone of every living thing, and of pencil lead and diamond alike."],
["N","Nitrogen",14.007,5,15,2,63.15,77.36,"Four out of every five breaths you take. It is what keeps crisp packets puffy."],
["O","Oxygen",15.999,5,16,2,54.36,90.2,"The one your cells burn for energy, and the one that turns iron to rust."],
["F","Fluorine",18.998,6,17,2,53.53,85.03,"A trace of it hardens tooth enamel. On its own it attacks almost everything."],
["Ne","Neon",20.18,7,18,2,24.56,27.07,"The original orange glow in old shop signs, humming away for a century."],
["Na","Sodium",22.99,0,1,3,370.95,1156,"Half of table salt. On its own it skitters and fizzes across water."],
["Mg","Magnesium",24.305,1,2,3,923,1363,"Burns with a blinding white flame. Also in bike frames and laptop shells."],
["Al","Aluminium",26.982,3,13,3,933.47,2792,"Cans, foil and window frames: light, cheap and endlessly recyclable."],
["Si","Silicon",28.085,4,14,3,1687,3538,"Sand, glass, and the chip inside whatever you are reading this on."],
["P","Phosphorus",30.974,5,15,3,317.3,553.7,"In every match head, and in the DNA of every cell you own."],
["S","Sulfur",32.06,5,16,3,388.36,717.8,"The yellow stuff that smells of struck matches and old eggs."],
["Cl","Chlorine",35.45,6,17,3,171.6,239.11,"Keeps swimming pools clear. Also the other half of table salt."],
["Ar","Argon",39.948,7,18,3,83.8,87.3,"Fills double glazing and light bulbs precisely because it refuses to react."],
["K","Potassium",39.098,0,1,4,336.53,1032,"Bananas are famous for it, and your nerves cannot fire without it."],
["Ca","Calcium",40.078,1,2,4,1115,1757,"Bones, teeth, chalk cliffs and seashells are mostly this."],
["Sc","Scandium",44.956,2,3,4,1814,3109,"A pinch of it stiffens bike frames and baseball bats."],
["Ti","Titanium",47.867,2,4,4,1941,3560,"As strong as steel at half the weight. In hip joints and glasses frames."],
["V","Vanadium",50.942,2,5,4,2183,3680,"A dash in steel is what makes a good spanner refuse to bend."],
["Cr","Chromium",51.996,2,6,4,2180,2944,"The mirror shine on taps and bumpers, and what makes stainless steel stainless."],
["Mn","Manganese",54.938,2,7,4,1519,2334,"Hidden inside every steel beam and every alkaline battery."],
["Fe","Iron",55.845,2,8,4,1811,3134,"Steel, and the reason your blood is red and can carry oxygen."],
["Co","Cobalt",58.933,2,9,4,1768,3200,"The deep blue in old pottery glazes, and a key part of rechargeable cells."],
["Ni","Nickel",58.693,2,10,4,1728,3186,"The shiny coin metal, and the backbone of stainless cutlery."],
["Cu","Copper",63.546,2,11,4,1357.77,2835,"Wires, pipes, and rooftops that weather to a soft green."],
["Zn","Zinc",65.38,2,12,4,692.68,1180,"Coats nails and gutters so they never rust. Also the inside of a penny."],
["Ga","Gallium",69.723,3,13,4,302.91,2673,"A metal that melts in your hand, and lights every LED on your screen."],
["Ge","Germanium",72.63,4,14,4,1211.4,3106,"The first transistors were made of it. Now it goes into camera lenses."],
["As","Arsenic",74.922,4,15,4,887,887,"A famous poison that skips liquid entirely: heat it and it goes straight to vapour."],
["Se","Selenium",78.971,5,16,4,494,958,"Your body needs a speck of it a day, and glassmakers use it for red tints."],
["Br","Bromine",79.904,6,17,4,265.8,332,"One of only two elements that sit there as a liquid in a warm room."],
["Kr","Krypton",83.798,7,18,4,115.79,119.93,"A heavy, lazy gas that makes some camera flashes brilliantly white."],
["Rb","Rubidium",85.468,0,1,5,312.46,961,"Soft enough to cut with a butter knife, and used in some precise clocks."],
["Sr","Strontium",87.62,1,2,5,1050,1655,"The element that paints the red into fireworks."],
["Y","Yttrium",88.906,2,3,5,1799,3609,"Makes the white glow inside LED bulbs and old television screens."],
["Zr","Zirconium",91.224,2,4,5,2128,4682,"Convincing fake diamonds and ceramic kitchen knives that never need sharpening."],
["Nb","Niobium",92.906,2,5,5,2750,5017,"Jewellery that never irritates skin, and the wire inside hospital scanner magnets."],
["Mo","Molybdenum",95.95,2,6,5,2896,4912,"Keeps engine grease slippery and keeps steel strong when it gets hot."],
["Tc","Technetium",98,2,7,5,2430,4538,"The first element people made rather than found. Every atom of it is radioactive."],
["Ru","Ruthenium",101.07,2,8,5,2607,4423,"A hard-wearing tip welded onto expensive fountain pen nibs."],
["Rh","Rhodium",102.91,2,9,5,2237,3968,"Rarer than gold, and the bright plating on white-gold rings."],
["Pd","Palladium",106.42,2,10,5,1828.05,3236,"Sits in the catalytic converter cleaning up what leaves the engine."],
["Ag","Silver",107.87,2,11,5,1234.93,2435,"The best conductor of electricity known, plus cutlery and old photo film."],
["Cd","Cadmium",112.41,2,12,5,594.22,1040,"Gave old rechargeable batteries their punch, then retired for being toxic."],
["In","Indium",114.82,3,13,5,429.75,2345,"The invisible see-through coating that makes a touchscreen respond."],
["Sn","Tin",118.71,3,14,5,505.08,2875,"Tin cans are steel with a whisper of it. Solder holds your gadgets together."],
["Sb","Antimony",121.76,4,15,5,903.78,1860,"Mixed into plastics and fabrics so they resist catching light."],
["Te","Tellurium",127.6,4,16,5,722.66,1261,"A pinch makes steel easier to machine, and it shows up in thin solar panels."],
["I","Iodine",126.9,6,17,5,386.85,457.4,"Added to table salt so your thyroid can do its job."],
["Xe","Xenon",131.29,7,18,5,161.4,165.03,"The bluish white of premium headlights and old photographic flash tubes."],
["Cs","Caesium",132.91,0,1,6,301.7,944,"The second is defined by how fast its atoms hum. It would melt on a hot day."],
["Ba","Barium",137.33,1,2,6,1000,2170,"The chalky drink you swallow so a stomach scan can see anything at all."],
["La","Lanthanum",138.91,8,0,6,1193,3737,"Filled the big rechargeable packs in the first generation of hybrid cars."],
["Ce","Cerium",140.12,8,0,6,1068,3716,"Sparks off the flint in a lighter, and polishes glass mirror-smooth."],
["Pr","Praseodymium",140.91,8,0,6,1208,3793,"Tints welding goggles a protective green and stiffens strong magnets."],
["Nd","Neodymium",144.24,8,0,6,1297,3347,"Fridge magnets are feeble. These are the ones in headphones and cordless drills."],
["Pm","Promethium",145,8,0,6,1315,3273,"Once painted onto glow-in-the-dark dials. Every atom of it is radioactive."],
["Sm","Samarium",150.36,8,0,6,1345,2067,"Magnets that keep their grip even when everything around them gets hot."],
["Eu","Europium",151.96,8,0,6,1099,1802,"Puts the red in banknote security marks that glow under a lamp."],
["Gd","Gadolinium",157.25,8,0,6,1585,3546,"Injected in tiny doses so a hospital scanner can pick out soft tissue."],
["Tb","Terbium",158.93,8,0,6,1629,3503,"Turns fluorescent tubes and phone screens a truer, deeper green."],
["Dy","Dysprosium",162.5,8,0,6,1680,2840,"Stops the magnets inside wind turbines from wilting in the heat."],
["Ho","Holmium",164.93,8,0,6,1734,2993,"Powers the lasers surgeons use to break up kidney stones."],
["Er","Erbium",167.26,8,0,6,1802,3141,"Amplifies the light in the undersea fibres carrying this page to you."],
["Tm","Thulium",168.93,8,0,6,1818,2223,"Sits inside portable x-ray units used to check welds on a building site."],
["Yb","Ytterbium",173.05,8,0,6,1097,1469,"Ticks away inside the most accurate clocks ever built."],
["Lu","Lutetium",174.97,8,0,6,1925,3675,"The densest of its family, used as a catalyst when refining oil."],
["Hf","Hafnium",178.49,2,4,6,2506,4876,"Its oxide is the whisker-thin insulating layer inside modern computer chips."],
["Ta","Tantalum",180.95,2,5,6,3290,5731,"In the tiny capacitors packed by the hundred into every phone."],
["W","Tungsten",183.84,2,6,6,3695,5828,"The old light bulb filament, and the metal that stays solid the longest."],
["Re","Rhenium",186.21,2,7,6,3459,5869,"Lets the blades in an airliner engine shrug off ferocious heat."],
["Os","Osmium",190.23,2,8,6,3306,5285,"The densest natural element: a drinks can of it would weigh over seven kilos."],
["Ir","Iridium",192.22,2,9,6,2719,4701,"So hard-wearing it tips luxury pen nibs and long-life spark plugs."],
["Pt","Platinum",195.08,2,10,6,2041.4,4098,"Jewellery, laboratory dishes, and the other half of a catalytic converter."],
["Au","Gold",196.97,2,11,6,1337.33,3129,"It never tarnishes, which is why it has meant valuable for six thousand years."],
["Hg","Mercury",200.59,2,12,6,234.32,629.88,"The only metal that pools as a liquid in a warm room. Old thermometers were full of it."],
["Tl","Thallium",204.38,3,13,6,577,1746,"Once sold as rat poison, until it turned out to be far too dangerous for that."],
["Pb","Lead",207.2,3,14,6,600.61,2022,"Heavy, soft and slow to corrode, which is why old plumbing was made of it."],
["Bi","Bismuth",208.98,3,15,6,544.7,1837,"The pink in stomach-settling medicine. It grows into rainbow staircase crystals."],
["Po","Polonium",209,3,16,6,527,1235,"So intensely radioactive that a speck of it warms itself with a faint glow."],
["At","Astatine",210,6,17,6,575,610,"The rarest natural element: less than a gram exists in the whole crust at once."],
["Rn","Radon",222,7,18,6,202,211.3,"A radioactive gas that seeps out of certain rocks and pools in basements."],
["Fr","Francium",223,0,1,7,300,950,"So unstable that nobody has ever collected a lump big enough to look at."],
["Ra","Radium",226,1,2,7,973,2010,"Once painted onto watch dials to make them glow. A famously bad idea."],
["Ac","Actinium",227,9,0,7,1323,3471,"Glows a faint blue in a dark room, lit by its own decay."],
["Th","Thorium",232.04,9,0,7,2115,5061,"More common in the ground than tin. It used to brighten camping gas lamps."],
["Pa","Protactinium",231.04,9,0,7,1841,4300,"Fiendishly rare and miserable to purify, with almost no everyday use."],
["U","Uranium",238.03,9,0,7,1405.3,4404,"The heaviest element found in real quantities underground. It runs power stations."],
["Np","Neptunium",237,9,0,7,917,4273,"Made only in traces, and named after the planet out past Uranus."],
["Pu","Plutonium",244,9,0,7,912.5,3501,"A pellet of it stays warm by itself, which is how probes stay alive past Pluto."],
["Am","Americium",243,9,0,7,1449,2880,"The invisible pinch inside most household smoke alarms."],
["Cm","Curium",247,9,0,7,1613,3383,"Powers the instrument that reads the chemistry of rocks on Mars."],
["Bk","Berkelium",247,9,0,7,1259,2900,"Only a gram or so has ever existed, and none of it near you."],
["Cf","Californium",251,9,0,7,1173,1743,"A microgram is enough to see inside sealed machinery or scan rock."],
["Es","Einsteinium",252,9,0,7,1133,1269,"Made in vanishing amounts, and bright enough to glow from its own decay."],
["Fm","Fermium",257,9,0,7,1800,null,"Only ever seen a few atoms at a time, never as anything you could hold."],
["Md","Mendelevium",258,9,0,7,1100,null,"Named for the chemist who first laid this table out."],
["No","Nobelium",259,9,0,7,1100,null,"A handful of atoms at a time, gone again within minutes."],
["Lr","Lawrencium",266,9,0,7,1900,null,"Closes the bottom row. Its longest-lived form lasts a few hours."],
["Rf","Rutherfordium",267,2,4,7,null,null,"Made one atom at a time, and gone within a minute of arriving."],
["Db","Dubnium",268,2,5,7,null,null,"Survives a couple of minutes at best after it is put together."],
["Sg","Seaborgium",269,2,6,7,null,null,"Exists for a fraction of a minute. Named after a chemist, while he was alive."],
["Bh","Bohrium",270,2,7,7,null,null,"Assembled atom by atom, and gone in seconds."],
["Hs","Hassium",269,2,8,7,null,null,"A few hundred atoms have ever existed, total."],
["Mt","Meitnerium",278,2,9,7,null,null,"Named for Lise Meitner, who worked out how heavy atoms split."],
["Ds","Darmstadtium",281,2,10,7,null,null,"Named after the German town where it was first assembled."],
["Rg","Roentgenium",282,2,11,7,null,null,"Named for the man who noticed x-rays coming out of his equipment."],
["Cn","Copernicium",285,2,12,7,null,null,"It might be a gas at room temperature, but nobody can hold enough to check."],
["Nh","Nihonium",286,3,13,7,null,null,"Named after Japan, where its first three atoms were made."],
["Fl","Flerovium",289,3,14,7,null,null,"Vanishes in seconds. Predicted to be strangely unreactive for a metal."],
["Mc","Moscovium",290,3,15,7,null,null,"Named after Moscow. Perhaps a hundred atoms are known."],
["Lv","Livermorium",293,3,16,7,null,null,"Comes apart within milliseconds of being put together."],
["Ts","Tennessine",294,6,17,7,null,null,"Named after Tennessee, and the second heaviest element known."],
["Og","Oganesson",294,7,18,7,null,null,"The heaviest element ever made: a few atoms, each gone almost instantly."]
  ];

  /* ── build element objects ──────────────────────────────────────────────── */
  var ELS = RAW.map(function (r, i) {
    var z = i + 1, row, col;
    if (r[4] === 0) {                       /* f-block sits in its own two rows */
      row = (r[5] === 6) ? 9 : 10;
      col = 3 + (r[5] === 6 ? z - 57 : z - 89);
    } else { row = r[5]; col = r[4]; }
    return {
      z: z, sym: r[0], name: r[1], mass: r[2], cat: r[3],
      group: r[4], period: r[5], melt: r[6], boil: r[7], fact: r[8],
      row: row, col: col, el: null, phase: ""
    };
  });

  var TMAX = 6000, SMAX = 1000;
  var PHNAME = { s: "solid", l: "liquid", g: "gas", u: "never measured" };

  var state = { T: 293, sel: 79, q: "", cat: -1 };   /* index 79 = mercury */

  /* ── helpers ────────────────────────────────────────────────────────────── */
  function sliderToT(v) { var f = v / SMAX; return Math.round(f * f * TMAX); }
  function tToSlider(T) { return Math.round(Math.sqrt(Math.max(0, T) / TMAX) * SMAX); }
  function celsius(K) {
    var r = Math.round(K - 273.15);
    return (r < 0 ? "−" : "") + Math.abs(r);
  }
  /* screen readers make a mess of "293 K · −196 °C", so speak it in words */
  function spokenT(K) {
    var r = Math.round(K - 273.15);
    return K + " kelvin, " + (r < 0 ? "minus " + Math.abs(r) : r) + " degrees Celsius";
  }
  function phaseOf(e, T) {
    if (e.melt !== null && T < e.melt) return "s";
    if (e.melt === null) return "u";
    if (e.boil === null) return "u";
    return T < e.boil ? "l" : "g";
  }
  function kelvin(v) {
    if (v === null) return "—";
    return (v >= 1000 ? Math.round(v) : Math.round(v * 10) / 10) + " K";
  }
  function $(id) { return document.getElementById(id); }

  /* ── DOM refs ───────────────────────────────────────────────────────────── */
  var table = $("table"), legend = $("legend"), live = $("live");
  var qIn = $("q"), qClear = $("qClear"), qStatus = $("qStatus"), qCount = $("qCount");
  var tIn = $("temp"), tVal = $("tVal"), tPresets = $("tPresets");
  var counts = { s: $("cS"), l: $("cL"), g: $("cG"), u: $("cU") };
  var bars = { s: $("barS"), l: $("barL"), g: $("barG"), u: $("barU") };

  /* ── build the table ────────────────────────────────────────────────────── */
  var pos = {};
  ELS.forEach(function (e, i) {
    var b = document.createElement("button");
    b.type = "button";
    b.className = "el c" + e.cat;
    b.style.gridRow = e.row;
    b.style.gridColumn = e.col;
    b.tabIndex = -1;
    b.dataset.i = i;
    b.title = e.name + " · " + e.mass + " u";
    b.innerHTML = '<span class="n">' + e.z + '</span><span class="s">' + e.sym + "</span>";
    table.appendChild(b);
    e.el = b;
    pos[e.row + "," + e.col] = i;
  });

  var spacer = document.createElement("div");
  spacer.className = "spacer"; spacer.style.gridRow = 8; spacer.setAttribute("aria-hidden", "true");
  table.appendChild(spacer);

  [[6, "57–71"], [7, "89–103"]].forEach(function (m, k) {
    var d = document.createElement("div");
    d.className = "mark c" + (k === 0 ? 8 : 9);
    d.style.gridRow = m[0]; d.style.gridColumn = 3;
    d.textContent = m[1];
    d.setAttribute("aria-hidden", "true");
    table.appendChild(d);
  });

  /* ── legend ─────────────────────────────────────────────────────────────── */
  var catCounts = CATS.map(function () { return 0; });
  ELS.forEach(function (e) { catCounts[e.cat]++; });
  CATS.forEach(function (c, i) {
    var b = document.createElement("button");
    b.type = "button"; b.className = "lg c" + i;
    b.setAttribute("aria-pressed", "false");
    b.dataset.c = i;
    b.innerHTML = '<span class="sw" aria-hidden="true"></span><span>' + c[1] +
                  '</span><span class="ct">' + catCounts[i] + "</span>";
    legend.appendChild(b);
  });

  /* ── rendering ──────────────────────────────────────────────────────────── */
  function matches(e, q) {
    if (!q) return true;
    if (String(e.z) === q) return true;
    var s = e.sym.toLowerCase();
    if (s === q || s.indexOf(q) === 0) return true;
    if (e.name.toLowerCase().indexOf(q) !== -1) return true;
    if (CATS[e.cat][1].toLowerCase().indexOf(q) !== -1) return true;
    if (CATS[e.cat][0].indexOf(q) === 0) return true;
    if (PHNAME[e.phase].indexOf(q) !== -1) return true;
    return false;
  }

  function renderPhases() {
    var T = state.T, c = { s: 0, l: 0, g: 0, u: 0 };
    ELS.forEach(function (e) {
      var p = phaseOf(e, T);
      c[p]++;
      if (p !== e.phase) {
        e.phase = p;
        e.el.classList.remove("ph-s", "ph-l", "ph-g", "ph-u");
        e.el.classList.add("ph-" + p);
      }
    });
    for (var k in c) {
      counts[k].textContent = c[k];
      bars[k].style.width = (c[k] / ELS.length * 100).toFixed(2) + "%";
    }
    $("phHead").textContent = "At " + state.T + " K";
    tVal.innerHTML = state.T + " K<span class=\"u\"> · " + celsius(state.T) + " °C</span>";
    tIn.setAttribute("aria-valuetext", spokenT(state.T));
    Array.prototype.forEach.call(tPresets.children, function (ch) {
      ch.setAttribute("aria-pressed", Number(ch.dataset.k) === state.T ? "true" : "false");
    });
  }

  function renderFilter() {
    var q = state.q, n = 0, anyFilter = !!q || state.cat >= 0, ok = [], first = -1;
    ELS.forEach(function (e, i) {
      ok[i] = (state.cat < 0 || e.cat === state.cat) && matches(e, q);
      if (ok[i]) { n++; if (first < 0) first = i; }
    });
    var ring = !!q && n <= 8;                 /* only ring a small, findable set */
    ELS.forEach(function (e, i) {
      e.el.classList.toggle("dim", anyFilter && !ok[i]);
      e.el.classList.toggle("hit", ring && ok[i]);
    });
    qCount.textContent = n;
    qStatus.classList.remove("ok", "none");
    if (!anyFilter) {
      qStatus.textContent = "Family, phase, symbol or number.";
    } else if (n === 0) {
      qStatus.textContent = "No element matches that.";
      qStatus.classList.add("none");
    } else {
      qStatus.textContent = q ? n + " of 118 match “" + q + "”."
                               : CATS[state.cat][1] + ": " + n + " of 118.";
      qStatus.classList.add("ok");
    }
    Array.prototype.forEach.call(legend.children, function (b) {
      b.setAttribute("aria-pressed", Number(b.dataset.c) === state.cat ? "true" : "false");
    });
    qClear.hidden = !q;
    /* follow the filter: keep the detail card — and the roving tab stop — on a
       square the filter is actually showing. Searching "gold" should put gold in
       the card, not leave whatever was selected before stranded and dimmed. */
    if (anyFilter && first >= 0 && !ok[state.sel]) {
      state.sel = first;
      renderDetail(false);
    }
  }

  function renderLabels() {
    ELS.forEach(function (e) {
      e.el.setAttribute("aria-label",
        e.name + ", symbol " + e.sym + ", number " + e.z + ", " +
        CATS[e.cat][1].toLowerCase() + ", " +
        (e.phase === "u" ? "phase never measured" : PHNAME[e.phase] + " at " + state.T + " kelvin"));
    });
  }

  function renderDetail(announce) {
    var e = ELS[state.sel];
    ELS.forEach(function (x, i) {
      x.el.classList.toggle("sel", i === state.sel);
      x.el.tabIndex = i === state.sel ? 0 : -1;
    });
    $("dTile").className = "dtile c" + e.cat + " ph-" + e.phase;
    $("dNum").textContent = e.z;
    $("dSym").textContent = e.sym;
    $("dName").textContent = e.name;
    $("dCatName").textContent = CATS[e.cat][1];
    $("dSw").className = "sw c" + e.cat;
    $("dCat").className = "dcat c" + e.cat;
    $("dFact").textContent = e.fact;
    $("dMass").textContent = e.mass;
    if (e.group > 0) { $("dGroup").textContent = e.group; $("dGroupK").textContent = "group"; }
    else { $("dGroup").textContent = "f"; $("dGroupK").textContent = "block"; }
    $("dPeriod").textContent = e.period;
    $("dMelt").textContent = kelvin(e.melt);
    $("dBoil").textContent = kelvin(e.boil);
    $("dPhase").textContent = PHNAME[e.phase];
    $("dPhaseK").textContent = "at " + state.T + " K";
    if (announce) {
      live.textContent = e.name + ", " + e.sym + ", number " + e.z + ". " +
        (e.phase === "u" ? "Phase never measured." : PHNAME[e.phase] + " at " + state.T + " kelvin.");
    }
  }

  function renderAll(announce) {
    renderPhases();
    renderLabels();
    renderFilter();
    renderDetail(announce);
  }

  function select(i, announce, focus) {
    if (i < 0 || i >= ELS.length) return;
    state.sel = i;
    renderDetail(announce !== false);
    if (focus) ELS[i].el.focus();
  }

  function setT(T) {
    state.T = Math.max(0, Math.min(TMAX, Math.round(T)));
    renderPhases();
    renderLabels();
    if (state.q) renderFilter();
    renderDetail(false);
  }

  /* ── events ─────────────────────────────────────────────────────────────── */
  table.addEventListener("click", function (ev) {
    var b = ev.target.closest ? ev.target.closest(".el") : null;
    if (b && b.dataset.i) select(Number(b.dataset.i), true, false);
  });

  table.addEventListener("keydown", function (ev) {
    var b = ev.target.closest ? ev.target.closest(".el") : null;
    if (!b) return;
    var i = Number(b.dataset.i), e = ELS[i], d = null;
    if (ev.key === "ArrowRight") d = [0, 1];
    else if (ev.key === "ArrowLeft") d = [0, -1];
    else if (ev.key === "ArrowDown") d = [1, 0];
    else if (ev.key === "ArrowUp") d = [-1, 0];
    else if (ev.key === "Home") { ev.preventDefault(); select(0, true, true); return; }
    else if (ev.key === "End") { ev.preventDefault(); select(ELS.length - 1, true, true); return; }
    else return;
    ev.preventDefault();
    var r = e.row, c = e.col;
    for (var step = 0; step < 20; step++) {
      r += d[0]; c += d[1];
      if (r < 1 || r > 10 || c < 1 || c > 18) return;
      var hit = pos[r + "," + c];
      if (hit !== undefined) { select(hit, true, true); return; }
    }
  });

  qIn.addEventListener("input", function () {
    state.q = qIn.value.trim().toLowerCase();
    renderFilter();
  });
  qClear.addEventListener("click", function () {
    qIn.value = ""; state.q = ""; renderFilter(); qIn.focus();
  });

  tIn.addEventListener("input", function () { setT(sliderToT(Number(tIn.value))); });

  tPresets.addEventListener("click", function (ev) {
    var b = ev.target.closest ? ev.target.closest(".chip") : null;
    if (!b) return;
    var K = Number(b.dataset.k);
    tIn.value = tToSlider(K);
    setT(K);
    live.textContent = state.T + " kelvin. " + counts.s.textContent + " solid, " +
      counts.l.textContent + " liquid, " + counts.g.textContent + " gas.";
  });

  legend.addEventListener("click", function (ev) {
    var b = ev.target.closest ? ev.target.closest(".lg") : null;
    if (!b) return;
    var c = Number(b.dataset.c);
    state.cat = state.cat === c ? -1 : c;
    renderFilter();
  });

  $("reset").addEventListener("click", function () {
    qIn.value = ""; state.q = ""; state.cat = -1;
    tIn.value = tToSlider(293);
    state.sel = 79;
    setT(293);
    renderFilter();
    renderDetail(true);
  });

  /* ── theme: platform postMessage + standalone prefers-color-scheme ──────── */
  window.addEventListener("message", function (ev) {
    var d = ev.data;
    if (d && d.type === "sloprun:theme" && (d.theme === "light" || d.theme === "dark")) {
      document.documentElement.setAttribute("data-theme", d.theme);
    }
  });

  /* ── go ─────────────────────────────────────────────────────────────────── */
  tIn.value = tToSlider(state.T);
  renderAll(false);
})();
</script>
</body>
</html>

source-visible by construction · nothing is published here without its code

Comments

0 total

No comments yet. If you ran it, say what happened.

Report this post

Goes straight to the moderation queue. Enough independent reports and the post is suspended automatically until a human looks.

what is wrong