cee097b75fe4d1b29f0debd8af938ba6bf729d53
max
  Tue Aug 11 08:18:42 2026 -0700
hgBlat: new client-rendered BLAT search form and sortable results page, refs #37893

An opt-in modern alternative to the classic hyperlink output, gated by the
blatNewForm / blatNewPage hg.conf flags. hgBlat.c emits the hit data and form
config as inline JSON (hgBlatData / hgBlatFormData) into empty containers, and
hgBlat.js builds the UI: the search form, a sortable/filterable DataTable with
identity and query-coverage bars, a docked per-hit detail panel, a FASTA query
viewer, and share-link and rename-track modals.

- hgBlat.css: all styling for both pages, loaded via webIncludeResourceFile
(not injected from JS); layout uses CSS classes rather than inline styles.
- utils.js: setupGenomeSelector, the combobox genome picker the form uses.
- hgc.c: blatOldTracks / blatKeepResults - the "Keep results" checkbox lets a
user accumulate BLAT result tracks instead of each search replacing the last.
- hgConfCatalog: register the blatNewForm / blatNewFormBanner gates.
- blatExample.fa: the example query the form loads on demand.

Markup is built in JS (escaped values via htmlEncode, everything else set through
DOM properties); no HTML strings in the C or a template engine.

diff --git src/hg/htdocs/style/hgBlat.css src/hg/htdocs/style/hgBlat.css
new file mode 100644
index 00000000000..b3033e1f327
--- /dev/null
+++ src/hg/htdocs/style/hgBlat.css
@@ -0,0 +1,210 @@
+/* hgBlat.css - styling for the modern BLAT search form and results pages (hgBlat.js).
+ * UCSC house style (per the project's UCSC UI Style Guide): steel-blue section header, tan/white
+ * content, navy links with maroon hover, #dbe4ee table header, zebra rows, navy selection bar.
+ * Loaded on the new pages via webIncludeResourceFile("hgBlat.css"); the classic C-rendered pages
+ * emit their own small .blatBanner rule in hgBlat.c instead. */
+
+/* Plain white page background, matching the track-settings model (gb.css).  The framework body
+ * is cream (#FFF9D2) and the hgInside content table carries a BGCOLOR=#FFFEE8 attribute; override
+ * both to white so the results sit on one uniform white background. */
+body.cgi { background:#fff; }
+table.hgInside { background:#fff; }
+/* Main page header, styled like gb.css .gbTrackTitleBanner (hgGtexTrackSettings model): black
+ * title on the house gold.  The framework's #sectTtl already holds "<assembly> BLAT Results". */
+.subheadingBar { background:#eaca92; padding:9px 16px; margin:0; border:0; box-sizing:border-box; }
+.subheadingBar #sectTtl { color:#000; font-weight:700; font-size:18px;
+    display:flex; align-items:center; justify-content:space-between; flex-wrap:wrap; gap:12px; }
+#blatResults, #blatFormBox { --accent:#003a72; --accentHover:#8b1a1a; --navy:#0a2b6b; --ink:#1e2833;
+    --muted:#5b6572; --faint:#93a0ad; --line:#d0d0d0; --lineSoft:#ececec; --card:#ffffff;
+    --panel:#ffffff; --section:#4c759c; --headrow:#ededed; --headrowLine:#cccccc;
+    --sel:#cfe0f5; --hover:#f0f0f0; --stripe:#f7f7f7;
+    --title:#eaca92; --titleLine:#d9bd82; --titleSub:#5a4a24;
+    --btn:#ffffff; --btnLine:#999999; --btnText:#003a72; --btnHover:#eef2f7;
+    --btnDark:#0a2b6b; --btnDarkHover:#0a2350;
+    font-family:'Helvetica Neue',Helvetica,Arial,sans-serif; color:var(--ink); font-size:14px; }
+.blatCard { background:var(--card); border:1px solid var(--line); border-radius:0; overflow:hidden;
+    box-shadow:0 1px 2px rgba(20,40,70,.10); margin:12px 0 24px; }
+/* Page actions sit in the gold main header (#sectTtl); no separate toolbar. */
+.blatHeadActions { display:flex; gap:8px; align-items:center; flex-wrap:wrap; }
+.blatStrip { display:flex; align-items:center; gap:24px; flex-wrap:wrap;
+    padding:11px 18px; border-bottom:1px solid var(--line); background:var(--panel); }
+.blatStat { display:flex; flex-direction:column; gap:1px; }
+.blatStat .k { font-size:12px; color:var(--muted); font-weight:700; }
+.blatStat .v { font-size:14px; font-weight:700; color:var(--ink); }
+.blatDiv { width:1px; height:28px; background:var(--line); }
+.blatStripActions { margin-left:auto; display:flex; gap:8px; align-items:center; flex-wrap:wrap; }
+/* Buttons live both inside the card (#blatResults) and in the gold header (#sectTtl); scope to
+ * both ids so the framework's link styles (which would add an underline) can't beat them. */
+#blatResults .blatPill, #sectTtl .blatPill, #blatFormBox .blatPill,
+#blatResults .blatStripActions input[type=submit],
+#blatResults .blatStripActions input[type=button] {
+    font-family:inherit; font-size:13px; font-weight:700; color:var(--btnText); background:var(--btn);
+    border:1px solid var(--btnLine); border-radius:0; padding:5px 12px; cursor:pointer;
+    text-decoration:none; display:inline-block; line-height:1.5; }
+#blatResults .blatPill:hover, #sectTtl .blatPill:hover, #blatFormBox .blatPill:hover,
+#blatResults .blatStripActions input[type=submit]:hover,
+#blatResults .blatStripActions input[type=button]:hover { background:var(--btnHover); text-decoration:none; }
+#blatResults .blatPill.primary, #sectTtl .blatPill.primary,
+#blatFormBox .blatPill.primary { background:var(--btnDark); color:#fff; border-color:var(--btnDark); }
+#blatResults .blatPill.primary:hover, #sectTtl .blatPill.primary:hover,
+#blatFormBox .blatPill.primary:hover { background:var(--btnDarkHover); }
+/* In the gold header (#sectTtl) the framework's link styles would otherwise beat .blatPill, so
+ * re-assert the button look with id-level specificity. */
+#sectTtl .blatPill { color:#003a72; background:#fff; border:1px solid #999; text-decoration:none; }
+#sectTtl .blatPill:hover { background:#eef2f7; text-decoration:none; }
+#sectTtl .blatPill.primary { background:#0a2b6b; color:#fff; border-color:#0a2b6b; }
+#sectTtl .blatPill.primary:hover { background:#0a2350; }
+.blatShareIcon { vertical-align:-2px; margin-right:6px; }
+.blatShareBox { display:flex; align-items:center; gap:10px; flex-wrap:wrap;
+    padding:11px 18px; border-bottom:1px solid var(--line); background:var(--panel); }
+.blatShareMsg { font-size:14px; color:var(--muted); }
+.blatShareInput { flex:1; min-width:260px; font-size:13px; padding:5px 8px;
+    border:1px solid var(--btnLine); border-radius:0; background:#fff; }
+.blatBanner { background:#fbf3e2; border:1px solid var(--titleLine); padding:10px 14px;
+    margin:12px 0 0; font-size:14px; color:var(--ink); }
+.blatBanner a { color:var(--accent); }
+.blatBanner a:hover { color:var(--accentHover); }
+.blatSeqBar { flex:1 1 100%; display:flex; gap:8px; align-items:center; flex-wrap:wrap; }
+.blatSeqText { flex:1 1 100%; min-height:150px; margin-top:10px; padding:8px; white-space:pre;
+    font-family:'Roboto Mono','Courier New',monospace; font-size:13px; color:var(--ink);
+    border:1px solid var(--btnLine); background:#fff; overflow:auto; }
+#blatTable { font-size:13px; width:100%; border-collapse:collapse; }
+/* Let the long text columns wrap so the table shrinks to fit smaller screens, while the fixed
+ * bar columns keep a min-width so they never compress into an overlap; when even that won't fit,
+ * #blatTable_wrapper scrolls horizontally. */
+#blatTable td.blatPos, #blatTable td.queryCol { white-space:normal; word-break:break-word; }
+#blatTable td.scoreCol { min-width:118px; }
+#blatTable td.covCol { min-width:120px; }
+#blatTable thead th { background:var(--headrow); border-bottom:1px solid var(--headrowLine);
+    font-size:12px; color:var(--navy); font-weight:700; padding:8px 12px; white-space:nowrap; }
+#blatTable tbody td { padding:8px 12px; border-bottom:1px solid var(--lineSoft); white-space:nowrap;
+    vertical-align:middle; }
+#blatTable tbody tr { cursor:pointer; }
+#blatTable tbody tr:nth-child(even) { background:var(--stripe); }
+#blatTable tbody tr:hover { background:var(--hover); }
+#blatTable tbody tr.blatSel { background:var(--sel); box-shadow:inset 3px 0 0 var(--navy); }
+#blatTable td.num, #blatTable th.num { text-align:right; font-variant-numeric:tabular-nums; }
+#blatTable td.rankCol { color:var(--faint); }
+#blatTable td.strandCol { text-align:center; color:var(--muted); }
+#blatTable td.actionsCol a { color:var(--accent); font-weight:700; }
+#blatTable td.actionsCol .blatActSep { color:var(--line); margin:0 10px; }
+/* "New tab" shown as the usual box-with-arrow icon (via a CSS background so it isn't repeated in
+ * every row's markup), sitting right after the position link with just a space between them. */
+#blatTable a.blatNewTab { display:inline-block; width:11px; height:11px;
+    margin-left:4px; vertical-align:-1px; background-repeat:no-repeat; background-position:center;
+    background-size:contain; background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E%3Cpath fill='%23003a72' d='M320 0c-17.7 0-32 14.3-32 32s14.3 32 32 32h82.7L201.4 265.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L448 109.3V192c0 17.7 14.3 32 32 32s32-14.3 32-32V32c0-17.7-14.3-32-32-32H320zM80 32C35.8 32 0 67.8 0 112V432c0 44.2 35.8 80 80 80H400c44.2 0 80-35.8 80-80V320c0-17.7-14.3-32-32-32s-32 14.3-32 32V432c0 8.8-7.2 16-16 16H80c-8.8 0-16-7.2-16-16V112c0-8.8 7.2-16 16-16H192c17.7 0 32-14.3 32-32s-14.3-32-32-32H80z'/%3E%3C/svg%3E"); }
+#blatTable a.blatNewTab:hover { opacity:.65; }
+#blatTable a { color:var(--accent); text-decoration:none; }
+#blatTable a:hover { color:var(--accentHover); text-decoration:underline; }
+.blatRowHint { padding:11px 18px 2px; font-size:13px; color:var(--muted); }
+.blatLocus { max-width:280px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
+.chrNote { color:var(--faint); text-decoration:none; margin-left:4px; }
+.chrNote:hover { color:var(--accent); }
+.blatScoreWrap { display:flex; align-items:center; gap:9px; justify-content:flex-end; }
+.blatScoreVal { font-size:13px; font-weight:700; text-align:right; font-variant-numeric:tabular-nums; }
+.blatScoreBar { flex:0 0 54px; height:8px; background:var(--headrow); border:1px solid var(--headrowLine); overflow:hidden; }
+.blatScoreBar > i { display:block; height:100%; background:var(--accent); }
+.blatIdPct { font-size:13px; font-weight:700; font-variant-numeric:tabular-nums; }
+.blatCov { position:relative; display:block; width:150px; max-width:100%; height:10px; background:var(--headrow); border:1px solid var(--headrowLine); }
+.blatCov > i { position:absolute; top:0; bottom:0; background:var(--accent); }
+#blatTable_filter { float:left; margin:0 0 10px; }
+#blatTable_filter input { width:300px; max-width:55vw; border:1px solid var(--btnLine);
+    border-radius:0; padding:5px 9px; font-size:14px; }
+#blatTable_wrapper { padding:6px 0 4px; overflow-x:auto; }
+.blatDetail { border-bottom:1px solid var(--line); background:var(--panel); padding:14px 18px 16px; }
+.blatDetail .dhead { display:flex; align-items:baseline; gap:10px; margin-bottom:12px; flex-wrap:wrap; }
+.blatDetail .dhead .lab { font-size:13px; color:var(--muted); }
+.blatSelectHint { font-size:14px; color:var(--ink); }
+.blatSelectHint a { color:var(--accent); }
+.blatSelectHint a:hover { color:var(--accentHover); }
+.blatDetail .dhead .loc { font-size:14px; font-weight:700; color:var(--ink); }
+.blatDetailCard { display:flex; gap:26px; flex-wrap:wrap; padding:14px 16px; background:var(--card);
+    border:1px solid var(--line); border-radius:0; }
+.blatTiles { display:grid; grid-template-columns:repeat(4,auto); gap:14px 26px; }
+.blatTile .k { font-size:13px; color:var(--muted); cursor:help; }
+.blatTile .v { font-size:14px; font-weight:700; color:var(--ink); }
+.blatDetailActions { display:flex; gap:8px; margin-top:14px; flex-wrap:wrap; }
+/* Detail panel layout (was inline styles in blatDetailSkeleton). */
+.blatDetailCol { display:flex; flex-direction:column; gap:16px; min-width:250px; }
+.blatAlignBox { flex:1; min-width:320px; border-left:1px solid var(--line); padding-left:24px;
+    display:flex; flex-direction:column; justify-content:center; }
+.blatAlignBox .blatPill { align-self:flex-start; }
+.blatAlignText { font-size:14px; color:var(--muted); line-height:1.55; margin:8px 0 14px; max-width:360px; }
+/* Full-width message row inside the flex-wrap share box. */
+.blatShareFull { flex:1 1 100%; }
+/* Rename modal.  On this page the old C-emitted inline rename form (#renameFormItem / #renameForm)
+ * is replaced by the JS button + modal below, so keep the C markup hidden even though hgBlat.c
+ * flips its display to block after buildBigPsl (our !important beats that inline style). */
+#renameFormItem, #renameForm { display:none !important; }
+.blatModalBg { position:fixed; top:0; right:0; bottom:0; left:0; z-index:1000;
+    background:rgba(0,0,0,.4); display:flex; align-items:center; justify-content:center; }
+.blatModal { background:#fff; border:1px solid var(--line); border-radius:0;
+    box-shadow:0 4px 18px rgba(0,0,0,.28); padding:18px 20px; min-width:340px; max-width:92vw; }
+.blatModalTitle { font-size:16px; font-weight:700; color:var(--ink); margin-bottom:12px; }
+.blatModalText { font-size:13px; color:var(--ink); line-height:1.5; max-width:430px; margin-bottom:14px; }
+.blatModalText.blatModalNote { margin-top:14px; }
+.blatModalText a { color:var(--accent); }
+.blatModalText a:hover { color:var(--accentHover); }
+.blatModalLabel { display:block; font-size:12px; font-weight:700; color:var(--muted);
+    margin:10px 0 3px; }
+.blatModalInput { width:100%; box-sizing:border-box; border:1px solid var(--btnLine);
+    border-radius:0; padding:6px 8px; font-size:14px; font-family:inherit; background:#fff; }
+.blatModalBtns { display:flex; gap:8px; justify-content:flex-end; margin-top:18px; }
+
+/* ---- search form (blatFormBuild) -------------------------------------
+ * Shares the tokens, .blatPill buttons, .blatBanner and the gold .subheadingBar page header
+ * above with the results page, so the two pages read as one design. */
+#blatFormBox { font-family:'Helvetica Neue',Helvetica,Arial,sans-serif; color:var(--ink); font-size:14px; }
+#blatFormBox .blatFormGrid { display:grid; grid-template-columns:minmax(0,1fr) 300px; gap:28px;
+    align-items:start; margin:12px 0 24px; }
+@media (max-width:900px) { #blatFormBox .blatFormGrid { grid-template-columns:minmax(0,1fr); } }
+#blatFormBox .blatSection { background:var(--section); color:#fff; padding:6px 13px; font-size:15px;
+    font-weight:700; margin:0 0 14px; }
+/* flex-start, not flex-end: the genome field carries a "Current genome:" line under its input,
+ * so bottom-aligning the row would push Query type down by the height of that line. */
+#blatFormBox .blatRow { display:flex; align-items:flex-start; gap:16px; flex-wrap:wrap; margin-bottom:14px; }
+#blatFormBox .blatField { display:flex; flex-direction:column; gap:5px; }
+#blatFormBox .blatField > span { font-size:13px; font-weight:700; color:var(--muted); }
+#blatFormBox .blatField select { padding:6px 8px; font-size:14px; border:1px solid var(--btnLine);
+    border-radius:0; background:#fff; color:var(--ink); font-family:inherit; }
+#blatFormBox .blatGenomeSlot { flex:1 1 650px; min-width:260px; max-width:650px; }
+#blatFormBox .blatChecks { display:flex; align-items:center; gap:22px; flex-wrap:wrap; margin-bottom:16px;
+    font-size:14px; }
+#blatFormBox .blatCheck { display:flex; align-items:center; gap:6px; }
+#blatFormBox .blatCheck label { display:flex; align-items:center; gap:6px; cursor:pointer; }
+#blatFormBox .blatCheck input { margin:0; }
+#blatFormBox .blatInfo { display:inline-flex; align-items:center; cursor:help; }
+#blatFormBox .blatTabs { display:flex; border-bottom:1px solid var(--line); margin-bottom:12px; }
+#blatFormBox .blatTab { padding:8px 16px; font-size:14px; font-weight:700; cursor:pointer; background:none;
+    border:none; border-bottom:3px solid transparent; color:var(--accent); font-family:inherit; }
+#blatFormBox .blatTab.on { border-bottom-color:var(--title); color:var(--ink); }
+#blatFormBox .blatPaneHint { display:flex; justify-content:space-between; align-items:baseline; gap:16px;
+    flex-wrap:wrap; font-size:13px; color:var(--muted); margin-bottom:6px; }
+#blatFormBox textarea.blatSeq { width:100%; height:200px; resize:vertical; padding:9px 11px; box-sizing:border-box;
+    font-family:'Roboto Mono','Courier New',monospace; font-size:13px; line-height:1.6;
+    border:1px solid var(--btnLine); border-radius:0; color:var(--ink); background:#fff; }
+#blatFormBox .blatCount { display:flex; justify-content:space-between; align-items:center; gap:16px;
+    padding:6px 1px 0; font-size:13px; color:var(--muted); }
+#blatFormBox .blatCount .over { color:#b1301f; font-weight:700; }
+#blatFormBox .blatDrop { border:2px dashed var(--headrowLine); background:#fafafa; padding:30px 20px;
+    text-align:center; }
+#blatFormBox .blatDrop.hot { background:var(--hover); border-color:var(--accent); }
+#blatFormBox .blatDropTitle { font-size:15px; font-weight:700; color:var(--accent); margin-bottom:4px; }
+#blatFormBox .blatDropSub { font-size:13px; color:var(--muted); margin-bottom:14px; }
+#blatFormBox .blatFileName { margin-top:12px; font-size:13px; color:var(--ink); font-weight:700; }
+#blatFormBox .blatActions { display:flex; align-items:center; gap:10px; flex-wrap:wrap;
+    border-top:1px solid var(--line); padding-top:16px; margin-top:16px; }
+#blatFormBox .blatCard { border:1px solid var(--line); padding:14px 16px; background:var(--card); }
+#blatFormBox .blatCard + .blatCard { margin-top:16px; }
+#blatFormBox .blatCard h3 { margin:0 0 9px; font-size:14px; font-weight:700; color:var(--accent); }
+#blatFormBox .blatCard div { font-size:13px; line-height:1.5; color:var(--ink); }
+#blatFormBox .blatCard div + div { margin-top:9px; }
+#blatFormBox .blatCard a { color:var(--accent); }
+#blatFormBox .blatCard a:hover { color:var(--accentHover); }
+#blatFormBox .blatLimitRow { display:flex; justify-content:space-between; gap:12px; padding-bottom:7px;
+    border-bottom:1px solid var(--lineSoft); font-size:13px; }
+/* The genome search bar is emitted by hgBlat.c (printGenomeSearchBar) and relocated into the
+ * layout by JS, so it keeps its real autocomplete over every assembly. */
+/* The genome picker is setupGenomeSelector (utils.js), which builds its own combobox; only
+ * its width is a page concern.  HGStyle.css does not style .gsWrap, so nothing to override. */
+#blatFormBox .gsWrap { max-width:650px; }