Project import generated by Copybara.

GitOrigin-RevId: d9e9e3fb4e31372ec1fb43b178994ca78fa8fe70
diff --git a/templates/features/activity-body.ezt b/templates/features/activity-body.ezt
new file mode 100644
index 0000000..640b1d3
--- /dev/null
+++ b/templates/features/activity-body.ezt
@@ -0,0 +1,9 @@
+[# This template is used to pre-render the title of an activity so that it can
+   later be accessed as activity.escaped_title.
+]
+
+[is activity_type "ProjectIssueUpdate"]
+  [include "updates-issueupdate-body.ezt"]
+[else]
+  Body?
+[end]
diff --git a/templates/features/activity-title.ezt b/templates/features/activity-title.ezt
new file mode 100644
index 0000000..c7e2f2f
--- /dev/null
+++ b/templates/features/activity-title.ezt
@@ -0,0 +1,9 @@
+[# This template is used to pre-render the title of an activity so that it can
+   later be accessed as activity.escaped_title.
+]
+
+[is activity_type "ProjectIssueUpdate"]
+  [include "updates-issueupdate-title.ezt"]
+[else]
+  title?
+[end]
diff --git a/templates/features/auto-ping-email.ezt b/templates/features/auto-ping-email.ezt
new file mode 100644
index 0000000..47b9f29
--- /dev/null
+++ b/templates/features/auto-ping-email.ezt
@@ -0,0 +1,11 @@
+[ping_comment_content]
+[detail_url]
+
+[for fields][if-any fields.docstring]
+[fields.field_name] field description:
+  [fields.docstring]
+[end][end]
+
+[# TODO(jrobbins): component triage notes]
+[# TODO(jrobbins): hotlist triage notes]
+[# TODO(jrobbins): context comment that set the date value]
diff --git a/templates/features/cues-conduct.ezt b/templates/features/cues-conduct.ezt
new file mode 100644
index 0000000..83a026e
--- /dev/null
+++ b/templates/features/cues-conduct.ezt
@@ -0,0 +1,18 @@
+[define show_code_of_conduct]False[end]
+[is cue "privacy_click_through"][define show_code_of_conduct]True[end][end]
+[is cue "code_of_conduct"][define show_code_of_conduct]True[end][end]
+
+[define code_of_conduct_url]https://chromium.googlesource.com/chromium/src/+/main/CODE_OF_CONDUCT.md[end]
+[is projectname "fuchsia"]
+  [define code_of_conduct_url]https://fuchsia.dev/fuchsia-src/CODE_OF_CONDUCT[end]
+[end]
+
+[is show_code_of_conduct "True"]
+ <table border="0" cellspacing="0" cellpadding="0" class="cue" style="margin: 2px">
+  <tr><td><span>
+      Please keep discussions respectful and constructive.
+      See our <a href="[code_of_conduct_url]" target="_blank">code of conduct</a>.
+      <a href="#" title="Don't show this message again" style="margin-left: 1em" class="dismiss_cue x_icon"></a>
+  </span></td></tr>
+ </table>
+[end]
\ No newline at end of file
diff --git a/templates/features/cues.ezt b/templates/features/cues.ezt
new file mode 100644
index 0000000..5cf059d
--- /dev/null
+++ b/templates/features/cues.ezt
@@ -0,0 +1,178 @@
+[if-any cue account_cue]
+
+[# Do not show cue if there is an alert shown on the page.]
+[if-any alerts.show][else]
+
+
+[# Dialog box for privacy settings.]
+[is cue "privacy_click_through"]
+  <div class="scrim cue">
+    <div id="privacy_dialog">
+      <h2>Email display settings</h2>
+
+      <p>There is a <a href="/hosting/settings" title="Settings"
+      class="dismiss_cue">setting</a> to control how your email
+      address appears on comments and issues that you post.
+
+      [if-any is_privileged_domain_user]
+        Since you are an integral part of this community, that setting
+        defaults to showing your full email address.</p>
+
+        <p>Also, you are being trusted to view email addresses of
+        non-members who post comments in your projects.  Please use
+        those addresses only to request additional information about
+        the posted comments, and do not share other users' email
+        addresses beyond the site.</p>
+      [else]
+        Project members will always see your full email address.  By
+        default, other users who visit the site will see an
+        abbreviated version of your email address.</p>
+
+        <p>If you do not wish your email address to be shared, there
+        are other ways to <a
+        href="http://www.chromium.org/getting-involved">get
+        involved</a> in the community.  To report a problem when using
+        the Chrome browser, you may use the "Report an issue..."  item
+        on the "Help" menu.</p>
+      [end]
+
+      <div class="actions">
+        <a href="#" title="Got it" class="dismiss_cue">GOT IT</a>
+      </div>
+    </div>
+  </div>
+
+[else][if-any account_cue]
+
+  <table id="alert-table" align="center" border="0" cellspacing="0" cellpadding="0">
+   <tr><td class="notice" id="notice">
+    [# Cue card to warn users who are using a child account.]
+    [is account_cue "switch_to_parent_account"]
+        <div>You are signed in to a linked account.</div>
+	<a href="[login_url]">Switch to [parent_email]</a>.
+    [end]
+  </td></tr>
+ </table>
+
+[else][is cue "code_of_conduct"]
+ [# Note: code-of-conduct cue card is implemented in cues-conduct.ezt which is
+    included from forms where users post text.]
+
+[else]
+
+ <table align="center" border="0" cellspacing="0" cellpadding="0" class="cue">
+  <tr><td><span>
+    [# Cue cards to teach users how to join a project.]
+    [is cue "how_to_join_project"]
+      <b>How-to:</b>
+      Join this project by contacting the project owners.
+    [end]
+
+    [# Cue card to teach users how to search for numbers in the issue tracker.]
+    [is cue "search_for_numbers"]
+     [if-any jump_local_id]
+       <b>Tip:</b>
+       To find issues containing "[jump_local_id]", use quotes.
+     [end]
+    [end]
+
+    [# Cue card to teach users how to search for numbers in the issue tracker.]
+    [is cue "dit_keystrokes"]
+      <b>Tip:</b>
+      Press <b>Esc</b> then <b style="font-size:130%"><tt>?</tt></b> for keyboard shortcuts.
+    [end]
+
+    [# Cue card to teach users that italics mean derived values in the issue tracker.]
+    [is cue "italics_mean_derived"]
+      <b>Note:</b>
+      <i>Italics</i> mean that a value was derived by a filter rule.
+      <a href="http://code.google.com/p/monorail/wiki/FilterRules">Learn more</a>
+    [end]
+
+    [# Teach users that color blocks mean that an issue participant may not be available.]
+    [is cue "availability_msgs"]
+      <b>Note:</b>
+      Color blocks (like <span class="availability_unsure" style="padding:0">&#9608;</span> or
+      <span class="availability_never" style="padding:0">&#9608;</span>)
+      mean that a user may not be available.  Tooltip shows the reason.
+    [end]
+
+    [# Cue card to teach users that full-text indexing takes time.]
+    [is cue "stale_fulltext"]
+      <b>Note:</b>
+      Searching for text in issues may show results that are a few minutes out of date.
+    [end]
+
+    [# Cue cards to improve discoverability of people roles.]
+    [is cue "document_team_duties"]
+     [if-any read_only][else]
+       <b>Tip:</b>
+       Document <a href="people/list">each teammate's project duties</a>.
+     [end]
+    [end]
+
+    [# Cue cards to explain grid mode.]
+    [is cue "showing_ids_instead_of_tiles"]
+       <b>Note:</b>
+       Grid mode automatically switches to displaying IDs when there are many results.
+    [end]
+
+    [# Cue cards to explain ownermodified, statusmodified, and componentmodified.]
+    [is cue "issue_timestamps"]
+       <b>Note:</b>
+       ownermodified, statusmodified, and componentmodified are the times at which
+       an issue's owner, status or component were changed.
+    [end]
+
+    [# Cue card to remind the user that they have set a vacation message.]
+    [is cue "you_are_on_vacation"]
+       <b>Note:</b>
+       Your <a href="/hosting/settings">vacation message</a> is set to:
+       "[logged_in_user.avail_message_short]".
+    [end]
+
+    [# Cue card to inform user that email to them bounced and that they must reset.]
+    [is cue "your_email_bounced"]
+       <b>Action required:</b>
+       An email to you bounced.  Once you can reliably receive email, clear the
+       <a href="[logged_in_user.profile_url]">bouncing status</a>.
+    [end]
+
+    [# Cue card to tell users what it means to star a hotlist]
+    [is cue "explain_hotlist_starring"]
+        <b>Note:</b>
+        Starring a hotlist will not cc you on any updates.
+        It simply adds the hotlist to your Starred hotlists category on this
+        page so that you can conveniently revisit it.
+    [end]
+
+   </span>
+    [# Link to dismiss the cue card.]
+    [if-any logged_in_user]
+     [if-any read_only][else]
+      <a href="#" title="Don't show this message again" style="margin-left: 1em" class="dismiss_cue x_icon"></a>
+     [end]
+    [end]
+  </td></tr>
+ </table>
+[end]
+[end]
+[end]
+
+
+ <script type="text/javascript" nonce="[nonce]">
+runOnLoad(function() {
+  var dismissLinks = document.querySelectorAll(".dismiss_cue");
+  for (var i = 0; i < dismissLinks.length; ++i) {
+   var dismissCue = dismissLinks[[]i];
+     dismissCue.addEventListener("click", function(event) {
+         _CS_dismissCue("[format "js"][cue][end]");
+         if (this.getAttribute("href") === "#")
+           event.preventDefault();
+     });
+  }
+});
+ </script>
+
+[end]
+[end]
diff --git a/templates/features/filterrules-preview.ezt b/templates/features/filterrules-preview.ezt
new file mode 100644
index 0000000..adbd7d0
--- /dev/null
+++ b/templates/features/filterrules-preview.ezt
@@ -0,0 +1,8 @@
+<div id="preview_filterrules_area" style="display:none">
+  Filter rules and components will add:
+  <div id="preview_filterrules_labels"></div>
+  <div id="preview_filterrules_owner"></div>
+  <div id="preview_filterrules_ccs"></div>
+  <div id="preview_filterrules_warnings"></div>
+  <div id="preview_filterrules_errors"></div>
+</div>
diff --git a/templates/features/hotlist-create-page.ezt b/templates/features/hotlist-create-page.ezt
new file mode 100644
index 0000000..16b6935
--- /dev/null
+++ b/templates/features/hotlist-create-page.ezt
@@ -0,0 +1,64 @@
+[define title]Create a new hotlist[end]
+[define category_css]css/ph_detail.css[end]
+[include "../framework/header.ezt" "hidetabs"]
+
+<h2>Create a hotlist</h2>
+
+<form action="createHotlist.do" method="POST" id="create_hotlist_form"
+      style="margin: 1em;">
+  <input type="hidden" name="token" value="[form_token]">
+
+  <label for="hotlistname">Hotlist Name:</label><br>
+  <input type="text" id="hotlistname" name="hotlistname" size="30" autocomplete="off"
+         value="[initial_name]">
+  <span class="graytext">Example: My-Hotlist-Name</span>
+  <div class="fielderror">&nbsp;
+    <span id="hotlistnamefeedback">
+       [if-any errors.hotlistname][errors.hotlistname][end]
+    </span>
+  </div>
+
+  <label for="summary">Summary</label><br>
+  <input type="text" id="summary" name="summary" size="75" autocomplete="off"
+         value="[initial_summary]">
+  <div class="fielderror">&nbsp;
+    <span id="summaryfeedback">
+      [if-any errors.summary][errors.summary][end]
+    </span>
+  </div>
+
+  <label for="description">Description</label><br>
+  <textarea id="description" name="description" rows="20" cols="90" wrap="soft">[initial_description]</textarea>
+  <br><br>
+
+  <div>
+    <span>Owner: [logged_in_user.email]</span>
+    <div class="graytext">
+    You will be the owner of this hotlist with permission to edit everything
+    </div>
+  </div>
+  <br>
+
+  <label for="editors">Editors</label><br>
+  <input type="text" id="editors" name="editors" size="75" autocomplete="off"
+  value="[initial_editors]">
+  <span class="graytext">Example: user@email.com, example@email.com</span>
+  <div class="graytext">Editors may add, remove, or rank issues</div>
+  <div class="fielderror">&nbsp;
+    <span id="editorsfeedback">
+      [if-any errors.editors][errors.editors][end]
+    <span>
+  </div>
+
+  <label for="privacy">Viewable by:</label>
+  <select name="is_private" id="privacy">
+    <option disabled="disabled">Select an access level...</option>
+    <option value="no">Anyone on the internet</option>
+    <option value="yes" selected="selected">Hotlist members</option>
+  </select>
+  <br><br>
+
+  <input type="submit" value="Create hotlist">
+</form>
+
+[include "../framework/footer.ezt"]
diff --git a/templates/features/hotlist-details-page.ezt b/templates/features/hotlist-details-page.ezt
new file mode 100644
index 0000000..194f6bd
--- /dev/null
+++ b/templates/features/hotlist-details-page.ezt
@@ -0,0 +1,72 @@
+[define category_css]css/ph_detail.css[end]
+[include "../framework/header.ezt" "showtabs"]
+
+[if-any read_only cant_administer_hotlist][include "read-only-hotlist-details-page.ezt"]
+[else]
+
+<form id="edithotlist" action="details.do" method="POST" autocomplete="off" enctype="multipart/form-data">
+  <input type="hidden" name="token" value="[form_token]">
+
+<h4>Hotlist settings</h4>
+
+<div class="section">
+  Hotlist name:<br>
+  <input type="text" id="name" name="name" size="75" value="[initial_name]"><br>
+  <div class="fielderror">&nbsp;
+    <span id="namefeedback">[if-any errors.name][errors.name][end]</span>
+  </div>
+
+  Hotlist summary:<br>
+  <input type="text" id="summary" name="summary" size="75" value="[initial_summary]"><br>
+  <div class="fielderror">&nbsp;
+    <span id="summaryfeedback">[if-any errors.summary][errors.summary][end]</span>
+  </div>
+
+  Hotlist description:<br>
+  <textarea id="description" name="description" rows="20" cols="90" wrap="soft"
+  	    >[initial_description]</textarea><br>
+</div>
+
+<h4>Hotlist defaults</h4>
+
+<div class="section">
+  Default columns shows in list view:<br/>
+  <input type="text" id="default_col_spec" name="default_col_spec" size="75" value="[initial_default_col_spec]"><br>
+  <div class="fielderror">&nbsp;
+    <span id="default_col_specfeedback">[if-any errors.default_col][errors.default_col][end]</span>
+  </div>
+
+  [# TODO(jojwang): add default issues per page]
+</div>
+
+<h4>Hotlist access</h4>
+
+<div class="section">
+  <select name="is_private" id="is_private">
+    <option disabled="disabled">Select an access level...</option>
+    <option value="no" [if-any initial_is_private][else]selected="selected"[end]>Anyone on the Internet</option>
+    <option value="yes" [if-any initial_is_private]selected="selected"[else][end]>Members only</option>
+  </select>
+  <p>Individual issues in the list can only be seen by users who can normally see them. The privacy status of an issue is considered when it is being displayed (or not displayed) in a hotlist.</p>
+</div>
+
+
+  <input type="hidden" id="delete" name="deletestate" value="false">
+  <input type="submit" id="savechanges" name="btn" value="Save changes" class="submit">
+  <input type="button" id="deletehotlist" name="btn" value="Delete hotlist" class="submit">
+
+</form>
+
+[include "../framework/footer-script.ezt"]
+
+<script type="text/javascript" nonce="[nonce]">
+runOnLoad(function() {
+  $('deletehotlist').addEventListener('click', function () {
+    HTL_deleteHotlist($('edithotlist'));
+  });
+});
+</script>
+
+[end]
+
+[include "../framework/footer.ezt"]
diff --git a/templates/features/hotlist-issues-body.ezt b/templates/features/hotlist-issues-body.ezt
new file mode 100644
index 0000000..3e9406d
--- /dev/null
+++ b/templates/features/hotlist-issues-body.ezt
@@ -0,0 +1,115 @@
+[for panels][# There will always be exactly one panel.]
+ [include "../tracker/issue-list-headings.ezt"]
+[end]
+
+[if-any table_data][else]
+<tr>
+  <td colspan="40" class="id">
+   <div style="padding: 3em; text-align: center">
+       This hotlist currently has no issues.<br>
+     [if-any owner_permissions editor_permissions]
+     Select 'Add issues...' in the above 'Actions...' dropdown menu to add some.
+     [end]
+    </div>
+   </td>
+  </tr>
+[end]
+
+
+
+<script type="text/javascript" nonce="[nonce]">
+runOnLoad(function(){
+  [if-any table_data]
+    var tableData = [[]
+    [for table_data]
+      {
+      "group":
+      [if-any table_data.group][if-any table_data.group.cells]
+        {
+        "rowsInGroup": "[table_data.group.rows_in_group]",
+        "cells": [[]
+        [for table_data.group.cells]
+          {
+          "groupName": "[table_data.group.cells.group_name]",
+          "values": [[]
+          [for table_data.group.cells.values]
+            {
+            "item": [if-any table_data.group.cells.values.item]"[format "js"][table_data.group.cells.values.item][end]"[else]"None"[end],
+            }[if-index table_data.group.cells.values last][else],[end]
+          [end]
+          ],
+          }[if-index table_data.group.cells last][else],[end]
+        [end]
+        ],
+        },
+      [else]"no",[end][else]"no",[end]
+      "cells" : [[]
+      [for table_data.cells]
+        {
+        "type": "[table_data.cells.type]",
+        "values": [[]
+        [for table_data.cells.values]
+          {
+            [is table_data.cells.type "issues"]
+              "id": "[format "js"][table_data.cells.values.item.id][end]",
+              "href": "[format "js"][table_data.cells.values.item.href][end]",
+              "title": "[format "js"][table_data.cells.values.item.title][end]",
+              "closed": "[format "js"][table_data.cells.values.item.closed][end]",
+            [else]
+              "item": "[format "js"][table_data.cells.values.item][end]",
+            [end]
+          "isDerived": "[table_data.cells.values.is_derived]",
+          }[if-index table_data.cells.values last][else],[end]
+        [end]
+        ],
+        "colIndex": "[table_data.cells.col_index]",
+        "align": "[table_data.cells.align]",
+        "noWrap": "[table_data.cells.NOWRAP]",
+        "nonColLabels": [[]
+        [for table_data.cells.non_column_labels]
+          {
+          "value": "[format "js"][table_data.cells.non_column_labels.value][end]",
+          "isDerived": "[table_data.cells.non_column_labels.is_derived]",
+          }[if-index table_data.cells.non_column_labels last][else],[end]
+        [end]
+        ],
+        }[if-index table_data.cells last][else],[end]
+      [end]
+      ],
+      "issueRef": "[table_data.issue_ref]",
+      "idx": "[table_data.idx]",
+      "projectName": "[table_data.project_name]",
+      "localID": "[table_data.local_id]",
+      "projectURL": [format "js"]"[table_data.project_url]"[end],
+      "issueID": "[table_data.issue_id]",
+      "isStarred": "[table_data.starred]",
+      "issueCleanURL": [format "js"]"[table_data.issue_clean_url]"[end],
+      "issueContextURL": [format "js"]"[table_data.issue_ctx_url]"[end],
+      }[if-index table_data last][else],[end]
+    [end]
+    ];
+
+    var pageSettings = {
+    "cursor": "[cursor]",
+    "userLoggedIn": "[if-any logged_in_user]yes[end]",
+    "ownerPerm": "[owner_permissions]",
+    "editorPerm": "[editor_permissions]",
+    "isCrossProject": "[is_cross_project]",
+    "readOnly": "[read_only]",
+    "allowRerank": "[allow_rerank]",
+    "hotlistID": "[hotlist_id]",
+    "colSpec": "[col_spec]",
+    "can": "[can]"
+    };
+
+    renderHotlistTable(tableData, pageSettings);
+    [if-any allow_rerank]
+    activateDragDrop(tableData, pageSettings, "[hotlist_id]");
+    [end]
+  [else]
+  [end]
+});
+</script>
+
+<script type="text/javascript" defer src="[version_base]/static/js/tracker/render-hotlist-table.js" nonce="[nonce]"></script>
+
diff --git a/templates/features/hotlist-issues-page.ezt b/templates/features/hotlist-issues-page.ezt
new file mode 100644
index 0000000..79898e0
--- /dev/null
+++ b/templates/features/hotlist-issues-page.ezt
@@ -0,0 +1,120 @@
+[define title]Hotlist [hotlist.name][end]
+[define category_css]css/ph_list.css[end]
+[define category2_css]css/ph_detail.css[end]
+[include "../framework/header.ezt" "showtabs"]
+
+<table width="100%" cellpadding="0" cellspacing="0" border="0" class="hotlist-issues-page" id="meta-container"
+       style="margin: 1em">
+<tbody class="collapse">
+  <tr>
+    <td nowrap="nowrap" style="min-width:9em;" class="sidebar">
+      <div style="text-align: center">
+       [if-any read_only][else]
+         <a id="hotlist_star"
+         style="color:[if-any hotlist.is_starred]cornflowerblue[else]gray[end]"
+         title="[if-any hotlist.is_starred]Un-s[else]S[end]tar this hotlist">
+         [if-any hotlist.is_starred]&#9733;[else]&#9734;[end]
+         </a>
+       [end]
+       Followed by [hotlist.num_followers]
+      </div>
+      <div id="meta-float">
+        [include "hotlist-meta-part.ezt"]
+      </div>
+    </td>
+    <td width="80%" class="vt" style="padding-left: 1em">
+      <h1 style="margin-top: 0"><a href="[hotlist.url]">Hotlist [hotlist.name]</a></h1>
+      <div>Summary: [hotlist.summary]</div>
+      Description: [hotlist.description]
+     </td>
+  </tr>
+
+
+</tbody>
+</table>
+
+<div id="colcontrol">
+
+  <span id="qq"><input type="hidden" id="searchq" name="q"
+                            value="[query]" autocomplete="off" ignore-dirty></span>
+       [if-any sortspec]<input type="hidden" id="sort" name="sort" value="[sortspec]">[end]
+       [if-any groupby]<input type="hidden" id="groupby" name="groupby" value="[groupby]">[end]
+       [if-any colspec]<span id="search_colspec"><input type="hidden" name="colspec" value="[colspec]"></span>[end]
+       <input type="hidden" id="hotlist_name" value="[hotlist.name]"></input>
+  <input type="hidden"  id="can" value="[can]"></span>
+
+  [if-any grid_mode]
+   [include "../tracker/issue-grid-controls-top.ezt"]
+  [end]
+
+  [if-any list_mode]
+   [include "../tracker/issue-list-controls-top.ezt"]
+  [end]
+
+  [if-any chart_mode]
+   [include "../tracker/issue-chart-controls-top.ezt"]
+  [end]
+
+  [include "../tracker/issue-hovercard.ezt"] [# TODO(jojwang): no hovercard appears right now]
+
+  <div id="cursorarea">
+  <table cellspacing="0" cellpadding="2" border="0" class="results striped drag_container" id="resultstable" width="100%">
+
+    [if-any grid_mode]
+     [include "../tracker/issue-grid-body.ezt"]
+    [end]
+
+    [if-any list_mode]
+     [include "hotlist-issues-body.ezt"]
+    [end]
+
+    [if-any chart_mode]
+     [include "../tracker/issue-chart-body.ezt" "testparam"]
+    [end]
+
+  </table>
+  </div>
+
+  [if-any list_mode]
+    [include "../tracker/issue-list-controls-bottom.ezt"]
+    [for panels]
+      [include "../tracker/issue-list-menus.ezt"]
+    [end]
+  [end]
+</div>
+
+[if-any grid_mode][else]
+  [include "../tracker/issue-list-js.ezt" "hotlist"]
+[end]
+
+<script type="text/javascript" nonce="[nonce]">
+runOnLoad(function() {
+  if ($("hotlist_star")) {
+    [# The user viewing this page wants to star this hotlist]
+    $("hotlist_star").addEventListener("click", function () {
+       _TKR_toggleStar($("hotlist_star"), null, null, null, "[hotlist_id]");
+    });
+  }
+
+
+    window.addEventListener("beforeunload", function(e) {
+      var selectedElement = document.activeElement;
+      if (selectedElement.classList.contains("itemnote")){
+        saveNote(selectedElement, "[hotlist_id]");
+      }
+      return;
+    });
+
+    $("hide-closed").addEventListener("click", function(e) {
+      HTL_toggleIssuesShown(e.target);
+    });
+    $("show-all").addEventListener("click", function(e) {
+      HTL_toggleIssuesShown(e.target);
+    });
+
+});
+</script>
+
+[#TODO(jojwang):make pretty]
+
+[include "../framework/footer.ezt"]
diff --git a/templates/features/hotlist-meta-part.ezt b/templates/features/hotlist-meta-part.ezt
new file mode 100644
index 0000000..0b865c6
--- /dev/null
+++ b/templates/features/hotlist-meta-part.ezt
@@ -0,0 +1,41 @@
+<table cellspacing="0" cellpadding="0">
+  <tr><th align="left" style="padding-right:.3em">Owners:</th>
+    <td width="100%">
+      <div style="margin-left:1em">
+      [if-any hotlist.owners]
+      [for hotlist.owners]
+      [include "../framework/user-link.ezt" hotlist.owners]
+      [end]
+      [end]
+      </div>
+    </td>
+  </tr>
+  <tr><th alight="left" style="padding-right:.3em">Members:</th>
+    <td width="100%">
+      <div style="margin-left:1em">
+      [if-any hotlist.editors]
+      [for hotlist.editors]
+      [include "../framework/user-link.ezt" hotlist.editors]
+      [end]
+      [end]
+      </div>
+    </td>
+  </tr>
+  <tr><th align="left" style="padding-right:.3em">Access:</th>
+    <td width="100%">
+      <div style="margin-left:1em">
+      [if-any hotlist.access_is_private]Private[else]Public[end]
+      </div>
+    </td>
+  </tr>
+  <tr><th align="left">Issues:</th>
+    <td width="100%">
+    <form>
+      <input type="radio" id="hide-closed" name="toggleissues" value="2" [is can "2"]checked[end]>Open<br>
+      <input type="radio" id="show-all" name="toggleissues" value="1" [is can "1"]checked[end]>All<br>
+    </form>
+    </form>
+    </td>
+  </tr>
+</table>
+
diff --git a/templates/features/inboundemail-banned.ezt b/templates/features/inboundemail-banned.ezt
new file mode 100644
index 0000000..f241fef
--- /dev/null
+++ b/templates/features/inboundemail-banned.ezt
@@ -0,0 +1,7 @@
+Subject: You are banned from using this issue tracker
+
+The email message you sent to [project_addr]
+was not processed because your account, [sender_addr],
+has been banned from using this issue tracker.
+
+[include "inboundemail-error-footer-part.ezt"]
diff --git a/templates/features/inboundemail-body-too-long.ezt b/templates/features/inboundemail-body-too-long.ezt
new file mode 100644
index 0000000..515e157
--- /dev/null
+++ b/templates/features/inboundemail-body-too-long.ezt
@@ -0,0 +1,6 @@
+Subject: Email body too long
+
+The email message you sent to [project_addr]
+was not processed because it was too large.
+
+[include "inboundemail-error-footer-part.ezt"]
diff --git a/templates/features/inboundemail-error-footer-part.ezt b/templates/features/inboundemail-error-footer-part.ezt
new file mode 100644
index 0000000..e86cbeb
--- /dev/null
+++ b/templates/features/inboundemail-error-footer-part.ezt
@@ -0,0 +1,2 @@
+To learn more, please visit:
+https://chromium.googlesource.com/infra/infra/+/main/doc/users/index.md
diff --git a/templates/features/inboundemail-no-account.ezt b/templates/features/inboundemail-no-account.ezt
new file mode 100644
index 0000000..0b7392d
--- /dev/null
+++ b/templates/features/inboundemail-no-account.ezt
@@ -0,0 +1,9 @@
+Subject: Could not determine account of sender
+
+The email message you sent to [project_addr]
+was not processed because your address, [sender_addr],
+does not correspond to an account known to the server.
+You must send from an email address that has already
+been used to interact with the issue tracker web UI.
+
+[include "inboundemail-error-footer-part.ezt"]
diff --git a/templates/features/inboundemail-no-artifact.ezt b/templates/features/inboundemail-no-artifact.ezt
new file mode 100644
index 0000000..bbc93fc
--- /dev/null
+++ b/templates/features/inboundemail-no-artifact.ezt
@@ -0,0 +1,7 @@
+Subject: Could not find [artifact_phrase] in project [project_name]
+
+The email message you sent to [project_addr]
+was not processed because [artifact_phrase] does
+not exist in project [project_name].
+
+[include "inboundemail-error-footer-part.ezt"]
diff --git a/templates/features/inboundemail-no-perms.ezt b/templates/features/inboundemail-no-perms.ezt
new file mode 100644
index 0000000..5fe57dc
--- /dev/null
+++ b/templates/features/inboundemail-no-perms.ezt
@@ -0,0 +1,8 @@
+Subject: User does not have permission to add a comment
+
+The email message you sent to [project_addr]
+was not processed because user [sender_addr] 
+does not have permission to add a comment to 
+[artifact_phrase] in [project_name].
+
+[include "inboundemail-error-footer-part.ezt"]
diff --git a/templates/features/inboundemail-not-a-reply.ezt b/templates/features/inboundemail-not-a-reply.ezt
new file mode 100644
index 0000000..9861fd7
--- /dev/null
+++ b/templates/features/inboundemail-not-a-reply.ezt
@@ -0,0 +1,7 @@
+Subject: Your message is not a reply to a notification email
+
+The email message you sent to [project_addr]
+was not processed because it was not a reply to a notification
+email that we sent specifically to [sender_addr].
+
+[include "inboundemail-error-footer-part.ezt"]
diff --git a/templates/features/inboundemail-project-not-found.ezt b/templates/features/inboundemail-project-not-found.ezt
new file mode 100644
index 0000000..d82bac1
--- /dev/null
+++ b/templates/features/inboundemail-project-not-found.ezt
@@ -0,0 +1,6 @@
+Subject: Project not found
+
+The email message you sent to [project_addr]
+was not processed because there is no project at that address.
+
+[include "inboundemail-error-footer-part.ezt"]
diff --git a/templates/features/inboundemail-replies-disabled.ezt b/templates/features/inboundemail-replies-disabled.ezt
new file mode 100644
index 0000000..c95839b
--- /dev/null
+++ b/templates/features/inboundemail-replies-disabled.ezt
@@ -0,0 +1,7 @@
+Subject: Email replies are not enabled in project [project_name]
+
+The email message you sent to [project_addr]
+was not processed because project [project_name]
+has not enabled email replies.
+
+[include "inboundemail-error-footer-part.ezt"]
diff --git a/templates/features/read-only-hotlist-details-page.ezt b/templates/features/read-only-hotlist-details-page.ezt
new file mode 100644
index 0000000..f526aa7
--- /dev/null
+++ b/templates/features/read-only-hotlist-details-page.ezt
@@ -0,0 +1,25 @@
+<h4>Hotlist settings</h4>
+
+<div class="section">
+  Hotlist name:<br>
+  [initial_name]<br>
+
+  Hotlist summary:<br>
+  [initial_summary]<br>
+
+  Hotlist description:<br>
+  [initial_description]<br>
+</div>
+
+<h4>Hotlist defaults</h4>
+
+<div class="section">
+  Default columns shown in list view:<br>
+  [initial_default_col_spec]<br>
+</div>
+
+<h4>Hotlist access</h4>
+<div class="section">
+  <p>Who can view this hotlist: [if-any initial_is_private]Members only[else]Anyone on the internet[end]</p>
+  <p>Individual issues in the list can only be seen by users who can normally see them. The privacy status of an issue is considered when it is being displayed (or not displayed) in a hotlist.</p>
+</div>
diff --git a/templates/features/remove-self-hotlist-form.ezt b/templates/features/remove-self-hotlist-form.ezt
new file mode 100644
index 0000000..cc351c2
--- /dev/null
+++ b/templates/features/remove-self-hotlist-form.ezt
@@ -0,0 +1,19 @@
+<div id="remove-self-container" style="display: [if-any open_dialog]block[else]none[end]">
+  <div id="remove-self-dialog">
+    <h2 style="margin-top:0">
+      Hotlist: [hotlist.name]
+    </h2>
+
+    <section>
+      Would you like to remove yourself as an editor of this hotlist?
+      <input type="checkbox" name="removeself">
+    </section>
+
+    <menu>
+      <button id="cancel-remove-self" type="reset">Cancel</button>
+      <button type="submit">Confirm</button>
+    </menu>
+  </div>
+</div>
+
+<script type="text/javascript" defer src="[version_base]/static/js/hotlists/edit-hotlist.js" nonce="[nonce]"></script>
diff --git a/templates/features/saved-queries-page.ezt b/templates/features/saved-queries-page.ezt
new file mode 100644
index 0000000..69fa654
--- /dev/null
+++ b/templates/features/saved-queries-page.ezt
@@ -0,0 +1,44 @@
+[define title][if-any viewing_self]My[else][viewed_username][end] saved queries[end]
+[define category_css]css/ph_detail.css[end]
+
+[include "../framework/header.ezt" "showusertabs" "t4"]
+
+
+<h3>Saved queries</h3>
+
+[if-any read_only][include "../framework/read-only-rejection.ezt"]
+[else]
+<div class="section">
+
+  <div class="closed">
+    <div>Saved queries allow you to quickly view issue lists that you use frequently.
+     <a class="ifClosed toggleHidden" href="#"
+        style="font-size:90%; margin:0 1em">Learn more</a>
+    </div>
+
+    <div id="filterhelp" class="ifOpened help">
+        Personal saved queries allow you to keep track of the issues that matter most to you.<br/>
+        When you are in a project, you can choose one of your saved queries from the
+        the bottom section of the search dropdown menu that is next to the issue search box.<br/>
+        You can also subscribe to any query to get email notifications when issues that
+        satisfy that query are modified.<br/>
+        Subscription notifications are only generated for users who have visited the
+        site within the past six months.
+    </div>
+    <br>
+
+    <form action="queries.do" method="POST">
+      <input type="hidden" name="token" value="[form_token]">
+      [include "../framework/saved-queries-admin-part.ezt" "user"]
+
+      <input type="submit" id="savechanges" name="btn" value="Save changes"
+             class="submit">
+
+    </form>
+
+  </div>
+</div>
+
+[end][# if not read-only]
+
+[include "../framework/footer.ezt"]
diff --git a/templates/features/transfer-hotlist-form.ezt b/templates/features/transfer-hotlist-form.ezt
new file mode 100644
index 0000000..a837173
--- /dev/null
+++ b/templates/features/transfer-hotlist-form.ezt
@@ -0,0 +1,31 @@
+<div id="transfer-ownership-container" style="display: [if-any open_dialog]block[else]none[end]">
+  <div id="transfer-ownership-dialog">
+    <h2 style="margin-top:0">
+      <a id="hotlist_star"
+            style="color:[if-any hotlist.is_starred]cornflowerblue[else]gray[end]"
+            title="[if-any hotlist.is_starred]Un-s[else]S[end]tar this hotlist">
+            [if-any hotlist.is_starred]&#9733;[else]&#9734;[end]
+            </a>
+      Hotlist: [hotlist.name]
+    </h2>
+
+    <section style="margin: 1em 0">
+      Transfer hotlist ownership to: <input name="changeowners" value="[initial_new_owner_username]" placeholder=[placeholder]>
+      [if-any errors.transfer_ownership]
+        <div class="fielderror">[errors.transfer_ownership]</div>
+      [end]
+    </section>
+
+    <section>
+      Would you like to stay on as an editor of this hotlist?
+      <input type="checkbox" name="becomeeditor" checked>
+    </section>
+
+    <menu>
+      <button id="cancel" type="reset">Cancel</button>
+      <button type="submit">Confirm</button>
+    </menu>
+  </div>
+</div>
+
+<script type="text/javascript" defer src="[version_base]/static/js/hotlists/edit-hotlist.js" nonce="[nonce]"></script>
\ No newline at end of file
diff --git a/templates/features/updates-bulkedit-body.ezt b/templates/features/updates-bulkedit-body.ezt
new file mode 100644
index 0000000..5fc10e7
--- /dev/null
+++ b/templates/features/updates-bulkedit-body.ezt
@@ -0,0 +1 @@
+[is num_issues "1"]Issue [else]Issues [end][for local_ids]<a class="ot-issue-link" href="/p/[project.project_name]/issues/detail?id=[local_ids]">[local_ids]</a>[if-index local_ids last][else], [end][end]
diff --git a/templates/features/updates-ending.ezt b/templates/features/updates-ending.ezt
new file mode 100644
index 0000000..0985311
--- /dev/null
+++ b/templates/features/updates-ending.ezt
@@ -0,0 +1,7 @@
+[is ending_type "in_project"]
+  in project [include "updates-project-link.ezt" "2"]
+[else][is ending_type "by_user"]
+  [define user_profile_url][user.profile_url][end]
+  [define user_display_name][user.display_name][end]
+  by [include "updates-profile-link.ezt" "2"]
+[end][end]
diff --git a/templates/features/updates-entry-part.ezt b/templates/features/updates-entry-part.ezt
new file mode 100644
index 0000000..a1fde4e
--- /dev/null
+++ b/templates/features/updates-entry-part.ezt
@@ -0,0 +1,57 @@
+[# Show one activity.  arg0 is the activity.]
+
+[is arg0.highlight ""]
+  [define column_width]160[end]
+[else]
+  [define column_width]300[end]
+[end]
+
+<li [is even "Yes"]class="even"[end]>
+  <div class="g-section g-tpl-[column_width]">
+    <div class="g-unit g-first">
+      <div class="g-c">
+        [if-any arg0.highlight]
+        <div class="g-section g-tpl-160">
+          <div class="g-unit g-first">
+            <div class="g-c">
+              <span class="date [if-any arg0.escaped_body]below-more[else][end] activity" title="[arg0.date_tooltip]">[arg0.date_relative]</span>
+            </div>
+          </div>
+          <div class="g-unit">
+            <div class="g-c" style="padding-right:1em">
+              <span class="highlight-column">
+                [is arg0.highlight "project"]
+                <a href="/p/[arg0.project_name]/" title="[arg0.project_name]">[arg0.project_name]</a>
+                [else][is arg0.highlight "user"]
+                <a href="[arg0.user.profile_url]" title="[arg0.user.display_name]">[arg0.user.display_name]</a>
+                [end][end]
+              </span>
+            </div>
+          </div>
+        </div>
+        [else]
+        <span class="date [if-any arg0.escaped_body]below-more[end] activity" title="[arg0.date_tooltip]">[arg0.date_relative]</span>
+        [end]
+      </div>
+    </div>
+    <div class="g-unit">
+      <div class="g-c">
+        <span class="content">
+          [# SECURITY: OK to use "raw" here because escaped_title was preprocessed through the template engine.]
+          <span class="title">[format "raw"][arg0.escaped_title][end]</span>
+          [if-any arg0.escaped_body]
+          <span class="details-inline" style="margin-left:.5em">
+            [# SECURITY: OK to use "raw" here because escaped_body was preprocessed through the template engine.]
+            - [format "raw"][arg0.escaped_body][end]
+          </span>
+          <div class="details-wrapper">
+            [# SECURITY: OK to use "raw" here because escaped_body was preprocessed through the template engine.]
+            <div class="details">[format "raw"][arg0.escaped_body][end]</div>
+          </div>
+          [end]
+        </span>
+      </div>
+    </div>
+  </div>
+</li>
+[define even][is even "Yes"]No[else]Yes[end][end]
diff --git a/templates/features/updates-issue-link.ezt b/templates/features/updates-issue-link.ezt
new file mode 100644
index 0000000..d3b6a95
--- /dev/null
+++ b/templates/features/updates-issue-link.ezt
@@ -0,0 +1,5 @@
+<a class="ot-issue-link"
+   [# Go to the first comment with the correct timestamp. That's usually right, and close
+      even in cases where it is wrong.  It avoids exposing a DB ID.]
+   href="/p/[issue.project_name]/issues/detail?id=[issue.local_id][if-any issue_change_id]#c_ts[issue_change_id][end]"
+   >issue [issue.local_id]</a>
diff --git a/templates/features/updates-issueupdate-body.ezt b/templates/features/updates-issueupdate-body.ezt
new file mode 100644
index 0000000..5d0f683
--- /dev/null
+++ b/templates/features/updates-issueupdate-body.ezt
@@ -0,0 +1,16 @@
+[# Format the body of one issue update in the activities list.]
+
+<span class="ot-issue-comment">
+  [for comment.text_runs][include "../tracker/render-rich-text.ezt" comment.text_runs][end]
+</span>
+
+[if-any comment.amendments]
+  <div class="ot-issue-fields">
+    [for comment.amendments]
+      <div class="ot-issue-field-wrapper">
+       <span class="ot-issue-field-name">[comment.amendments.field_name]: </span>
+       <span class="ot-issue-field-value">[comment.amendments.newvalue]</span>
+      </div>
+    [end]
+  </div>
+[end]
diff --git a/templates/features/updates-issueupdate-title.ezt b/templates/features/updates-issueupdate-title.ezt
new file mode 100644
index 0000000..1475df9
--- /dev/null
+++ b/templates/features/updates-issueupdate-title.ezt
@@ -0,0 +1,28 @@
+[# Pre-render the title of an activity for an issue update.]
+
+[include "updates-issue-link.ezt"]
+([issue.short_summary])
+
+[define field_changed][end]
+[define multiple_fields_changed][end]
+[for comment.amendments]
+  [if-any field_changed]
+    [define multiple_fields_changed]True[end]
+  [else]
+    [define field_changed][comment.amendments.field_name][end]
+  [end]
+[end]
+
+[if-any issue_change_id]
+  [if-any multiple_fields_changed]
+    changed
+  [else][if-any field_changed]
+    [field_changed] changed
+  [else]
+    commented on
+  [end][end]
+[else]
+  reported
+[end]
+
+[include "updates-ending.ezt"]
diff --git a/templates/features/updates-newproject-body.ezt b/templates/features/updates-newproject-body.ezt
new file mode 100644
index 0000000..2d0feb0
--- /dev/null
+++ b/templates/features/updates-newproject-body.ezt
@@ -0,0 +1 @@
+<span class="ot-project-summary">[project_summary]</span>
diff --git a/templates/features/updates-page.ezt b/templates/features/updates-page.ezt
new file mode 100644
index 0000000..e5306f3
--- /dev/null
+++ b/templates/features/updates-page.ezt
@@ -0,0 +1,177 @@
+[define title]Updates[end]
+[if-any updates_data]
+
+[define even]Yes[end]
+
+<div id="colcontrol">
+<div class="list">
+    <table style="width: 100%;" cellspacing="0" cellpadding="0">
+     <tbody><tr>
+     <td style="text-align: left;">
+       Details:
+       <a id="detailsshow" href="#" class="showAll">Show all</a>
+       <a id="detailshide" href="#" class="hideAll">Hide all</a></td>
+     <td>
+     [include "../framework/artifact-list-pagination-part.ezt"]
+     </td>
+     </tr>
+     </tbody>
+    </table>
+  </div>
+
+  <table cellspacing="0" cellpadding="0" border="0" width="100%" id="resultstable" class="results" style="table-layout:fixed; width:100%">
+  <tbody>
+  <tr>
+  <td style="padding:0px" width="100%">
+
+  <div id='activity-streams-list' class='activity-stream-list'>
+    [if-any updates_data.today]
+      <h4>Today</h4>
+      <ul class='activity-stream'>
+      [for updates_data.today]
+        [include "updates-entry-part.ezt" updates_data.today]
+      [end]
+      </ul>
+    [end]
+
+    [if-any updates_data.yesterday]
+      <h4>Yesterday</h4>
+      <ul class='activity-stream'>
+      [for updates_data.yesterday]
+        [include "updates-entry-part.ezt" updates_data.yesterday]
+      [end]
+      </ul>
+    [end]
+
+    [if-any updates_data.pastweek]
+      <h4>Last 7 days</h4>
+      <ul class='activity-stream'>
+      [for updates_data.pastweek]
+        [include "updates-entry-part.ezt" updates_data.pastweek]
+      [end]
+      </ul>
+    [end]
+
+    [if-any updates_data.pastmonth]
+      <h4>Last 30 days</h4>
+      <ul class='activity-stream'>
+      [for updates_data.pastmonth]
+        [include "updates-entry-part.ezt" updates_data.pastmonth]
+      [end]
+      </ul>
+    [end]
+
+    [if-any updates_data.thisyear]
+      <h4>Earlier this year</h4>
+      <ul class='activity-stream'>
+      [for updates_data.thisyear]
+        [include "updates-entry-part.ezt" updates_data.thisyear]
+      [end]
+      </ul>
+    [end]
+
+    [if-any updates_data.older]
+      <h4>Older</h4>
+      <ul class='activity-stream'>
+      [for updates_data.older]
+        [include "updates-entry-part.ezt" updates_data.older]
+      [end]
+      </ul>
+    [end]
+  </div>
+
+  </td></tr></tbody></table>
+
+  <div class="list-foot">
+    [include "../framework/artifact-list-pagination-part.ezt"]
+  </div>
+</div>
+
+[else]
+
+  [if-any no_stars]
+    [is user_updates_tab_mode "st2"]
+      <div class="display-error">There are no starred projects.</div>
+    [else][is user_updates_tab_mode "st3"]
+      <div class="display-error">There are no starred developers.</div>
+    [end][end]
+  [else][if-any no_activities]
+    <div class="display-error">There are no updates yet.</div>
+  [end][end]
+
+[end]
+
+[if-any updates_data]
+<script type="text/javascript" nonce="[nonce]">
+runOnLoad(function() {
+
+  /**
+   * Shows the activity detail for the particular activity selected.
+   */
+  function handleActivityLinkClick(e) {
+    var targetEl;
+
+    if (!e) {
+      var e = window.event;
+    }
+    if (e.target) {
+      targetEl = e.target;
+
+    } else if (e.srcElement) {
+      targetEl = e.srcElement;
+    }
+    if (targetEl.nodeType == 3) {
+      targetEl = targetEl.parentNode;
+    }
+
+    while (targetEl.tagName.toLowerCase() != 'li') {
+      targetEl = targetEl.parentNode;
+    }
+    if (targetEl.className.indexOf('click') != -1) {
+      targetEl.className = targetEl.className.replace(/click/, '');
+    } else {
+      targetEl.className += ' click';
+    }
+
+    e.preventDefault();
+  }
+
+  /**
+   * Array of <li> elements for activity streams
+   */
+  var _CS_asElemList = document.getElementById('activity-streams-list').
+      getElementsByTagName('li');
+
+  /**
+   * Shows all activity details
+   */
+  function expandAll(event) {
+    for (var i=0; i < _CS_asElemList.length; i++) {
+      _CS_asElemList[[]i].className = 'click';
+    }
+    event.preventDefault();
+  }
+
+  /**
+   * Hides all activity details
+   */
+  function closeAll(event) {
+    for (var i=0; i < _CS_asElemList.length; i++) {
+      _CS_asElemList[[]i].className = '';
+    }
+    event.preventDefault();
+  }
+
+  if ($("detailsshow"))
+    $("detailsshow").addEventListener("click", expandAll);
+  if ($("detailshide"))
+    $("detailshide").addEventListener("click", closeAll);
+
+  var activityLinks = document.getElementsByClassName("activity");
+  for (var i = 0; i < activityLinks.length; ++i) {
+    var link = activityLinks[[]i];
+    link.addEventListener("click", handleActivityLinkClick);
+  }
+});
+</script>
+[end]
diff --git a/templates/features/updates-profile-link.ezt b/templates/features/updates-profile-link.ezt
new file mode 100644
index 0000000..722dbae
--- /dev/null
+++ b/templates/features/updates-profile-link.ezt
@@ -0,0 +1 @@
+<a class="ot-profile-link-[arg0]" href="[user_profile_url]">[user_display_name]</a>
diff --git a/templates/features/updates-project-link.ezt b/templates/features/updates-project-link.ezt
new file mode 100644
index 0000000..b1a0ec2
--- /dev/null
+++ b/templates/features/updates-project-link.ezt
@@ -0,0 +1 @@
+<a class="ot-project-link-[arg0]" href="/p/[project.project_name]/">[project.project_name]</a>
diff --git a/templates/features/updates-staractivity-body.ezt b/templates/features/updates-staractivity-body.ezt
new file mode 100644
index 0000000..a016c4d
--- /dev/null
+++ b/templates/features/updates-staractivity-body.ezt
@@ -0,0 +1 @@
+[# Placeholder for star activity]
diff --git a/templates/features/updates-staractivity-title.ezt b/templates/features/updates-staractivity-title.ezt
new file mode 100644
index 0000000..93673ad
--- /dev/null
+++ b/templates/features/updates-staractivity-title.ezt
@@ -0,0 +1,5 @@
+[is scope "projects"]
+[is starred "yes"]Starred[else]Unstarred[end] project [include "updates-project-link.ezt" "1"]
+[else][is scope "users"]
+[is starred "yes"]Starred[else]Unstarred[end] <a class="ot-profile-link-1" href="[starred_user_profile_url]">[starred_user_display_name]</a>
+[end][end]
diff --git a/templates/features/user-hotlists.ezt b/templates/features/user-hotlists.ezt
new file mode 100644
index 0000000..438e7b1
--- /dev/null
+++ b/templates/features/user-hotlists.ezt
@@ -0,0 +1,130 @@
+[define title][if-any viewing_self]My[else][viewed_user_display_name][end] hotlists[end]
+[define category_css]css/ph_detail.css[end]
+
+[include "../framework/header.ezt" "showusertabs" "t5"]
+
+
+<h3>Hotlists</h3>
+
+<div class="section">
+
+  <div class="closed">
+    <div>Hotlists allow you to group and rank issues independently of projects and with other users.</div><br>
+    [if-any viewing_self]
+    <div>
+      <a href="/hosting/createHotlist" title="Create a new hotlist">
+        <input type="button" class="primary" value="Create hotlist">
+      </a>
+    </div><br>
+    [end]
+
+    <div class="list">
+      <table style="width:100%;" cellspacing="0" cellpadding="0">
+        <tr>
+          <th style="text-align:left;">Hotlists</th>
+        </tr>
+      </table>
+    </div>
+    <table cellspacing="0" cellpadding="2" border="0" class="results striped" width="100%">
+      <tr id="headingrow">
+        [if-any logged_in_user]<th style="white-space:nowrap; width:3%;">&nbsp;</th>[end]
+        <th style="white-space:nowrap; width:15%;">Role</th>
+        <th style="white-space:nowrap; width:25%">Hotlist</th>
+        <th style="white-space:nowrap; width:10%;">Issues</th>
+        <th style="white-space:nowrap; width:[if-any viewing_self]50[else]47[end]%;">Summary</th>
+      </tr>
+    [if-any owner_of_hotlists editor_of_hotlists]
+      [for owner_of_hotlists]
+        <tr data-url="[owner_of_hotlists.url]">
+          [if-any logged_in_user]
+            <td class="rowwidgets">
+              <a class="star"
+                 style="color:[if-any owner_of_hotlists.is_starred]cornflowerblue[else]gray[end]"
+                 title="[if-any owner_of_hotlists.is_starred]Un-s][else]S[end]tar this hotlist"
+                 data-hotlist-id="[owner_of_hotlists.hotlist_id]">
+             [if-any owner_of_hotlists.is_starred]&#9733;[else]&#9734;[end]
+              </a>
+            </td>
+          [end]
+          <td>Owner</td>
+          <td class="id" name="owner">
+            <a href="[owner_of_hotlists.url]">[owner_of_hotlists.name]</a></td>
+          <td>[owner_of_hotlists.num_issues]</td>
+          <td>[owner_of_hotlists.summary]</td>
+        </tr>
+      [end]
+      [for editor_of_hotlists]
+        <tr data-url="[editor_of_hotlists.url]">
+          [if-any logged_in_user]
+            <td class="rowwidgets">
+              <a class="star"
+                 style="color:[if-any editor_of_hotlists.is_starred]cornflowerblue[else]gray[end]"
+                 title="[if-any editor_of_hotlists.is_starred]Un-s][else]S[end]tar this hotlist"
+                 data-hotlist-id="[editor_of_hotlists.hotlist_id]">
+             [if-any editor_of_hotlists.is_starred]&#9733;[else]&#9734;[end]
+            </td>
+          [end]
+          <td>Editor</td>
+          <td class="id" name="editor">
+            <a href="[editor_of_hotlists.url]">[editor_of_hotlists.name]</a></td>
+          <td>[editor_of_hotlists.num_issues]</td>
+          <td>[editor_of_hotlists.summary]</td>
+        </tr>
+      [end]
+    [else]
+      <td colspan="4"><i>No hotlists.</i></td>
+    [end]
+    </table>
+    [if-any starred_hotlists]
+      <div class="list">
+        <table style="width:100%;" cellspacing="0" cellpadding="0">
+          <tr>
+            <th style="text-align:left;">Hotlists starred by [if-any viewing_self]you[else][viewed_user_display_name][end]</th>
+          </tr>
+        </table>
+      </div>
+      <table cellspacing="0" cellpadding="2" border="0" class="results striped" width="100%">
+        <tr>
+          <th style="white-space:nowrap; width:3%;">&nbsp;</th>
+          <th style="white-space:nowrap; width:30%;">Hotlist</th>
+          <th style="white-space:nowrap; width:10%;">Issues</th>
+          <th style="white-space:nowrap; width:57%;">Summary</th>
+        </tr>
+        [for starred_hotlists]
+          <tr data-url="[starred_hotlists.url]">
+            <td class="rowwidgets">
+              <a class="star"
+                 style="color:[if-any starred_hotlists.is_starred]cornflowerblue[else]gray[end]"
+                 title="[if-any starred_hotlists.is_starred]Un-s][else]S[end]tar this hotlist"
+                 data-hotlist-id="[starred_hotlists.hotlist_id]">
+             [if-any starred_hotlists.is_starred]&#9733;[else]&#9734;[end]
+            </td>
+            <td class="id" name="follower">
+              <a href="[starred_hotlists.url]">[starred_hotlists.name]</a></td>
+            </td>
+            <td>[starred_hotlists.num_issues]</td>
+            <td>[starred_hotlists.summary]</td>
+          </tr>
+        [end]
+      </table>
+    [end]
+
+  </div>
+</div>
+
+<script type="text/javascript" nonce="[nonce]">
+runOnLoad(function() {
+
+ var stars = document.getElementsByClassName("star");
+  for (var i = 0; i < stars.length; ++i) {
+    var star = stars[[]i];
+    star.addEventListener("click", function (event) {
+        var hotlistID = event.target.getAttribute("data-hotlist-id");
+        _TKR_toggleStar(event.target, null, null, null, hotlistID);
+    });
+  }
+
+});
+</script>
+
+[include "../framework/footer.ezt"]
diff --git a/templates/framework/admin-email-sender-part.ezt b/templates/framework/admin-email-sender-part.ezt
new file mode 100644
index 0000000..783a87f
--- /dev/null
+++ b/templates/framework/admin-email-sender-part.ezt
@@ -0,0 +1,15 @@
+[if-any project_is_restricted]
+<p style="width:35em; border: 1px solid #933; padding: 3px">
+  <b style="color:#933">Important</b>: Access to this project is restricted, so
+  please do not specify a public mailing list address for all notifications.
+  Use only private mailing lists to avoid unwanted disclosures.  If you make
+  your project public later, choose a new mailing list at that time.
+</p>
+[end]
+
+<p>
+    Notifications will be sent from:
+    <tt>[email_from_addr]</tt><br>
+    You may need to add this address as an allowed poster to your mailing list.<br>
+    If using Google Groups, add the address directly with no email delivery.
+</p>
diff --git a/templates/framework/alert.ezt b/templates/framework/alert.ezt
new file mode 100644
index 0000000..45a61aa
--- /dev/null
+++ b/templates/framework/alert.ezt
@@ -0,0 +1,35 @@
+  <table id="alert-table" align="center" border="0" cellspacing="0" cellpadding="0" style="margin-bottom: 6px[if-any alerts.show][else];display: none[end]">
+   <tr><td class="notice" id="notice">
+     [if-any alerts.updated]
+      <a href="[project_home_url]/issues/detail?id=[alerts.updated]">Issue [alerts.updated]</a>
+      has been updated.
+     [end]
+
+     [if-any alerts.moved]
+       Issue has been moved to
+       <a href="/p/[alerts.moved_to_project]/issues/detail?id=[alerts.moved_to_id]">
+         [alerts.moved_to_project]:[alerts.moved_to_id]
+      </a>
+     [end]
+
+     [if-any alerts.copied]
+       <a href="[project_home_url]/issues/detail?id=[alerts.copied_from_id]">Issue [alerts.copied_from_id]</a>
+       has been copied to
+       <a href="/p/[alerts.copied_to_project]/issues/detail?id=[alerts.copied_to_id]">
+         [alerts.copied_to_project]:[alerts.copied_to_id]
+      </a>
+     [end]
+
+     [if-any alerts.saved]
+      Changes have been saved
+     [end]
+
+     [if-any alerts.deleted]
+      [is alerts.deleted "1"]
+       Item deleted
+      [else]
+       [alerts.deleted] items deleted
+      [end]
+     [end]
+   </td></tr>
+  </table>
diff --git a/templates/framework/artifact-collision-page.ezt b/templates/framework/artifact-collision-page.ezt
new file mode 100644
index 0000000..0c34479
--- /dev/null
+++ b/templates/framework/artifact-collision-page.ezt
@@ -0,0 +1,30 @@
+[include "../framework/header.ezt" "showtabs"]
+
+[# Note: No need for UI element permission checking here. ]
+
+<h3>Update Collision</h3>
+
+<h4>What happened?</h4>
+
+<p>While you were viewing or updating [artifact_name], another user
+submitted an update to it.  That user's update has already
+taken effect.  Your update cannot be saved because your changes could
+overwrite the other user's changes.</p>
+
+<p>Note: if you have been viewing and updating [artifact_name] in multiple
+browser windows or tabs, it is possible that the "other user" is
+actually yourself.</p>
+
+
+<div style="margin:2em" class="help">
+  <b style="margin:0.5em">Your options:</b>
+
+  <ul>
+   <li>Start over: view the up-to-date
+   <a href="[artifact_detail_url]">[artifact_name]</a>
+   and consider making your changes again.</li>
+  </ul>
+
+</div>
+
+[include "../framework/footer.ezt"]
diff --git a/templates/framework/artifact-list-admin-part.ezt b/templates/framework/artifact-list-admin-part.ezt
new file mode 100644
index 0000000..e24a236
--- /dev/null
+++ b/templates/framework/artifact-list-admin-part.ezt
@@ -0,0 +1,129 @@
+[# If any value is supplied for arg0, the user will also be able
+   to edit grid preferences.]
+<h4>[if-any arg0]List and grid preferences[else]List preferences[end]</h4>
+<div class="section">
+
+ <div class="closed">
+  <div>Default query for project members:
+   <a class="ifClosed toggleHidden" href="#"
+      style="font-size:90%; margin-left:.5em">Learn more</a>
+  </div>
+
+  <div id="colhelp" class="ifOpened help">
+      <div>
+       You may enter a default query for project members.  They will run
+       this query when they click on the "Issues" tab.
+      </div>
+  </div>
+  <br>
+ </div>
+
+ <input type="text" size="75" name="member_default_query"
+        value="[config.member_default_query]" id="searchq"
+        [if-any perms.EditProject][else]readonly="readonly"[end]
+        class="acob" style="margin-left:.7em">
+ <br>
+ <br>
+ <br>
+
+ <div class="closed">
+  <div>Default columns shown in list view:
+   <a class="ifClosed toggleHidden" href="#"
+      style="font-size:90%; margin-left:.5em">Learn more</a>
+  </div>
+
+  <div id="colhelp" class="ifOpened help">
+      <div>
+       You may enter a series of column names separated by spaces.  The
+       columns will be displayed in order on the list view page.
+      </div>
+      <br>
+      <div>
+       Columns may be the names of built-in attributes, e.g., "Summary"
+       or "Stars". Columns may also be prefixes of the labels on items.
+       To experiment with label prefixes, label some items with
+       Key-Value labels, then click the "..." menu in the far upper right
+       heading of the list view.
+      </div>
+  </div>
+  <br>
+ </div>
+
+ <input type="text" size="75" name="default_col_spec" value="[config.default_col_spec]"
+        [if-any perms.EditProject][else]readonly="readonly"[end]
+        class="acob" style="margin-left:.7em">
+ <br>
+ <br>
+ <br>
+
+ <div class="closed">
+  <div>Default sorting order:
+   <a class="ifClosed toggleHidden" href="#"
+      style="font-size:90%; margin-left:.7em">Learn more</a>
+  </div>
+
+  <div class="ifOpened help">
+      <div>
+       You may enter a series of column names separated by spaces.  Items
+       will be sorted by the first column specified.  If two items have
+       the same value in the first column, the items' values in the second
+       column will be used to break the tie, and so on. Use a leading
+       minus-sign to reverse the sort order within a column.
+      </div>
+      <br>
+      <div>
+       To experiment with column sorting, click the list view header cells and
+       choose "Sort up" or "Sort down". The sorting specification used becomes
+       part of the page URL.
+      </div>
+  </div>
+  <br>
+ </div>
+
+ <input type="text" size="75" name="default_sort_spec" value="[config.default_sort_spec]"
+        [if-any perms.EditProject][else]readonly="readonly"[end]
+        class="acob" style="margin-left:.7em">
+
+
+ [if-any arg0]
+ <br>
+ <br>
+ <br>
+
+ <div class="closed">
+  <div>Default grid axes:
+   <a class="ifClosed toggleHidden" href="#"
+      style="font-size:90%; margin-left:.7em">Learn more</a>
+  </div>
+
+  <div class="ifOpened help">
+      <div>
+       You may enter one attribute name for the default grid rows and one for
+       the default grid columns.  For example, "milestone" and "priority".  Or,
+       you may leave each field blank.
+      </div>
+      <br>
+      <div>
+       To experiment with grid axes, click the "grid" link in the list view and
+       use the drop-down menus to select row and column attributes.
+      </div>
+  </div>
+  <br>
+ </div>
+
+ <span style="margin-left:.7em">
+   Rows: <input type="text" size="10" name="default_y_attr" value="[config.default_y_attr]"
+                [if-any perms.EditProject][else]readonly="readonly"[end]
+                class="acob">
+ </span>
+
+ <span style="margin-left:.7em">
+   Columns: <input type="text" size="10" name="default_x_attr" value="[config.default_x_attr]"
+                   [if-any perms.EditProject][else]readonly="readonly"[end]
+                   class="acob">
+ </span>
+
+ [end]
+
+</div>
+
diff --git a/templates/framework/artifact-list-pagination-part.ezt b/templates/framework/artifact-list-pagination-part.ezt
new file mode 100644
index 0000000..8896c14
--- /dev/null
+++ b/templates/framework/artifact-list-pagination-part.ezt
@@ -0,0 +1,18 @@
+[if-any pagination]
+ [if-any pagination.visible]
+  <div class="pagination">
+   [if-any pagination.prev_url]
+     <a href="[pagination.prev_url]"><b>&lsaquo;</b> Prev</a>
+   [end]
+   [if-any pagination.start]
+     [pagination.start] - [pagination.last]
+   [end]
+   [if-any pagination.total_count]
+     of [pagination.total_count][if-any pagination.limit_reached]+[end]
+   [end]
+   [if-any pagination.next_url]
+     <a href="[pagination.next_url]">Next <b>&rsaquo;</b></a>
+   [end]
+  </div>
+ [end]
+[end]
diff --git a/templates/framework/banned-page.ezt b/templates/framework/banned-page.ezt
new file mode 100644
index 0000000..4b3effb
--- /dev/null
+++ b/templates/framework/banned-page.ezt
@@ -0,0 +1,31 @@
+[include "../framework/header.ezt" "hidetabs"]
+
+<h3>Access Not Allowed</h3>
+
+<h4>What happened?</h4>
+
+<p>
+[if-any is_plus_address]
+  We do not accept accounts with "+" in the email address.
+[else]
+  You are not allowed to access this service.
+[end]
+</p>
+
+<p>Please <a href="mailto:[feedback_email]">contact us</a> if you believe that you should be able to access this service. (This is a Google Group; what you write will be visible on the Internet.)</p>
+
+[# Note: we do not show the reason for being banned. ]
+
+
+<div style="margin:2em" class="help">
+  <b style="margin:0.5em">Your options:</b>
+
+  <ul>
+   <li>Participate in the open source community through other websites.</li>
+   <li><a href="[logout_url_goto_home]">Sign out</a> and access this site as
+     an anonymous user.</li>
+   <li><a href="mailto:[feedback_email]">Contact us</a> for further assistance.</li>
+  </ul>
+</div>
+
+[include "../framework/footer.ezt"]
diff --git a/templates/framework/banner_message.ezt b/templates/framework/banner_message.ezt
new file mode 100644
index 0000000..fc31310
--- /dev/null
+++ b/templates/framework/banner_message.ezt
@@ -0,0 +1,8 @@
+[if-any site_banner_message]
+ <div style="font-weight:bold; color:var(--chops-field-error-color); padding:5px; margin-top:10px; text-align:center; background:var(--chops-orange-50);">
+  [site_banner_message]
+  [if-any banner_time]
+    <chops-timestamp timestamp="[banner_time]"></chops-timestamp>
+  [end]
+ </div>
+[end]
diff --git a/templates/framework/comment-pagination-part.ezt b/templates/framework/comment-pagination-part.ezt
new file mode 100644
index 0000000..6867c57
--- /dev/null
+++ b/templates/framework/comment-pagination-part.ezt
@@ -0,0 +1,8 @@
+[if-any cmnt_pagination.prev_url]
+  <a href="[cmnt_pagination.prev_url]" style="margin-right:.7em"><b>&lsaquo;</b> Newer</a>
+[end]
+Showing comments [cmnt_pagination.last] - [cmnt_pagination.start]
+[if-any cmnt_pagination.total_count]of [cmnt_pagination.total_count][end]
+[if-any cmnt_pagination.next_url]
+  <a href="[cmnt_pagination.next_url]" style="margin-left:.7em">Older <b>&rsaquo;</b></a>
+[end]
diff --git a/templates/framework/component-validation-row.ezt b/templates/framework/component-validation-row.ezt
new file mode 100644
index 0000000..5d37b84
--- /dev/null
+++ b/templates/framework/component-validation-row.ezt
@@ -0,0 +1,5 @@
+<tr>
+  <td colspan="3">
+    <div id="component_blocksubmitarea" class="blockingsubmit"><span id="component_blocksubmitmsg"></span></div>
+  </td>
+</tr>
diff --git a/templates/framework/database-maintenance.ezt b/templates/framework/database-maintenance.ezt
new file mode 100644
index 0000000..fe8d7b4
--- /dev/null
+++ b/templates/framework/database-maintenance.ezt
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <link rel="icon" type="image/vnd.microsoft.icon" href="/static/images/monorail.ico">
+  <title>This bug tracker is unavailable due to database issues.</title>
+  <meta name="ROBOTS" content="NOARCHIVE">
+  <link type="text/css" rel="stylesheet" href="/static/css/ph_core.css">
+</head>
+<body>
+  <h2>This bug tracker is currently unavailable due to database issues.</h2>
+  Please <a href="[requested_url]">try again</a> later.
+</body>
+</html>
diff --git a/templates/framework/debug.ezt b/templates/framework/debug.ezt
new file mode 100644
index 0000000..7903de5
--- /dev/null
+++ b/templates/framework/debug.ezt
@@ -0,0 +1,50 @@
+[is dbg "off"]
+ [if-any perms._ViewDebug]
+   <div class="debug">
+    - <a href="[debug_uri]">Reload w/ debug info</a>
+   </div>
+ [end]
+[else]
+   [# Note that this only handles the top two levels of (sub)phases.
+    # If you nest phases further than that (which we haven't wanted/needed to
+    # do so far), you'll have to modify this code in order to render it.]
+   <style type="text/css">
+    .debug, .debug a { color: #444; font-size: x-small}
+    .debug td, .debug th { background: #ddf}
+    .debug th { text-align: left; font-family: courier; font-size: small}
+   </style>
+
+   <div class="debug">Profile Data
+     <table class="ifOpened" cellpadding="2" cellspacing="2" border="0"  style="padding-left: 1em">
+       [for profiler.top_phase.subphases]
+        <tr>
+         <th style="white-space:nowrap">[profiler.top_phase.subphases.name]:</th>
+         <td align="right">[profiler.top_phase.subphases.ms][is profiler.top_phase.subphases.ms "in_progress"][else] ms[end]</td>
+         <td><table cellspacing="1" cellpadding="0"><tr>
+         [for profiler.top_phase.subphases.subphases]
+          <td title="[profiler.top_phase.subphases.subphases.name]: [profiler.top_phase.subphases.subphases.ms]ms"
+            width="[is profiler.top_phase.subphases.subphases.ms "in_progress"]100%[else][profiler.top_phase.subphases.subphases.ms][end]"
+            style="padding:2px;color:#fff;background:#[profiler.top_phase.subphases.subphases.color]">[profiler.top_phase.subphases.subphases.ms]</td>
+         [end]
+
+         [if-any profiler.top_phase.subphases.uncategorized_ms]
+           <td title="uncategorized: [profiler.top_phase.subphases.uncategorized_ms]ms"
+              width="[profiler.top_phase.subphases.uncategorized_ms]"
+              style="padding:1px">[profiler.top_phase.subphases.uncategorized_ms]</td>
+         [end]
+        </tr></table>
+         </td>
+        </tr>
+       [end]
+     </table>
+   </div><br>
+ [for debug]
+   <div class="debug">[debug.title]
+     <table cellpadding="2" cellspacing="2" border="0" style="padding-left: 1em">
+      [for debug.collection]
+       <tr><th>[debug.collection.key]</th><td>[debug.collection.val]</td></tr>
+      [end]
+     </table>
+   </div><br>
+ [end]
+[end]
diff --git a/templates/framework/display-project-logo.ezt b/templates/framework/display-project-logo.ezt
new file mode 100644
index 0000000..fd787c6
--- /dev/null
+++ b/templates/framework/display-project-logo.ezt
@@ -0,0 +1,29 @@
+[# This template displays the project logo with the file name and a View link.
+
+   arg0: Whether to display a checkbox to delete the logo.
+]
+
+<table cellspacing="5" cellpadding="2" border="0">
+  <tr>
+    <td>
+      <b>[logo_view.filename]</b>
+    </td>
+    <td>
+      <a href="[logo_view.viewurl]" target="_blank" style="margin-left:.2em">View</a>
+    </td>
+  </tr>
+  <tr>
+    <td colspan=2>
+      <a href="[logo_view.viewurl]" target="_blank">
+        <img src="[logo_view.thumbnail_url]" class="preview">
+      </a>
+    </td>
+  </tr>
+  [if-any arg0]
+    <tr>
+      <td colspan=2>
+        <input type="checkbox" name="delete_logo" id="delete_logo"> Delete this logo
+      </td>
+    </tr>
+  [end]
+</table>
diff --git a/templates/framework/excessive-activity-page.ezt b/templates/framework/excessive-activity-page.ezt
new file mode 100644
index 0000000..e7b10ad
--- /dev/null
+++ b/templates/framework/excessive-activity-page.ezt
@@ -0,0 +1,31 @@
+[include "../framework/header.ezt" "hidetabs"]
+
+<h3>Action Limit Exceeded</h3>
+
+<h4>What happened?</h4>
+
+<div style="width:60em">
+
+<p>You have performed the requested action too many times in a 24-hour
+time period.  Or, you have performed the requested action too many
+times since the creation of your account.</p>
+
+<p>We place limits on the number of actions that can be performed by
+each user in order to reduce the potential for abuse.  We feel that we have set
+these limits high enough that legitimate use will very rarely
+reach them.  Without these limits, a few abusive users could degrade
+the quality of this site for everyone.</p>
+
+
+<div style="margin:2em" class="help">
+  <b style="margin:0.5em">Your options:</b>
+
+  <ul>
+   <li>Wait 24 hours and then try this action again.</li>
+   <li>Ask another member of your project to perform the action for you.</li>
+   <li><a href="mailto:[feedback_email]">Contact us</a> for further assistance.</li>
+  </ul>
+</div>
+
+</div>
+[include "../framework/footer.ezt"]
diff --git a/templates/framework/file-content-js.ezt b/templates/framework/file-content-js.ezt
new file mode 100644
index 0000000..72e882e
--- /dev/null
+++ b/templates/framework/file-content-js.ezt
@@ -0,0 +1,89 @@
+[# TODO(jrobbins): move this into compiled javascript. ]
+<script type="text/javascript" nonce="[nonce]">
+runOnLoad(function() {
+  var numsGenState = {table_base_id: 'nums_table_'};
+  var srcGenState = {table_base_id: 'src_table_'};
+  var alignerRunning = false;
+  var startOver = false;
+
+  function setLineNumberHeights() {
+    if (alignerRunning) {
+      startOver = true;
+      return;
+    }
+    numsGenState.chunk_id = 0;
+    numsGenState.table = document.getElementById('nums_table_0');
+    numsGenState.row_num = 0;
+
+    if (!numsGenState.table) {
+      return;  // Silently exit if no file is present.
+    }
+
+    srcGenState.chunk_id = 0;
+    srcGenState.table = document.getElementById('src_table_0');
+    srcGenState.row_num = 0;
+
+    alignerRunning = true;
+    continueToSetLineNumberHeights();
+  }
+
+  function rowGenerator(genState) {
+    if (genState.row_num < genState.table.rows.length) {
+      var currentRow = genState.table.rows[[]genState.row_num];
+      genState.row_num++;
+      return currentRow;
+    }
+
+    var newTable = document.getElementById(
+        genState.table_base_id + (genState.chunk_id + 1));
+    if (newTable) {
+      genState.chunk_id++;
+      genState.row_num = 0;
+      genState.table = newTable;
+      return genState.table.rows[[]0];
+    }
+
+    return null;
+  }
+
+  var MAX_ROWS_PER_PASS = 1000;
+
+  function continueToSetLineNumberHeights() {
+    var rowsInThisPass = 0;
+    var numRow = 1;
+    var srcRow = 1;
+
+    while (numRow && srcRow && rowsInThisPass < MAX_ROWS_PER_PASS) {
+      numRow = rowGenerator(numsGenState);
+      srcRow = rowGenerator(srcGenState);
+      rowsInThisPass++;
+
+      if (numRow && srcRow) {
+        if (numRow.offsetHeight != srcRow.offsetHeight) {
+          numRow.firstChild.style.height = srcRow.offsetHeight + 'px';
+        }
+      }
+    }
+
+    if (rowsInThisPass >= MAX_ROWS_PER_PASS) {
+      setTimeout(continueToSetLineNumberHeights, 10);
+    } else {
+      alignerRunning = false;
+      if (startOver) {
+        startOver = false;
+        setTimeout(setLineNumberHeights, 500);
+      }
+    }
+
+  }
+
+  function initLineNumberHeights() {
+    // Do 2 complete passes, because there can be races
+    // between this code and prettify.
+    startOver = true;
+    setTimeout(setLineNumberHeights, 250);
+    window.addEventListener('resize', setLineNumberHeights);
+  }
+  initLineNumberHeights();
+});
+</script>
diff --git a/templates/framework/file-content-part.ezt b/templates/framework/file-content-part.ezt
new file mode 100644
index 0000000..5a89915
--- /dev/null
+++ b/templates/framework/file-content-part.ezt
@@ -0,0 +1,46 @@
+[# Safely display user-content text, such a program source code, with
+   line numbers.
+
+Other EZT variables used:
+  file_lines: List of lines in the file, each with a line number and content.
+  should_prettify: whether the text should be syntax highlighted.
+  prettify_class: additional CSS class used to tell prettify.js how to
+      best syntax highlight this source file.
+]
+
+[# Display the line numbers and source lines in separate columns.
+   See corresponding comments L1, L2, L3 and S1, S2, S3 below.
+   This is messy because the pre tags have significant whitespace, so we
+   break lines inside the tags themslves to make our templates readable.]
+<table class="opened"><tr>
+<td id="nums">
+[# L1. Start with a nocursor row at the top to space the line numbers down the
+       same amount as the source code lines w/ their initial cursor_hidden row.]
+<pre><table width="100%"><tr class="nocursor"><td></td></tr></table></pre>
+
+[# L2. Display each line number in a row that we can refer
+       to by ID, and make each line number a self-link w/ anchor.]
+<pre><table width="100%" id="nums_table_0">[for file_lines]<tr id="gr_[file_lines.num]"
+><td id="[file_lines.num]"><a href="#[file_lines.num]">[file_lines.num]</a></td></tr
+>[end]</table></pre>
+
+[# L3. Finish the line numbers column with another nocursor row to match
+       the spacing of the source code column's final cursor_hidden row.]
+<pre><table width="100%"><tr class="nocursor"><td></td></tr></table></pre>
+</td>
+<td id="lines">
+
+[# S1. Start the source code column with a cursor row. ]
+<pre><table width="100%"><tr class="cursor_stop cursor_hidden"><td></td></tr></table></pre>
+
+[# S2. Display each source code line in a table row and cell
+       that we can identify by id.]
+<pre [if-any should_prettify]class="prettyprint [prettify_class]"[end]><table id="src_table_0">[for file_lines]<tr
+id=sl_[file_lines.num]
+><td class="source">[file_lines.line]<br></td></tr
+>[end]</table></pre>
+
+[# S3. Finish the line numbers column with another cursor stop.]
+<pre><table width="100%"><tr class="cursor_stop cursor_hidden"><td></td></tr></table></pre>
+</td>
+</tr></table>
diff --git a/templates/framework/filter-rule-admin-part.ezt b/templates/framework/filter-rule-admin-part.ezt
new file mode 100644
index 0000000..4822265
--- /dev/null
+++ b/templates/framework/filter-rule-admin-part.ezt
@@ -0,0 +1,155 @@
+<style>
+  #rules th, #rules td {  padding-bottom: 1em }
+</style>
+
+[# If any value is supplied for arg0, the user will be able to set actions
+   that set default owner, set default status, and add CC users.]
+<h4 id="filters">Filter rules</h4>
+<div class="section">
+
+ <div class="closed">
+  <div>Filter rules can help you fill in defaults and stay organized.
+   <a class="ifClosed toggleHidden" href="#"
+      style="font-size:90%; margin-left:.5em">Learn more</a>
+  </div>
+
+  <div id="filterhelp" class="ifOpened help">
+       Filter rules can help your team triage issues by automatically
+       filling in default values based on other values.  They can be used
+       in the same way that you might use message filters in an email client.
+       Filter rules are evaluated after each edit, not just on new items. And,
+       filter rules only add values or set default values, they never override
+       values that were explicitly set by a user.<br>
+       <br>
+       Note that exclusive prefixes still apply.  So, if a user has set a label
+       with one of the exclusive prefixes, a rule that adds another label with
+       the same prefix will have no effect.
+  </div>
+  <br>
+
+  <table border="0" id="rules">
+   <tr>
+    <th></th>
+    <th style="text-align:left">If the issue matches this query:</th>
+    <th colspan="2" style="text-align:left">Then, [if-any arg0]do the following[else]add these labels[end]:</th>
+    <th></th>
+   </tr>
+
+   [for rules]
+   <tr>
+    <td style="text-align:right" width="20">[rules.idx].</td>
+    <td><input type="text" name="predicate[rules.idx]" size="60" value="[rules.predicate]"
+               autocomplete="off" id="predicate_existing_[rules.idx]" class="acob"></td>
+    <td>
+      [if-any arg0]
+       <select name="action_type[rules.idx]">
+         <option value="" disabled="disabled" [is rules.action_type ""]selected="selected"[end]>Choose...</option>
+         <option value="default_status" [is rules.action_type "default_status"]selected="selected"[end]>Set default status:</option>
+         <option value="default_owner" [is rules.action_type "default_owner"]selected="selected"[end]>Set default owner:</option>
+         <option value="add_ccs" [is rules.action_type "add_ccs"]selected="selected"[end]>Add Cc:</option>
+         <option value="add_labels" [is rules.action_type "add_labels"]selected="selected"[end]>Add labels:</option>
+         <option value="also_notify" [is rules.action_type "also_notify"]selected="selected"[end]>Also notify email:</option>
+         <option value="warning" [is rules.action_type "warning"]selected="selected"[end]>Show warning:</option>
+       </select>
+      [end]
+    </td>
+    <td>
+      <input type="text" name="action_value[rules.idx]" size="70" value="[rules.action_value]" class="acob">
+    </td>
+    <td></td>
+   </tr>
+   [end]
+
+   [for new_rule_indexes]
+   <tr id="newrow[new_rule_indexes]" [if-index new_rule_indexes first][else]style="display:none"[end]>
+    <td style="text-align:right" width="20">[new_rule_indexes].</td>
+    <td><input type="text" name="new_predicate[new_rule_indexes]" size="60" value=""
+               class="showNextRuleRow acob" data-index="[new_rule_indexes]"
+               autocomplete="off" id="predicate_new_[new_rule_indexes]"></td>
+    <td>
+      [if-any arg0]
+       <select name="new_action_type[new_rule_indexes]">
+         <option value="" disabled="disabled" selected="selected">Choose...</option>
+         <option value="default_status">Set default status:</option>
+         <option value="default_owner">Set default owner:</option>
+         <option value="add_ccs">Add Cc:</option>
+         <option value="add_labels">Add labels:</option>
+         <option value="also_notify">Also notify email:</option>
+         <option value="warning">Show warning:</option>
+       </select>
+      [end]
+    </td>
+    <td>
+      <input type="text" name="new_action_value[new_rule_indexes]" size="70" value="" class="acob">
+      [# TODO(jrobbins): figure out a way to display error messages on each rule. ]
+    </td>
+    <td width="40px">
+     [if-index new_rule_indexes last][else]
+      <span id="addrow[new_rule_indexes]" class="fakelink" class="fakelink" data-index="[new_rule_indexes]">Add a row</span
+     [end]
+    </td>
+   </tr>
+   [end]
+
+  </table>
+ </div>
+
+ [if-any errors.rules]
+  [for errors.rules]
+    <div class="fielderror">[errors.rules]</div>
+  [end]
+  <script type="text/javascript" nonce="[nonce]">
+runOnLoad(function() {
+   document.location.hash = 'filters';
+});
+  </script>
+ [end]
+
+</div>
+
+<script type="text/javascript" nonce="[nonce]">
+runOnLoad(function() {
+  function showNextRuleRow(i) {
+   if (i < [max_rules]) {
+     _showID('newrow' + (i + 1));
+     _hideID('addrow' + i);
+   }
+  }
+
+  var addARowLinks = document.getElementsByClassName("fakelink");
+  for (var i = 0; i < addARowLinks.length; ++i) {
+    var link = addARowLinks[[]i];
+    link.addEventListener("click", function(event) {
+        var index = Number(event.target.getAttribute("data-index"));
+        showNextRuleRow(index);
+    });
+  }
+
+  var typeToAddARow = document.getElementsByClassName("showNextRuleRow");
+  for (var i = 0; i < typeToAddARow.length; ++i) {
+    var el = typeToAddARow[[]i];
+    el.addEventListener("keydown", function(event) {
+        var index = Number(event.target.getAttribute("data-index"));
+        showNextRuleRow(index);
+    });
+  }
+
+  var acobElements = document.getElementsByClassName("acob");
+  for (var i = 0; i < acobElements.length; ++i) {
+     var el = acobElements[[]i];
+     el.addEventListener("focus", function(event) {
+         _acrob(null);
+         _acof(event);
+     });
+  }
+
+  var acobElements = document.getElementsByClassName("acob");
+  for (var i = 0; i < acobElements.length; ++i) {
+     var el = acobElements[[]i];
+     el.addEventListener("focus", function(event) {
+         _acrob(null);
+         _acof(event);
+     });
+  }
+});
+</script>
diff --git a/templates/framework/footer-script.ezt b/templates/framework/footer-script.ezt
new file mode 100644
index 0000000..5e338ea
--- /dev/null
+++ b/templates/framework/footer-script.ezt
@@ -0,0 +1,34 @@
+[# The order of imports matters in this file. Scripts are imported after other scripts they depend on.]
+
+<script type="text/javascript" defer src="[version_base]/static/js/graveyard/common.js" nonce="[nonce]"></script>
+<script type="text/javascript" defer src="[version_base]/static/js/graveyard/listen.js" nonce="[nonce]"></script>
+<script type="text/javascript" defer src="[version_base]/static/js/graveyard/xmlhttp.js" nonce="[nonce]"></script>
+<script type="text/javascript" defer src="[version_base]/static/js/graveyard/shapes.js" nonce="[nonce]"></script>
+<script type="text/javascript" defer src="[version_base]/static/js/graveyard/geom.js" nonce="[nonce]"></script>
+<script type="text/javascript" defer src="[version_base]/static/js/graveyard/popup_controller.js" nonce="[nonce]"></script>
+
+[if-any is_ezt]
+  [# Note that this file will be requested twice on some pages, but chrome is smart enough
+    to not even request it the second time.]
+  <script type="text/javascript" defer src="[version_base]/static/js/framework/framework-ajax.js" nonce="[nonce]"></script>
+
+  <script type="text/javascript" defer src="[version_base]/static/js/tracker/externs.js" nonce="[nonce]"></script>
+[end]
+<script type="text/javascript" defer src="[version_base]/static/js/tracker/ac.js" nonce="[nonce]"></script>
+<script type="text/javascript" defer src="[version_base]/static/js/tracker/tracker-ac.js" nonce="[nonce]"></script>
+<script type="text/javascript" defer src="[version_base]/static/js/tracker/tracker-install-ac.js" nonce="[nonce]"></script>
+[if-any is_ezt]
+  <script type="text/javascript" defer src="[version_base]/static/js/tracker/tracker-components.js" nonce="[nonce]"></script>
+  <script type="text/javascript" defer src="[version_base]/static/js/tracker/tracker-dd.js" nonce="[nonce]"></script>
+  <script type="text/javascript" defer src="[version_base]/static/js/tracker/tracker-display.js" nonce="[nonce]"></script>
+[end]
+<script type="text/javascript" defer src="[version_base]/static/js/tracker/tracker-editing.js" nonce="[nonce]"></script>
+[if-any is_ezt]
+  <script type="text/javascript" defer src="[version_base]/static/js/tracker/tracker-fields.js" nonce="[nonce]"></script>
+  <script type="text/javascript" defer src="[version_base]/static/js/tracker/tracker-keystrokes.js" nonce="[nonce]"></script>
+  <script type="text/javascript" defer src="[version_base]/static/js/tracker/tracker-nav.js" nonce="[nonce]"></script>
+  <script type="text/javascript" defer src="[version_base]/static/js/tracker/tracker-update-issues-hotlists.js" nonce="[nonce]"></script>
+  <script type="text/javascript" defer src="[version_base]/static/js/tracker/tracker-util.js" nonce="[nonce]"></script>
+  <script type="text/javascript" defer src="[version_base]/static/js/tracker/tracker-onload.js" nonce="[nonce]"></script>
+  <script type="text/javascript" defer src="[version_base]/static/js/framework/clientmon.js" nonce="[nonce]"></script>
+[end]
diff --git a/templates/framework/footer-shared.ezt b/templates/framework/footer-shared.ezt
new file mode 100644
index 0000000..9630e4c
--- /dev/null
+++ b/templates/framework/footer-shared.ezt
@@ -0,0 +1,107 @@
+[# This template displays the part of the footer used by both web components and EZT pages. ]
+
+<div id="footer">
+  [if-any old_ui_url]
+    <a href="[old_ui_url]">
+      View in the old UI
+    </a>
+  [else][if-any new_ui_url]
+    <a href="[new_ui_url]">
+      View in the new UI
+    </a>
+  [end][end]
+  [is projectname "fuchsia"]
+    <a href="https://bugs.fuchsia.dev/p/fuchsia/issues/entry?template=Report+Community+Abuse" title="Monorail [app_version]">Report Abuse</a>
+  [end]
+  <a href="https://bugs.chromium.org/p/monorail/adminIntro" title="Monorail [app_version]">About Monorail</a>
+  <a href="https://chromium.googlesource.com/infra/infra/+/main/appengine/monorail/doc/userguide/README.md">User Guide</a>
+  <a href="https://chromium.googlesource.com/infra/infra/+/main/appengine/monorail/doc/release-notes.md">Release Notes</a>
+  <a href="https://bugs.chromium.org/p/monorail/issues/entry?template=Online%20Feedback" target="_blank">Feedback on Monorail</a>
+  <a href="https://chromium.googlesource.com/infra/infra/+/main/appengine/monorail/doc/terms.md">Terms</a>
+  <a href="https://www.google.com/policies/privacy/">Privacy</a>
+</div>
+
+[include "debug.ezt"]
+
+[include "../webpack-out/ezt-footer-scripts-package.ezt"]
+
+<script type="module" nonce="[nonce]">
+// Load and instantiate pRPC client before any other script.
+window.prpcClient = new AutoRefreshPrpcClient(
+  CS_env.token, CS_env.tokenExpiresSec);
+</script>
+
+[if-any is_ezt]
+  <script type="text/javascript" defer src="[version_base]/static/js/framework/externs.js" nonce="[nonce]"></script>
+  <script type="text/javascript" defer src="[version_base]/static/js/framework/env.js" nonce="[nonce]"></script>
+  <script type="text/javascript" defer src="[version_base]/static/js/framework/framework-ajax.js" nonce="[nonce]"></script>
+  <script type="text/javascript" defer src="[version_base]/static/js/framework/framework-cues.js" nonce="[nonce]"></script>
+  <script type="text/javascript" defer src="[version_base]/static/js/framework/framework-display.js" nonce="[nonce]"></script>
+  <script type="text/javascript" defer src="[version_base]/static/js/framework/framework-menu.js" nonce="[nonce]"></script>
+  <script type="text/javascript" defer src="[version_base]/static/js/framework/framework-myhotlists.js" nonce="[nonce]"></script>
+  <script type="text/javascript" defer src="[version_base]/static/js/framework/framework-stars.js" nonce="[nonce]"></script>
+  <script type="text/javascript" defer src="[version_base]/static/js/framework/project-name-check.js" nonce="[nonce]"></script>
+  <script type="text/javascript" defer src="[version_base]/static/js/graveyard/xmlhttp.js" nonce="[nonce]"></script>
+[end]
+[include "footer-script.ezt"]
+
+
+[if-any is_ezt]
+  <script type="text/javascript" nonce="[nonce]">
+  runOnLoad(function() {
+    var toggles = document.getElementsByClassName("toggleHidden");
+    for (var i = 0; i < toggles.length; ++i) {
+      var toggle = toggles[[]i];
+      toggle.addEventListener("click", function (event) {
+          _toggleHidden(event.target);
+          event.preventDefault();
+      });
+    }
+
+    toggles = document.getElementsByClassName("toggleCollapse");
+    for (var i = 0; i < toggles.length; ++i) {
+      var toggle = toggles[[]i];
+      toggle.addEventListener("click", function (event) {
+          _toggleCollapse(event.target);
+          event.preventDefault();
+      });
+    }
+
+    [if-any form_token]
+      var tokenFields = document.querySelectorAll("input[[]name=token]");
+      for (var i = 0; i < tokenFields.length; ++i) {
+        var field = tokenFields[[]i];
+        field.form.addEventListener("submit", function(event) {
+            refreshTokens(
+                event, "[form_token]", "[form_token_path]", [token_expires_sec]);
+        });
+      }
+    [end]
+
+    [if-any project]
+      _fetchUserProjects(false);
+    [end]
+    _onload();
+
+  });
+  </script>
+[else]
+  <script type="text/javascript" nonce="[nonce]">
+  runOnLoad(function() {
+    TKR_install_ac();
+  });
+  </script>
+[end]
+
+<script type="text/javascript" nonce="[nonce]">
+runOnLoad(function() {
+  // CrDX Feedback Button
+  (function(i,s,o,g,r,a,m){i[[]'CrDXObject']=r;i[[]r]=i[[]r]||function(){
+  (i[[]r].q=i[[]r].q||[]).push(arguments)},a=s.createElement(o),
+  m=s.getElementsByTagName(o)[0];a.async=1;a.setAttribute('nonce','[nonce]');
+  a.src=g;m.parentNode.insertBefore(a,m)
+  })(window,document,'script','https://storage.googleapis.com/chops-feedback/feedback.js','crdx');
+
+  crdx('setFeedbackButtonLink', 'https://bugs.chromium.org/p/monorail/issues/entry?template=Online%20Feedback');
+});
+</script>
diff --git a/templates/framework/footer.ezt b/templates/framework/footer.ezt
new file mode 100644
index 0000000..963b61d
--- /dev/null
+++ b/templates/framework/footer.ezt
@@ -0,0 +1,42 @@
+</div> [# End <div id="maincol"> from header.ezt]
+
+[include "footer-shared.ezt"]
+
+<script type="text/javascript" nonce="[nonce]">
+// Google Analytics
+(function(i,s,o,g,r,a,m){i[[]'GoogleAnalyticsObject']=r;i[[]r]=i[[]r]||function(){
+(i[[]r].q=i[[]r].q||[[]]).push(arguments)},i[[]r].l=1*new Date();a=s.createElement(o),
+m=s.getElementsByTagName(o)[[]0];a.async=1;a.setAttribute('nonce','[nonce]');
+a.src=g;m.parentNode.insertBefore(a,m)
+})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
+
+(function setupGoogleAnalytics() {
+  const _EMAIL_REGEX =
+      ["/([a-zA-Z0-9_\-\.]+)@([a-zA-Z0-9_\-\.]+)\.([a-zA-Z]{2,5})/"];
+
+  ga('create', '[analytics_id]', {'siteSpeedSampleRate': 100});
+
+  [if-any logged_in_user]
+    ga('set', 'dimension1', 'Logged in');
+  [else]
+    ga('set', 'dimension1', 'Not logged in');
+  [end]
+
+  const path = window.location.href.slice(window.location.origin.length);
+  if (path.startsWith('/u')) {
+    [# Keep anything that looks like an email address out of GA.]
+    ga('set', 'title', 'A user page');
+    ga('set', 'location', path.replace(_EMAIL_REGEX, 'user@example.com'));
+  }
+
+  ga('send', 'pageview');
+})();
+</script>
+
+<ezt-app-base [if-any logged_in_user]
+  userDisplayName="[logged_in_user.email]"[end]
+  projectName="[projectname]"
+></ezt-app-base>
+
+</body>
+</html>
diff --git a/templates/framework/group-setting-fields.ezt b/templates/framework/group-setting-fields.ezt
new file mode 100644
index 0000000..4d62ab2
--- /dev/null
+++ b/templates/framework/group-setting-fields.ezt
@@ -0,0 +1,95 @@
+[# Diplay a widget to choose group visibility level, or read-only text showing
+   the visibility level.  Read-only text is used when the user does not have
+   permission to edit, or if there is only one available choice.
+]
+
+[define vis_menu_was_shown]False[end]
+
+[if-any read_only][else]
+  <select name="visibility" id="visibility" [if-any import_group]disabled="disabled"[end]>
+    <option value="" disabled="disabled" [if-any initial_visibility][else]selected="selected"[end]>
+      Select a visibility level...
+    </option>
+    [for visibility_levels]
+      <option value="[visibility_levels.key]"
+        [if-any initial_visibility]
+          [is initial_visibility.key visibility_levels.key]selected="selected"[end]
+        [end]>
+        [visibility_levels.name]
+      </option>
+    [end]
+  </select>
+  [define vis_menu_was_shown]True[end]
+
+  <br><br>
+  Friend projects: <br>
+  <input size="60" type="text" id="friendprojects" name="friendprojects" value="[initial_friendprojects]">
+  <div class="fielderror">
+    <span id="friendprojectsfeedback"></span>
+    [if-any errors.friendprojects][errors.friendprojects][end]
+  </div>
+
+  <br><br>
+  <input type="checkbox" name="import_group" id="import_group"
+         [if-any import_group]checked="checked"[end]
+         [if-any groupadmin]disabled="disabled"[end] >
+  <label for="import_group">Import from external group</label>
+
+  <div class="fielderror">
+    <span id="groupimportfeedback"></span>
+    [if-any errors.groupimport][errors.groupimport][end]
+  </div>
+
+  <br>
+  &nbsp;&nbsp;External group type:
+  <select name="group_type" id="group_type"
+          [if-any import_group][else]disabled="disabled"[end]
+          [if-any groupadmin]disabled="disabled"[end] >
+    <option value="" disabled="disabled" [if-any initial_group_type][else]selected="selected"[end]>
+      Select a group type...
+    </option>
+    [for group_types]
+      <option value="[group_types.key]"
+        [if-any initial_group_type]
+          [is initial_group_type.key group_types.key]selected="selected"[end]
+        [end]>
+        [group_types.name]
+      </option>
+    [end]
+  </select>
+  <br><br>
+
+  <script type="text/javascript" nonce="[nonce]">
+runOnLoad(function() {
+    cur_vis_value = $("visibility").value;
+
+    function _updateSettings() {
+      if ($("import_group").checked) {
+        $("group_type").disabled = false;
+        cur_vis_value = $("visibility").value;
+        $("visibility").value = 0;
+        $("visibility").disabled = true;
+        $("friendprojects").disabled = true;
+      } else {
+        $("group_type").disabled = true;
+        $("visibility").value = cur_vis_value;
+        $("visibility").disabled = false;
+        $("friendprojects").disabled = false;
+      }
+    }
+
+    $("import_group").addEventListener("click", _updateSettings);
+});
+  </script>
+[end]
+
+[is vis_menu_was_shown "False"]
+  [initial_visibility.name]
+  <input type="hidden" name="visibility" value="[initial_visibility.key]">
+[end]
+
+<div class="formerror">
+  [if-any errors.access]
+    <div class="emphasis">[errors.access]</div>
+  [end]
+</div>
\ No newline at end of file
diff --git a/templates/framework/header-shared.ezt b/templates/framework/header-shared.ezt
new file mode 100644
index 0000000..614fe5c
--- /dev/null
+++ b/templates/framework/header-shared.ezt
@@ -0,0 +1,115 @@
+<!DOCTYPE html>
+[# This is the part of header.ezt used by both the legacy
+   Monorail UI, and the new Polymer pages.
+]
+<html lang="en">
+<head>
+  <link rel="icon" type="image/vnd.microsoft.icon" href="/static/images/monorail.ico">
+  [if-any link_rel_canonical]
+    <link rel="canonical" href="[link_rel_canonical]">
+  [end]
+
+  <script type="text/javascript" nonce="[nonce]">
+   [# Javascript object containing basic page data. ]
+   window.CS_env = {
+     'absoluteBaseUrl': '[format "js"][absolute_base_url][end]',
+     'app_version': '[format "js"][app_version][end]',
+     'token': '[format "js"][xhr_token][end]',
+     'tokenExpiresSec': [format "js"][token_expires_sec][end],
+     'loggedInUserEmail':
+       [if-any logged_in_user]
+         '[format "js"][logged_in_user.email][end]'
+       [else]
+         null
+       [end],
+     'login_url': '[format "js"][login_url][end]',
+     'logout_url': '[format "js"][logout_url][end]',
+     'profileUrl':
+       [if-any logged_in_user]
+         '[format "js"][logged_in_user.profile_url][end]'
+       [else]
+         null
+       [end],
+     'projectName': '[format "js"][projectname][end]',
+     'projectIsRestricted': [if-any project_is_restricted]true[else]false[end],
+     'is_member': '[format "js"][is_member][end]',
+     'gapi_client_id': '[format "js"][gapi_client_id][end]',
+   };
+  </script>
+
+  [# Improve the snippet that appears in search]
+  [if-any show_search_metadata]
+    <meta name="Description" content="Monorail is simple, reliable, and flexible issue tracking tool.">
+    <meta name="robots" content="NOODP">
+  [end]
+
+    <title>
+      [if-any title][title] - [end]
+      [if-any title_summary][title_summary] - [end]
+      [if-any projectname]
+        [projectname] -
+      [else]
+        [if-any viewing_user_page][viewed_user.display_name] - [end]
+      [end]
+      [if-any title_summary][else]
+        [if-any project_summary][project_summary] - [end]
+      [end]
+      [site_name]
+    </title>
+    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
+    <meta name="referrer" content="no-referrer">
+    [if-any robots_no_index]
+     <meta name="ROBOTS" content="NOINDEX,NOARCHIVE">
+    [else]
+     <meta name="ROBOTS" content="NOARCHIVE">
+    [end]
+    <meta name="viewport" content="width=device-width, minimum-scale=1.0">
+    <link type="text/css" rel="stylesheet" href="[version_base]/static/css/chopsui-normal.css">
+
+    [if-any is_ezt]
+      <link type="text/css" rel="stylesheet" href="[version_base]/static/css/ph_core.css">
+      <link type="text/css" rel="stylesheet" media="(max-width:425px)"
+            href="[version_base]/static/css/ph_mobile.css">
+
+      [if-any category_css]
+        <link type="text/css" rel="stylesheet" href="[version_base]/static/[category_css]">
+      [end]
+      [if-any category2_css]
+        <link type="text/css" rel="stylesheet" href="[version_base]/static/[category2_css]">
+      [end]
+      [if-any page_css]
+        <link type="text/css" rel="stylesheet" href="[version_base]/static/[page_css]">
+      [end]
+    [end]
+
+    <!-- Lazy load icons. -->
+    <link rel="stylesheet"
+      href="https://fonts.googleapis.com/icon?family=Material+Icons"
+      media="none"
+      id="icons-stylesheet">
+    <script type="module" async defer nonce="[nonce]">
+      document.getElementById('icons-stylesheet').media = 'all';
+    </script>
+    [# NO MORE SCRIPTS IN HEAD, it makes page loading too slow.]
+</head>
+
+[# Tiny script used sitewide. ]
+<script type="text/javascript" nonce="[nonce]">
+   function _go(url, newWindow) {
+     if (newWindow)
+       window.open(url, '_blank');
+     else
+       document.location = url;
+   }
+   function $(id) { return document.getElementById(id); }
+
+   var loadQueue = [];
+   function runOnLoad(fn) { loadQueue.push(fn); }
+
+   window.onload = function() {
+     for (var i = 0; i < loadQueue.length; i++)
+       loadQueue[[]i]();
+     delete loadQueue;
+   };
+</script>
diff --git a/templates/framework/header.ezt b/templates/framework/header.ezt
new file mode 100644
index 0000000..fe798af
--- /dev/null
+++ b/templates/framework/header.ezt
@@ -0,0 +1,39 @@
+[# This is the main header file that is included in all Monorail servlets that render a page.
+
+   Args:
+     arg0: Can be "showtabs", "showusertabs" or "showusergrouptabs" to select which top-plevel tabs are shown.
+     arg1: String like "t1", "t2", "t3" to identify the currently active tab.
+]
+[define is_ezt]Yes[end]
+[include "header-shared.ezt"]
+
+[include "../webpack-out/ezt-element-package.ezt"]
+
+<body class="[main_tab_mode] [if-any perms.EditIssue]perms_EditIssue[end]">
+
+[# Tiny script used sitewide. ]
+<script type="text/javascript" nonce="[nonce]">
+   function _go(url, newWindow) {
+     if (newWindow)
+       window.open(url, '_blank');
+     else
+       document.location = url;
+   }
+   function $(id) { return document.getElementById(id); }
+
+   var loadQueue = [];
+   function runOnLoad(fn) { loadQueue.push(fn); }
+
+   window.onload = function() {
+     for (var i = 0; i < loadQueue.length; i++)
+       loadQueue[[]i]();
+     delete loadQueue;
+   };
+</script>
+
+[include "maintabs.ezt" arg0 arg1]
+
+[include "banner_message.ezt"]
+
+<div id="maincol">
+[include "alert.ezt"]
diff --git a/templates/framework/js-placeholders.ezt b/templates/framework/js-placeholders.ezt
new file mode 100644
index 0000000..ae3548a
--- /dev/null
+++ b/templates/framework/js-placeholders.ezt
@@ -0,0 +1,12 @@
+[# Empty function definitions because we load the JS at bottom of page.
+   Without this, some rollovers or on-click handlers might give errors for
+   the first 200ms or so after the page loads. With them, they simply are
+   no-ops. ]
+<script type="text/javascript" nonce="[nonce]">
+runOnLoad(function() {
+function _showBelow(){}
+function _toggleStar(){}
+function _goIssue(){}
+function _goFile(){}
+});
+</script>
diff --git a/templates/framework/label-validation-row.ezt b/templates/framework/label-validation-row.ezt
new file mode 100644
index 0000000..4c2d1a3
--- /dev/null
+++ b/templates/framework/label-validation-row.ezt
@@ -0,0 +1,6 @@
+<tr>
+  <td colspan="3">
+    <div id="confirmarea" class="novel"><span id="confirmmsg"></span></div>
+    <div id="blocksubmitarea" class="blockingsubmit"><span id="blocksubmitmsg"></span></div>
+  </td>
+</tr>
diff --git a/templates/framework/maintabs.ezt b/templates/framework/maintabs.ezt
new file mode 100644
index 0000000..416b992
--- /dev/null
+++ b/templates/framework/maintabs.ezt
@@ -0,0 +1,99 @@
+[# Show top-level tabs.
+
+   Args:
+     arg0: Can be "showtabs", or "showusertabs" to select which
+         top-level tabs are shown.
+     arg1: String like "t1", "t2", "t3" to identify the currently active tab.
+]
+[if-any projectname]
+
+[# Non-fixed container around mr-header to allow the fixed header to "take up space". ]
+<div style="width: 100%; height: var(--monorail-header-height); margin-bottom: -1px;">
+  <mr-header
+    [if-any logged_in_user]
+    userDisplayName="[logged_in_user.email]"
+    [end]
+    projectThumbnailUrl="[project_thumbnail_url]"
+    projectName="[projectname]"
+    loginUrl="[login_url]"
+    logoutUrl="[logout_url]"
+  ></mr-header>
+</div>
+[else]
+<table id="monobar" width="100%" cellpadding="0" cellspacing="0" role="presentation">
+  <tr>
+    <th class="padded">
+      <a href="/" id="wordmark">[site_name]</a>
+    </th>
+    [if-any viewed_user]
+      <th class="padded">
+        User: <a href="[viewed_user.profile_url]">[viewed_user.display_name]</a>
+        [if-any viewed_user_pb.is_site_admin_bool]<i>(Administrator)</i>[end]
+      </th>
+    [end]
+    [if-any hotlist_id]
+      <th class="toptabs padded">
+      <a href="[hotlist.url]" title="[hotlist_id]"
+        id = "hotlists-dropdown">Hotlist: [hotlist.name] <small>&#9660;</small></a>
+      <a href="[hotlist.url]" class="[is main_tab_mode "ht2"]active[end]">Issues</a>
+      <a href="[hotlist.url]/people" class="[is main_tab_mode "ht3"]active[end]">People</a>
+      <a href="[hotlist.url]/details" class="[is main_tab_mode "ht4"]active[end]">Settings</a>
+      </th>
+    [end]
+
+    <td width="100%" id="userbar">
+      [include "user-bar.ezt"]
+    </td>
+  </tr>
+</table>
+[end]
+
+<script type="text/javascript" nonce="[nonce]">
+runOnLoad(function() {
+  if ($("hotlists-dropdown"))
+    $("hotlists-dropdown").addEventListener("click", function(event) {
+        event.preventDefault();
+    });
+});
+</script>
+
+[is arg0 "showtabs"]
+  <div class="subt">
+    [include "projecttabs.ezt"]
+  </div>
+[else][is arg0 "showusertabs"]
+  <div class="subt">
+    [include "../sitewide/usertabs.ezt" arg1]
+  </div>
+[else][is arg0 "showusergrouptabs"]
+  <div class="subt">
+    [include "../sitewide/usergrouptabs.ezt" arg1]
+  </div>
+[end][end][end]
+
+[if-any warnings]
+  <table align="center" border="0" cellspacing="0" cellpadding="0" style="margin-bottom: 6px">
+   [for warnings]
+     <tr><td class="notice">
+         [warnings]
+     </td></tr>
+   [end]
+  </table>
+[end]
+[if-any errors.query]
+  <table align="center" border="0" cellspacing="0" cellpadding="0" style="margin-bottom: 6px">
+   <tr><td class="notice">
+       [errors.query]
+   </td></tr>
+  </table>
+[end]
+
+[if-any site_read_only][else]
+  [if-any project_alert]
+    <div style="font-weight: bold; color: #c00; margin-top: 5px; display: block;">
+      [project_alert]
+    </div>
+  [end]
+[end]
+
+[include "../features/cues.ezt"]
diff --git a/templates/framework/polymer-footer.ezt b/templates/framework/polymer-footer.ezt
new file mode 100644
index 0000000..a83d40f
--- /dev/null
+++ b/templates/framework/polymer-footer.ezt
@@ -0,0 +1,14 @@
+[include "footer-shared.ezt"]
+
+<script type="text/javascript" nonce="[nonce]">
+// Google Analytics
+(function(i,s,o,g,r,a,m){i[[]'GoogleAnalyticsObject']=r;i[[]r]=i[[]r]||function(){
+(i[[]r].q=i[[]r].q||[[]]).push(arguments)},i[[]r].l=1*new Date();a=s.createElement(o),
+m=s.getElementsByTagName(o)[[]0];a.async=1;a.setAttribute('nonce','[nonce]');
+a.src=g;m.parentNode.insertBefore(a,m)
+})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
+
+(function setupGoogleAnalytics() {
+  ga('create', '[analytics_id]', {'siteSpeedSampleRate': 100});
+})();
+</script>
diff --git a/templates/framework/project-access-part.ezt b/templates/framework/project-access-part.ezt
new file mode 100644
index 0000000..2400dfe
--- /dev/null
+++ b/templates/framework/project-access-part.ezt
@@ -0,0 +1,36 @@
+[# Diplay a widget to choose project access level, or read-only text showing
+   the access level.  Read-only text is used when the user does not have
+   permission to edit, or if there is only one available choice.
+]
+
+[define access_menu_was_shown]False[end]
+
+[if-any read_only][else]
+  [if-any offer_access_level]
+    <select name="access" id="access">
+      <option value="" disabled="disabled" [if-any initial_access][else]selected="selected"[end]>
+        Select an access level...
+      </option>
+      [for available_access_levels]
+        <option value="[available_access_levels.key]"
+          [if-any initial_access]
+            [is initial_access.key available_access_levels.key]selected="selected"[end]
+          [end]>
+          [available_access_levels.name]
+        </option>
+      [end]
+    </select>
+    [define access_menu_was_shown]True[end]
+  [end]
+[end]
+
+[is access_menu_was_shown "False"]
+  [initial_access.name]
+  <input type="hidden" name="access" value="[initial_access.key]">
+[end]
+
+<div class="formerror">
+  [if-any errors.access]
+    <div class="emphasis">[errors.access]</div>
+  [end]
+</div>
diff --git a/templates/framework/project-descriptive-fields.ezt b/templates/framework/project-descriptive-fields.ezt
new file mode 100644
index 0000000..b237f20
--- /dev/null
+++ b/templates/framework/project-descriptive-fields.ezt
@@ -0,0 +1,41 @@
+Summary:<br>
+<input type="text" id="summary" name="summary" size="75" value="[initial_summary]"><br>
+<div class="fielderror">&nbsp;
+   <span id="summaryfeedback">[if-any errors.summary][errors.summary][end]</span>
+</div>
+
+Description:<br>
+<textarea id="description" name="description" rows="20" cols="90" wrap="soft"
+          >[initial_description]</textarea><br>
+<div class="fielderror">&nbsp;
+  <span id="descriptionfeedback">[if-any errors.description][errors.description][end]</span>
+</div>
+
+Project home page (optional):<br/>
+<input type="text" id="project_home" name="project_home" size="75" value="[initial_project_home]"><br>
+<div class="fielderror">&nbsp;
+  <span id="project_homefeedback">[if-any errors.project_home][errors.project_home][end]</span>
+</div>
+
+Project documentation page (optional):<br/>
+<input type="text" id="docs_url" name="docs_url" size="75" value="[initial_docs_url]"><br>
+<div class="fielderror">&nbsp;
+  <span id="docs_urlfeedback">[if-any errors.docs_url][errors.docs_url][end]</span>
+</div>
+
+Project source browser (optional):<br/>
+<input type="text" id="source_url" name="source_url" size="75" value="[initial_source_url]"><br>
+<div class="fielderror">&nbsp;
+  <span id="source_urlfeedback">[if-any errors.source_url][errors.source_url][end]</span>
+</div>
+
+[if-any logo_view.viewurl]
+  Project logo:<br>
+  [include "display-project-logo.ezt" True]
+[else]
+  Upload project logo (optional, will be resized to 110x30):<br/>
+  <input type="file" name="logo" id="logo">
+  <div class="fielderror">&nbsp;
+    <span id="logofeedback">[if-any errors.logo][errors.logo][end]</span>
+  </div>
+[end]
diff --git a/templates/framework/projecttabs.ezt b/templates/framework/projecttabs.ezt
new file mode 100644
index 0000000..6a05c74
--- /dev/null
+++ b/templates/framework/projecttabs.ezt
@@ -0,0 +1,25 @@
+[is main_tab_mode "t4"]
+  <div class="[admin_tab_mode]">
+      <div class="at isf">
+       <span class="inst1"><a href="/p/[projectname]/adminIntro">Introduction</a></span>
+       <span class="inst3"><a href="/p/[projectname]/adminStatuses">Statuses</a></span>
+       <span class="inst4"><a href="/p/[projectname]/adminLabels">Labels and fields</a></span>
+       [if-any perms.EditProject][# Rule might be too sensitive for non-members to view.]
+         <span class="inst5"><a href="/p/[projectname]/adminRules">Rules</a></span>
+       [end]
+       <span class="inst6"><a href="/p/[projectname]/adminTemplates">Templates</a></span>
+       <span class="inst7"><a href="/p/[projectname]/adminComponents">Components</a></span>
+       <span class="inst8"><a href="/p/[projectname]/adminViews">Views</a></span>
+     </div>
+  </div>
+[end]
+
+
+[is main_tab_mode "t6"]
+  <div class="[admin_tab_mode]">
+      <div class="at isf">
+       <span class="inst1"><a href="/p/[projectname]/admin">General</a></span>
+       <span class="inst9"><a href="/p/[projectname]/adminAdvanced">Advanced</a></span>
+      </div>
+  </div>
+[end]
diff --git a/templates/framework/read-only-rejection.ezt b/templates/framework/read-only-rejection.ezt
new file mode 100644
index 0000000..3147d9a
--- /dev/null
+++ b/templates/framework/read-only-rejection.ezt
@@ -0,0 +1,10 @@
+<span style="color:#a30">
+  [if-any site_read_only]
+    This operation cannot be done when the site is read-only.
+    Please come back later.
+  [else]
+    [if-any project_read_only]
+      READ-ONLY
+    [end]
+  [end]
+</span>
diff --git a/templates/framework/saved-queries-admin-part.ezt b/templates/framework/saved-queries-admin-part.ezt
new file mode 100644
index 0000000..6514e30
--- /dev/null
+++ b/templates/framework/saved-queries-admin-part.ezt
@@ -0,0 +1,135 @@
+[# arg0 is either "user" for user saved queries or "project" for canned queries]
+<style>
+  #queries th, #queries td {  padding-bottom: 1em }
+</style>
+
+<table border="0" id="queries">
+   <tr>
+    <th></th>
+    <th style="text-align:left">Saved query name:</th>
+    [is arg0 "user"]
+      <th style="text-align:left">Project(s):</th>
+    [end]
+    <th colspan="2" style="text-align:left">Query:</th>
+    [is arg0 "user"]
+      <th style="text-align:left">Subscription options:</th>
+    [end]
+    <th></th>
+   </tr>
+
+   [for canned_queries]
+   <tr>
+    <td style="text-align:right" width="20">[canned_queries.idx].
+      <input type="hidden" name="savedquery_id_[canned_queries.idx]" value="[canned_queries.query_id]">
+    </td>
+    <td><input type="text" name="savedquery_name_[canned_queries.idx]" size="35" value="[canned_queries.name]" class="acob"></td>
+    [is arg0 "user"]
+      <td><input type="text" name="savedquery_projects_[canned_queries.idx]" size="35" value="[canned_queries.projects]"
+           class="acob" autocomplete="off" id="savedquery_projects_[canned_queries.idx]"></td>
+    [end]
+
+    <td>
+       <select name="savedquery_base_[canned_queries.idx]">
+         [define can][canned_queries.base_query_id][end]
+         [include "../tracker/issue-can-widget.ezt" "admin"]
+       </select>
+    </td>
+    <td>
+      <input type="text" name="savedquery_query_[canned_queries.idx]" size="50" value="[canned_queries.query]" autocomplete="off" id="query_existing_[canned_queries.idx]" class="acob">
+    </td>
+    [is arg0 "user"]
+      <td>
+        <select id="savedquery_sub_mode_[canned_queries.idx]" name="savedquery_sub_mode_[canned_queries.idx]">
+          <option [is canned_queries.subscription_mode "noemail"]selected="select"[end] value="noemail"
+                  >No emails</option>
+          <option [is canned_queries.subscription_mode "immediate"]selected="select"[end] value="immediate">Notify Immediately</option>
+          [# TODO(jrobbins): <option disabled="disabled">Notify Daily</option>]
+          [# TODO(jrobbins): <option disabled="disabled">Notify Weekly on Monday</option>]
+        </select>
+      </td>
+    [end]
+    <td></td>
+   </tr>
+   [end]
+
+   [is arg0 "user"]
+     [define can]1[end][# All blank lines for user queries default to "All Issues" scope.]
+   [else]
+     [define can]2[end][# All blank lines for project queries default to "Open issues" scope.]
+   [end]
+   [for new_query_indexes]
+   <tr id="newquery[new_query_indexes]" [if-index new_query_indexes first][else]style="display:none"[end]>
+    <td style="text-align:right" width="20">[new_query_indexes].</td>
+    <td><input type="text" name="new_savedquery_name_[new_query_indexes]"
+               class="showNextQueryRow acob" data-index="[new_query_indexes]"
+               size="35" value="" placeholder="Required"></td>
+    [is arg0 "user"]
+      <td><input type="text" name="new_savedquery_projects_[new_query_indexes]" size="35" value="" class="acob"
+           autocomplete="off" id="new_savedquery_projects_[new_query_indexes]" placeholder="Optional"></td>
+    [end]
+    <td>
+       <select name="new_savedquery_base_[new_query_indexes]">
+         [include "../tracker/issue-can-widget.ezt" "admin"]
+       </select>
+    </td>
+    <td>
+      <input type="text" name="new_savedquery_query_[new_query_indexes]" size="50" value="" autocomplete="off" id="query_new_[new_query_indexes]" class="acob" placeholder="Optional. Example- &quot;label:Security owner:me&quot;">
+    </td>
+    [is arg0 "user"]
+      <td>
+        <select id="new_savedquery_sub_mode_[new_query_indexes]" name="new_savedquery_sub_mode_[new_query_indexes]">
+          <option selected="selected" value="noemail">No emails</option>
+          <option value="immediate">Notify Immediately</option>
+          [# TODO(jrobbins): <option disabled="disabled">Notify Daily</option>]
+          [# TODO(jrobbins): <option disabled="disabled">Notify Weekly</option>]
+        </select>
+      </td>
+    [end]
+    <td width="40px">
+     [if-index new_query_indexes last][else]
+      <span id="addquery[new_query_indexes]" class="fakelink" data-index="[new_query_indexes]">Add a row</span
+     [end]
+    </td>
+   </tr>
+   [end]
+
+</table>
+
+<script type="text/javascript" nonce="[nonce]">
+runOnLoad(function() {
+  function showNextQueryRow(i) {
+   if (i < [max_queries]) {
+     _showID('newquery' + (i + 1));
+     _hideID('addquery' + i);
+   }
+  }
+  _fetchUserProjects(true);
+
+  var addARowLinks = document.getElementsByClassName("fakelink");
+  for (var i = 0; i < addARowLinks.length; ++i) {
+    var link = addARowLinks[[]i];
+    link.addEventListener("click", function(event) {
+        var index = Number(event.target.getAttribute("data-index"));
+        showNextQueryRow(index);
+    });
+  }
+
+  var typeToAddARow = document.getElementsByClassName("showNextQueryRow");
+  for (var i = 0; i < typeToAddARow.length; ++i) {
+    var el = typeToAddARow[[]i];
+    el.addEventListener("keydown", function(event) {
+        var index = Number(event.target.getAttribute("data-index"));
+        showNextQueryRow(index);
+    });
+  }
+
+  var acobElements = document.getElementsByClassName("acob");
+  for (var i = 0; i < acobElements.length; ++i) {
+     var el = acobElements[[]i];
+     el.addEventListener("focus", function(event) {
+         _acrob(null);
+         _acof(event);
+     });
+  }
+});
+</script>
diff --git a/templates/framework/user-bar.ezt b/templates/framework/user-bar.ezt
new file mode 100644
index 0000000..a80fe90
--- /dev/null
+++ b/templates/framework/user-bar.ezt
@@ -0,0 +1,11 @@
+<span style="padding: 0 1em">
+  [if-any logged_in_user]
+    <mr-account-dropdown
+      userDisplayName="[logged_in_user.email]"
+      loginUrl="[login_url]"
+      logoutUrl="[logout_url]"
+    ></mr-account-dropdown>
+  [else]
+    <a href="[login_url]"><u>Sign in</u></a>
+  [end]
+</span>
diff --git a/templates/framework/user-link-availability.ezt b/templates/framework/user-link-availability.ezt
new file mode 100644
index 0000000..cc02db7
--- /dev/null
+++ b/templates/framework/user-link-availability.ezt
@@ -0,0 +1,18 @@
+[# arg0: The UserView to display.
+   arg1: "Yes" to show a shortened reason as visible text on the page.
+]
+<div class="userlink_avail" title="[arg0.display_name][if-any arg0.avail_message]:
+[arg0.avail_message][end]">
+  [if-any arg0.avail_message]
+    <span class="availability_[arg0.avail_state]">&#9608;</span>
+  [end]
+  [if-any arg0.profile_url]
+    <a href="[arg0.profile_url]">[arg0.display_name]</a>[else]<a>[arg0.display_name]</a>[end]
+</div>
+[is arg1 "Yes"]
+ [if-any arg0.avail_message]
+  <div class="availability_[arg0.avail_state]" title="[arg0.display_name]:
+[arg0.avail_message]"
+  >[arg0.avail_message_short]</div>
+ [end]
+[end]
diff --git a/templates/framework/user-link.ezt b/templates/framework/user-link.ezt
new file mode 100644
index 0000000..18d7e6c
--- /dev/null
+++ b/templates/framework/user-link.ezt
@@ -0,0 +1 @@
+[if-any arg0.profile_url]<a style="white-space: nowrap" href="[arg0.profile_url]" title="[arg0.display_name]">[arg0.display_name]</a>[else][arg0.display_name][end]
\ No newline at end of file
diff --git a/templates/project/people-add-members-form.ezt b/templates/project/people-add-members-form.ezt
new file mode 100644
index 0000000..f013a0a
--- /dev/null
+++ b/templates/project/people-add-members-form.ezt
@@ -0,0 +1,103 @@
+
+[if-any offer_membership_editing]
+<br>
+<div class="h4" style="margin-bottom:4px" id="addmembers">Add Members</div>
+
+<div id="makechanges" class="closed">
+
+  <div class="ifClosed">
+   <textarea id="tempt" rows="4" style="color:#666; width:500px; margin-left:4px"
+    >Enter new member email addresses</textarea>
+  </div>
+
+
+<table class="ifOpened vt" cellspacing="2" cellpadding="2" style="margin-top:0">
+  <tr>
+   <td colspan="2">
+      <textarea name="addmembers" style="width:500px" rows="4"
+                id="addMembersTextArea">[initial_add_members]</textarea>
+     [if-any errors.addmembers]
+      <div class="fielderror">[errors.addmembers]</div>
+     [end]<br>
+   </td>
+   <td rowspan="3">
+       <div class="tip" style="margin-top:0; margin-left:4px">
+           Enter the email addresses of users that you would like to
+           add to this [is arg0 "project"]project[else]
+	   [is arg0 "hotlist"]hotlist.
+	   <strong>You can also add group lists to give every member of the group permission to edit this hotlist</strong>
+	   [else]group[end][end].<br><br>
+           Each email address must correspond to a Google Account when in use.
+       </div>
+    </td>
+   </tr>
+
+  <tr>
+    <th width="30" align="left">Role:</th>
+
+    <td width="470" align="left">
+    [is arg0 "project"]
+       <input type="radio" name="role" value="owner" id="owner">
+       <label for="owner">Owner: may make any change to this
+       project.</label><br>
+
+       <input type="radio" name="role" value="committer" id="committer"
+              checked="checked">
+       <label for="committer">Committer: may work in the project, but may
+       not reconfigure it.</label><br>
+
+       <input type="radio" name="role" value="contributor" id="contributor">
+       <label for="contributor">Contributor: starts with the same permissions
+       as non-members.</label><br>
+       [# TODO(jrobbins): custom roles]
+    [else][is arg0 "hotlist"]
+       <input type="radio" name="role" value="editor" id="editor"
+              checked="checked">
+       <label for="editor">Editor: may add/remove/rank issues.</label><br>
+       [if-any errors.incorrect_email_input]
+       <div class="fielderror">[errors.incorrect_email_input]</div>
+       [end]
+    [else]
+       <input type="radio" name="role" value="owner" id="owner">
+       <label for="owner">Owner: may make any change to this
+       group.</label><br>
+
+       <input type="radio" name="role" value="member" id="member"
+              checked="checked">
+       <label for="member">Member: member of this user group.</label><br>
+    [end][end]
+    </td>
+
+    </tr>
+    <tr>
+     <td colspan="2">
+      <input type="submit" name="addbtn" id="addbtn"
+             value="Save changes" style="margin-top:1em">
+     </td>
+    </tr>
+</table>
+
+</div>
+
+
+<script type="text/javascript" nonce="[nonce]">
+runOnLoad(function() {
+  window._openAddMembersForm = function _openAddMembersForm() {
+    document.location.hash='addmembers';
+    document.getElementById('makechanges').className = "opened";
+    window.setTimeout(
+        function () { document.getElementById('addMembersTextArea').focus(); },
+        100);
+  }
+
+  [if-any initially_expand_form]
+    _openAddMembersForm();
+  [end]
+
+  if ($("tempt"))
+    $("tempt").addEventListener("mousedown", _openAddMembersForm);
+
+});
+</script>
+
+[end]
diff --git a/templates/project/people-detail-page.ezt b/templates/project/people-detail-page.ezt
new file mode 100644
index 0000000..fda3e96
--- /dev/null
+++ b/templates/project/people-detail-page.ezt
@@ -0,0 +1,186 @@
+[define category_css]css/ph_detail.css[end]
+[include "../framework/header.ezt" "showtabs"]
+
+<a href="list">&lsaquo; Back to people list</a>
+
+<form action="detail.do" method="POST" id="peopledetail">
+<input type="hidden" name="token" value="[form_token]">
+<input type="hidden" name="u" value="[member.user.user_id]">
+<table cellspacing="8" class="rowmajor vt">
+<tr>
+ <th width="1%">User:</th>
+ <td>[include "../framework/user-link.ezt" member.user]</td>
+</tr>
+
+ <tr class="[if-any expand_perms]opened[else]closed[end]">
+ <th>Role:</th>
+ <td>
+   [# Show a widget if the current user is allowed to edit roles.]
+   [if-any perms.EditProject]
+     [define offer_role_select]Yes[end]
+   [else]
+     [define offer_role_select]No[end]
+   [end]
+   [# But, don't offer it if the user could remove themselves as the last owner.]
+   [is total_num_owners "1"][if-any warn_abandonment]
+     [define offer_role_select]No[end]
+   [end][end]
+
+   [is offer_role_select "Yes"]
+     <select name="role">
+       <option [is member.role "Owner"]selected="selected"[end]
+               value="owner">Owner</option>
+       <option [is member.role "Committer"]selected="selected"[end]
+               value="committer">Committer</option>
+       <option [is member.role "Contributor"]selected="selected"[end]
+               value="contributor">Contributor</option>
+     </select>
+   [else]
+     [member.role]
+   [end]
+   <a class="ifClosed toggleHidden" href="#" id="show_permissions"
+      style="font-size:90%; margin-left:1em">Show permissions</a>
+   <a class="ifOpened toggleHidden" href="#" id="hide_permissions"
+      style="font-size:90%; margin-left:1em">Hide permissions</a>
+   [include "people-detail-perms-part.ezt"]
+ </td>
+ <td>
+   <div class="ifOpened tip" style="width:17em">
+      <b>Permissions</b> enable members to perform specific actions in
+      a project.  Appropriate permissions are already defined for each
+      role: Owner, Committer, and Contributor.  Additional permissions can
+      be granted to individual members, if needed.
+
+      <p>Most project owners will never need to grant any individual
+      member permissions.  It is usually more important to describe
+      each member's duties in the notes.</p>
+
+      <div style="margin-top:.5em">
+        <a href="https://chromium.googlesource.com/infra/infra/+/main/appengine/monorail/doc/userguide/working-with-issues.md#Who-can-view-an-issue" target="new">Learn more</a>
+        <a href="http://code.google.com/p/monorail/wiki/Permissions" target="new"><img src="/static/images/tearoff_icon.gif" width="16" height="16"></a>
+      </div>
+   </div>
+ </td>
+</tr>
+
+
+<tr>
+ <th>Notes:</th>
+ <td>
+  [if-any offer_edit_member_notes]
+   <div style="width:40em">
+    <textarea style="width:100%" rows="4" class="ifExpand" name="notes"
+              >[member.notes]</textarea>
+   </div>
+  [else]
+   [if-any member.notes][member.notes][else]----[end]
+  [end]
+
+ </td>
+</tr>
+
+<tr>
+ <th>Autocomplete:</th>
+ <td>
+    [if-any perms.EditProject]
+      [define disable_checkbox]No[end]
+    [else]
+      [define disable_checkbox]Yes[end]
+    [end]
+    [if-any member.is_service_account]
+      [define disable_checkbox]Yes[end]
+    [end]
+     <div>
+       <input type="checkbox" name="ac_include" id="ac_include"
+            [if-any member.is_service_account][else]
+              [if-any member.ac_include]checked[end]
+            [end]
+            [is disable_checkbox "Yes"]disabled[end]
+            value="[member.user.user_id]"
+            >
+       <label for="ac_include">Include this member in autocomplete menus</label>
+     </div>
+     [if-any member.is_service_account]
+       <div>(service account is excluded by default)</div>
+     [end]
+
+     [if-any member.is_group]
+       <div>
+         <input type="checkbox" name="ac_expand" id="ac_expand"
+              [if-any member.ac_expand]checked[end]
+              [is disable_checkbox "Yes"]disabled[end]
+              value="[member.user.user_id]"
+              >
+         <label for="ac_expand">Expand this user group in autocomplete menus</label>
+       </div>
+     [else]
+       <input type="hidden" name="ac_expand" value="[member.user.user_id]">
+     [end]
+
+ </td>
+</tr>
+
+[if-any read_only]
+   <tr>
+     <th></th>
+     <td>
+       [include "../framework/read-only-rejection.ezt"]
+     </td>
+   </tr>
+[else]
+  [if-any offer_edit_perms offer_edit_member_notes]
+   <tr>
+     <th></th>
+     <td>
+      <input type="submit" name="submit" value="Save changes">
+      [if-any offer_remove_role]
+        <input type="submit" class="secondary" name="remove" value="Remove member"
+               id="remove_member">
+      [end]
+     </td>
+   </tr>
+  [end]
+[end]
+
+</table>
+</form>
+
+
+<script type="text/javascript" nonce="[nonce]">
+runOnLoad(function() {
+ function _confirmRemove() {
+  [if-any warn_abandonment]
+    [is total_num_owners "1"]
+      alert('You cannot remove the last project owner.');
+      return false;
+    [else]
+      return confirm('Remove yourself?\nYou will be locked out of making further changes.');
+    [end]
+  [else]
+    return confirm('Remove member [format "js"][member.user.email][end]?');
+  [end]
+ }
+
+ if ($("remove_member"))
+   $("remove_member").addEventListener("click", function(event) {
+      if (!_confirmRemove())
+        event.preventDefault();
+   });
+
+ [if-any read_only][else]
+   if ($("show_permissions"))
+     $("show_permissions").addEventListener("click", function() {
+        window.prpcClient.call(
+            'monorail.Users', 'SetExpandPermsPreference', {expandPerms: true});
+     });
+   if ($("hide_permissions"))
+     $("hide_permissions").addEventListener("click", function() {
+        window.prpcClient.call(
+            'monorail.Users', 'SetExpandPermsPreference', {expandPerms: false});
+     });
+ [end]
+
+});
+</script>
+
+[include "../framework/footer.ezt"]
diff --git a/templates/project/people-detail-perms-part.ezt b/templates/project/people-detail-perms-part.ezt
new file mode 100644
index 0000000..3a30417
--- /dev/null
+++ b/templates/project/people-detail-perms-part.ezt
@@ -0,0 +1,82 @@
+<table id="perm_defs" class="ifOpened">
+ [if-any offer_edit_perms displayed_extra_perms]
+  <tr><th colspan="2">Standard permissions</th></tr>
+ [end]
+
+ [include "people-detail-row-part.ezt" role_perms.View member_perms.View "View" "View issues"]
+ [include "people-detail-row-part.ezt" role_perms.Commit member_perms.Commit "Commit" "Full project member"]
+
+ [include "people-detail-row-part.ezt" role_perms.CreateIssue member_perms.CreateIssue "CreateIssue" "Enter a new issue"]
+ [include "people-detail-row-part.ezt" role_perms.AddIssueComment member_perms.AddIssueComment "AddIssueComment" "Add a comment to an issue"]
+ [include "people-detail-row-part.ezt" role_perms.EditIssue member_perms.EditIssue "EditIssue" "Edit any attribute of an issue"]
+ [include "people-detail-row-part.ezt" role_perms.EditIssueOwner member_perms.EditIssueOwner "EditIssueOwner" "- Edit the owner of an issue"]
+ [include "people-detail-row-part.ezt" role_perms.EditIssueSummary member_perms.EditIssueSummary "EditIssueSummary" "- Edit the summary of an issue"]
+ [include "people-detail-row-part.ezt" role_perms.EditIssueStatus member_perms.EditIssueStatus "EditIssueStatus" "- Edit the status of an issue"]
+ [include "people-detail-row-part.ezt" role_perms.EditIssueCc member_perms.EditIssueCc "EditIssueCc" "- Edit the CC list of an issue"]
+ [include "people-detail-row-part.ezt" role_perms.DeleteIssue member_perms.DeleteIssue "DeleteIssue" "Delete/undelete an issue"]
+
+ [include "people-detail-row-part.ezt" role_perms.DeleteAny member_perms.DeleteAny "DeleteAny" "Delete comments by anyone"]
+ [include "people-detail-row-part.ezt" role_perms.EditAnyMemberNotes member_perms.EditAnyMemberNotes "EditAnyMemberNotes" "Edit anyone's member notes"]
+ [include "people-detail-row-part.ezt" role_perms.ModerateSpam member_perms.ModerateSpam "ModerateSpam" "Mark or un-mark issues and comments as spam"]
+
+
+
+ [if-any offer_edit_perms displayed_extra_perms]
+  <tr><th colspan="2">Custom permissions</th></tr>
+ [end]
+
+ [if-any offer_edit_perms]
+  <tr>
+   <td id="displayed_extra_perms" colspan="2">
+   <div style="width:12em">
+    [for displayed_extra_perms]
+        <input style="width:100%" name="extra_perms"
+               value="[displayed_extra_perms]">
+    [end]
+     <input style="width:100%" name="extra_perms"
+            id="first_extra_perms"
+            value="" autocomplete="off">
+   </div>
+   </td>
+  </tr>
+ [else]
+   [for displayed_extra_perms]
+    <tr>
+     <td>
+      <input type="checkbox" checked="checked" disabled="disabled">
+      [displayed_extra_perms]
+     </td>
+     <td></td>
+    </tr>
+   [end]
+ [end]
+
+</table>
+
+<script type="text/javascript" nonce="[nonce]">
+runOnLoad(function() {
+  function _addInput(event) {
+    if (event.target.value == "") {
+      return;
+    }
+    var area = event.target.parentNode;
+    var newInput = document.createElement("input");
+    newInput.style.width = "100%";
+    newInput.name = event.target.name;
+    newInput.onfocus = function(e) {
+        _acrob(null);
+        _acof(e);
+    };
+    newInput.setAttribute("autocomplete", "off");
+    newInput.addEventListener("keyup", _addInput);
+    area.appendChild(newInput);
+    area.appendChild(document.createElement("br"));
+
+    // Make it only fire once.
+    event.target.removeEventListener("keyup", _addInput);
+  }
+
+  if ($("first_extra_perms"))
+    $("first_extra_perms").addEventListener("keyup", _addInput);
+});
+</script>
diff --git a/templates/project/people-detail-row-part.ezt b/templates/project/people-detail-row-part.ezt
new file mode 100644
index 0000000..785005f
--- /dev/null
+++ b/templates/project/people-detail-row-part.ezt
@@ -0,0 +1,29 @@
+[#  Display one row in the permissions table.
+
+Args:
+  arg0: True if the permission is native to the role. So, it cannot be removed.
+  arg1: True if the user has this permission. So, it will be shown when not in editing mode.
+  arg2: Permission name.
+  arg3: Permission description.
+
+References globals:
+  offer_edit_perms: True if the user can edit permissions on this page.
+]
+
+<tr>
+ <td>
+   <input type="checkbox" [if-any arg1]checked="checked"[end] id="[arg2]"
+    [if-any offer_edit_perms]
+      [if-any arg0]
+       disabled="disabled"
+      [else]
+       name="extra_perms" value="[arg2]"
+      [end]
+    [else]
+      disabled="disabled"
+    [end]
+    >
+  <label for="[arg2]">[arg2]</label>
+ </td>
+ <td>[arg3]</td>
+</tr>
diff --git a/templates/project/people-list-page.ezt b/templates/project/people-list-page.ezt
new file mode 100644
index 0000000..f3756e8
--- /dev/null
+++ b/templates/project/people-list-page.ezt
@@ -0,0 +1,197 @@
+[define title]People[end]
+[define category_css]css/ph_list.css[end]
+[if-any is_hotlist][define category2_css]css/ph_detail.css[end][end]
+[include "../framework/header.ezt" "hidetabs"]
+[include "../framework/js-placeholders.ezt"]
+
+<form method="POST" action=[if-any is_hotlist]"people.do"[else]"list.do"[end] id="membership_form">
+<input type="hidden" name="token" value="[form_token]">
+[if-any newly_added_views]
+  <br/>
+  The following new members were successfully added:
+  <br/>
+  [for newly_added_views]
+    <a href="[newly_added_views.detail_url]">[newly_added_views.user.display_name]</a> ([newly_added_views.role])
+    <br/>
+  [end]
+  <br/>
+[end]
+
+<div id="colcontrol">
+   <div class="list">
+     [if-any pagination.visible]
+       <div class="pagination">
+         [if-any pagination.prev_url]<a href="[pagination.prev_url]"><b>&lsaquo;</b> Prev</a>[end]
+         Members [pagination.start] - [pagination.last] of [pagination.total_count]
+         [if-any pagination.next_url]<a href="[pagination.next_url]">Next <b>&rsaquo;</b></a>[end]
+       </div>
+     [end]
+
+     <h4 style="display: inline">[if-any is_hotlist]Hotlist[else]Project[end] People</h4>
+
+     [if-any read_only][else]
+       [if-any offer_membership_editing]
+        <input type="button" value="Add members"
+               id="add_members_button" class="primary">
+        <input type="submit" value="Remove members"
+               id="removebtn" class="secondary" name="removebtn" disabled="disabled">
+        [# TOOD(jrobbins): extra confirmation when removing yourself as owner.]
+        [if-any is_hotlist]
+          <a id="transfer-ownership" class="buttonify">Transfer ownership</a>
+          [include "../features/transfer-hotlist-form.ezt"]
+        [end]
+       [end]
+       [if-any is_hotlist]
+        [if-any offer_remove_self]
+         <a id="remove-self" class="buttonify">Remove myself</a>
+         [include "../features/remove-self-hotlist-form.ezt"]
+        [end]
+       [end]
+     [end]
+   </div>
+
+  <table cellspacing="0" cellpadding="2" border="0" class="results striped vt" id="resultstable" width="100%">
+  <tbody>
+   [if-any pagination.visible_results]
+
+      <tr id="headingrow">
+       [if-any offer_membership_editing]
+         <th style="border-right:0; padding-right:2px">&nbsp;</th>
+       [end]
+
+       <th style="white-space:nowrap">Name</th>
+       <th style="white-space:nowrap">Role</th>
+       [if-any is_hotlist]
+       [else]
+       <th style="white-space:nowrap">Autocomplete</th>
+       <th style="white-space:nowrap">Notes</th>
+       [end]
+      </tr>
+
+      [for pagination.visible_results]
+       [if-any is_hotlist]
+         [include "people-list-row-part.ezt" "hotlist"]
+       [else]
+        [include "people-list-row-part.ezt" "project"]
+       [end]
+      [end]
+
+   [else]
+    <tr>
+     <td colspan="40" class="id">
+      <div style="padding: 3em; text-align: center">
+       This [if-any is_hotlist]hotlist[else]project[end] does not have any members.
+      </div>
+     </td>
+    </tr>
+   [end]
+
+
+  </tbody>
+  </table>
+  <div class="list-foot">
+    <div class="pagination">
+    [if-any pagination.prev_url]<a href="[pagination.prev_url]"><b>&lsaquo;</b> Prev</a>[end]
+    [pagination.start] - [pagination.last] of [pagination.total_count]
+    [if-any pagination.next_url]<a href="[pagination.next_url]">Next <b>&rsaquo;</b></a>[end]
+    </div>
+  </div>
+</div>
+
+[if-any untrusted_user_groups]
+  <div style="width:45em">
+    [include "untrusted-user-groups-part.ezt"]
+  </div>
+[end]
+
+[if-any read_only][else]
+  [if-any is_hotlist]
+  [include "people-add-members-form.ezt" "hotlist"]
+  [else]
+  [include "people-add-members-form.ezt" "project"]
+  [end]
+  [# TODO(jojwang): make more elegant later, just one line]
+[end]
+
+</form>
+
+[if-any offer_membership_editing]
+  <script type="text/javascript" nonce="[nonce]">
+runOnLoad(function() {
+    $("add_members_button").addEventListener("click", _openAddMembersForm);
+
+    function _countChecked(opt_className) {
+      var numChecked = 0;
+      var inputs = document.getElementsByTagName('input');
+      for (var i = 0; i < inputs.length; i++) {
+        var el = inputs[[]i];
+        if (el.type == 'checkbox' && el.name == 'remove' && el.checked &&
+            (!opt_className || opt_className == el.className)) {
+          numChecked++;
+        }
+      }
+      return numChecked;
+    }
+
+   function _enableRemoveButton() {
+     var removeButton = document.getElementById('removebtn');
+     if (_countChecked() > 0) {
+       removeButton.disabled = false;
+     } else {
+       removeButton.disabled = true;
+     }
+   }
+
+   setInterval(_enableRemoveButton, 700);
+
+   function _preventAbandonment(event) {
+      var meCheckbox = document.getElementById("me_checkbox");
+      if (meCheckbox && meCheckbox.checked) {
+        numOwnersChecked = _countChecked("owner");
+        if (numOwnersChecked == [total_num_owners]) {
+          alert("You cannot remove all project owners.");
+          event.preventDefault();
+        } else {
+          if (!confirm("Remove yourself as project owner?\n" +
+                       "You will be locked out of making further changes.")) {
+              event.preventDefault();
+          }
+        }
+      }
+      return true;
+   }
+   [if-any check_abandonment]
+     $("membership_form").addEventListener("submit", _preventAbandonment);
+   [end]
+
+   [if-any is_hotlist]
+   initializeDialogBox("[hotlist_id]");
+   [end]
+});
+  </script>
+[end]
+[if-any is_hotlist][if-any offer_remove_self]
+  <script type="text/javascript" nonce="[nonce]">
+  runOnLoad(function () {initializeDialogBoxRemoveSelf()});
+  </script>
+[end][end]
+
+<script type="text/javascript" nonce="[nonce]">
+runOnLoad(function() {
+  function _handleResultsClick(event) {
+    var target = event.target;
+    if (target.tagName == "A")
+      return;
+    if (target.classList.contains("rowwidgets") || target.parentNode.classList.contains("rowwidgets"))
+      return;
+    if (target.tagName != "TR") target = target.parentNode;
+    _go(target.attributes[[]"data-url"].value,
+        (event.metaKey || event.ctrlKey || event.button == 1));
+  };
+  _addClickListener($("resultstable"), _handleResultsClick);
+
+});
+</script>
+
+
+[include "../framework/footer.ezt"]
diff --git a/templates/project/people-list-row-part.ezt b/templates/project/people-list-row-part.ezt
new file mode 100644
index 0000000..f30eaf5
--- /dev/null
+++ b/templates/project/people-list-row-part.ezt
@@ -0,0 +1,62 @@
+[define detail_url][pagination.visible_results.detail_url][end]
+<tr data-url="[detail_url]">
+
+  [if-any offer_membership_editing]
+    [is arg0 "hotlist"][is pagination.visible_results.role "Owner"]
+    <td style="padding-right:2px" class="rowwidgets"></td>
+    [else]
+     <td style="padding-right:2px" class="rowwidgets">
+         <input type="checkbox" name="remove"
+                value="[pagination.visible_results.user.email]"
+                >
+     </td>
+    [end]
+    [else]
+     <td style="padding-right:2px" class="rowwidgets">
+         <input type="checkbox" name="remove"
+                [is pagination.visible_results.role "Owner"]class="owner"[end]
+                value="[pagination.visible_results.user.email]"
+                [if-any pagination.visible_results.viewing_self]
+                  id="me_checkbox"
+                [end]
+                >
+     </td>
+  [end][end]
+
+  <td style="white-space:nowrap; text-align:left;" class="id">
+     <a href="[detail_url]"
+      >[pagination.visible_results.user.display_name]</a>
+      [if-any pagination.visible_results.viewing_self]
+       <b>- me</b>
+      [end]
+  </td>
+
+  <td>
+    <a href="[detail_url]" style="white-space:nowrap">
+      [pagination.visible_results.role]<br>
+      [is arg0 "hotlist"][else]
+        [for pagination.visible_results.extra_perms]
+          <div style="font-size:90%">+ [pagination.visible_results.extra_perms]</div>
+        [end]
+      [end]
+    </a>
+  </td>
+
+  <td style="white-space:nowrap">
+    [is arg0 "hotlist"][else]
+      [if-any pagination.visible_results.is_service_account]
+        <a href="[detail_url]">Excluded</a>
+      [else][if-any pagination.visible_results.ac_include]
+        [# Nothing is displayed when the member is included.]
+      [else]
+        <a href="[detail_url]">Excluded</a>
+      [end][end]
+    [end]
+  </td>
+
+  [is arg0 "hotlist"][else]
+  <td width="90%">
+    <a href="[detail_url]">[pagination.visible_results.notes]</a>
+  </td>
+  [end]
+</tr>
diff --git a/templates/project/project-admin-advanced-page.ezt b/templates/project/project-admin-advanced-page.ezt
new file mode 100644
index 0000000..de91e40
--- /dev/null
+++ b/templates/project/project-admin-advanced-page.ezt
@@ -0,0 +1,11 @@
+[include "../framework/header.ezt" "showtabs"]
+
+ <form action="adminAdvanced.do" method="POST">
+  <input type="hidden" name="token" value="[form_token]">
+
+  [include "project-admin-publishing-part.ezt"]
+  [include "project-admin-quota-part.ezt"]
+
+ </form>
+
+[include "../framework/footer.ezt"]
diff --git a/templates/project/project-admin-page.ezt b/templates/project/project-admin-page.ezt
new file mode 100644
index 0000000..84c001f
--- /dev/null
+++ b/templates/project/project-admin-page.ezt
@@ -0,0 +1,134 @@
+[define category_css]css/ph_detail.css[end]
+[include "../framework/header.ezt" "showtabs"]
+
+[if-any read_only][include "../framework/read-only-rejection.ezt"]
+[else]
+
+ <form action="admin.do" method="POST" autocomplete="off" enctype="multipart/form-data">
+  <input type="hidden" name="token" value="[form_token]">
+
+<h4>Project metadata</h4>
+
+<div class="section">
+  [include "../framework/project-descriptive-fields.ezt"]
+</div>
+
+
+<h4>Project access</h4>
+
+<div class="section">
+  [if-any offer_access_level initial_access]
+    <br>This project may be viewed by:
+    [include "../framework/project-access-part.ezt" "dontchecksubmit"]<br>
+  [end]
+
+<div class="section">
+ <div class="closed">
+  <p>Restriction labels allow project members to restrict access to individual
+     issues.
+  <a class="ifClosed toggleHidden" href="#" style="font-size:90%; margin-left:.5em">Learn more</a></p>
+  <div class="ifOpened help">
+      Normally, if a project member may edit the labels, then they may also
+      edit restriction labels.  That allows project committers to adjust access
+      controls for the items that they are working on.  However, some project
+      owners may prefer that once a restriction label is in place, only a project
+      owner may remove it.
+  </div>
+ </div>
+ <input type="checkbox" name="only_owners_remove_restrictions"
+        id="only_owners_remove_restrictions"
+        [if-any only_owners_remove_restrictions]checked="checked"[end] >
+ <label for="only_owners_remove_restrictions">Only project owners
+  may remove <tt>Restrict-*</tt> labels</label>
+</div>
+
+<div class="section">
+ <div class="closed">
+  <p>Collaboration style
+  <a class="ifClosed toggleHidden" href="#" style="font-size:90%; margin-left:.5em">Learn more</a></p>
+  <div class="ifOpened help">
+      Project workspaces are usually intended to promote collaboration among
+      all project members.  However, sometimes a compartmentalized collaboration
+      style is more appropriate.  For example, one company might want to work
+      with several partners, but not let each partner know about the others.
+      Note: In such a project, all artifacts should have restriction labels.
+  </div>
+ </div>
+ <input type="checkbox" name="only_owners_see_contributors" id="only_owners_see_contributors"
+        [if-any only_owners_see_contributors]checked="checked"[end] >
+ <label for="only_owners_see_contributors">Only project owners may see the list of contributors.</label>
+</div>
+
+</div>
+
+
+<h4>Activity notifications</h4>
+
+<div class="section">
+  <p>Email notifications of issue tracker activity will automatically be sent to
+     the following email address.</p>
+
+   <table cellpadding="2">
+     <tr><th>All issue changes:</th>
+      <td><input type="email" name="issue_notify" size="35" value="[issue_notify]"><br>
+       [if-any errors.issue_notify]
+       <div class="fielderror">[errors.issue_notify]</div>
+       [end]
+      </td>
+     </tr>
+   </table>
+  [# TODO: validate as address is entered ]
+
+  [include "../framework/admin-email-sender-part.ezt"]
+
+ <div class="closed">
+  <p>Notification contents
+  <a class="ifClosed toggleHidden" href="#" style="font-size:90%; margin-left:.5em">Learn more</a></p>
+  <div class="ifOpened help">
+      By default, notifications content will be limited based on user preference,
+      Restrict-* labels, and their membership in a given project. This option
+      forces the full notification content to be sent regardless of other factors.
+  </div>
+ </div>
+ <input type="checkbox" name="issue_notify_always_detailed" id="issue_notify_always_detailed"
+        [if-any issue_notify_always_detailed]checked="checked"[end] >
+ <label for="issue_notify_always_detailed">Always send detailed notification content.</label>
+</div>
+
+
+<h4>Email reply processing</h4>
+
+<div class="section">
+ <div class="closed">
+  <p>Users may add comments and make updates by replying to
+   certain notification emails.
+  <a class="ifClosed toggleHidden" style="font-size:90%; margin-left:.5em">Learn more</a></p>
+  <div class="ifOpened help">
+      Users may add comments to an issue
+      by replying to a notification email:
+
+      <ul>
+       <li>Look for a note in the footer of the email indicating that
+           a reply will be processed by the server.</li>
+       <li>Comments must be in replies to notification emails sent directly
+           to the member, not through a mailing list.</li>
+       <li>The reply must be <tt>From:</tt> the same email address to which
+           the notification was sent.</li>
+       <li>Project members who have permission to edit issues may make
+           changes via email replies.</li>
+      </ul>
+  </div>
+ </div>
+ <input type="checkbox" name="process_inbound_email" id="process_inbound_email"
+        [if-any process_inbound_email]checked="checked"[end] >
+ <label for="process_inbound_email">Process email replies</label>
+</div>
+
+<br>
+
+  <input type="submit" id="savechanges" name="btn" value="Save changes" class="submit">
+ </form>
+
+[end]
+
+[include "../framework/footer.ezt"]
diff --git a/templates/project/project-admin-publishing-part.ezt b/templates/project/project-admin-publishing-part.ezt
new file mode 100644
index 0000000..bef52df
--- /dev/null
+++ b/templates/project/project-admin-publishing-part.ezt
@@ -0,0 +1,110 @@
+[# This is the "Project publishing options" on the "Advanced" subtab. ]
+
+<h4>Project state</h4>
+
+<div class="section">
+<table class="vt" cellspacing="20" style="width:60em">
+ [if-any offer_archive]
+ <tr>
+  <td>
+    <input type="submit" name="archivebtn" style="width:6em"
+           value="Archive">
+  </td>
+  <td>
+    Archive this project. It will only be visible read-only to
+    project members.  Once it is archived, you may unarchive it, or go ahead
+    and fully delete it.
+    <br><br>
+  </td>
+ </tr>
+ [end]
+
+ [if-any offer_delete]
+ <tr>
+  <td>
+    <input type="submit" name="deletebtn" style="width:6em"
+           value="Delete" id="delbtn">
+  </td>
+  <td>
+    Completely delete this project now.
+    <br><br>
+  </td>
+ </tr>
+ [end]
+
+ [if-any offer_publish]
+ <tr>
+  <td>
+    <input type="submit" name="publishbtn" style="width:6em"
+           value="Unarchive">
+  </td>
+  <td>
+    Make this project active again.
+    All project contents will become visible and editable to users as normal.
+    <br><br>
+  </td>
+ </tr>
+ [end]
+
+ [if-any offer_move]
+ <tr>
+  <td>
+    <input type="submit" name="movedbtn" style="width:6em"
+           value="Move">
+  </td>
+  <td>
+    If you have moved your project to a different location, enter it here and
+    users will be directed to that location.  If the destination is another
+    project on this site, enter just the new project name.  If the destination
+    is another site, enter the new project home page URL.
+    <br><br>
+    <b>Location:</b>
+    <input type="text" name="moved_to" size="50" value="[moved_to]">
+  </td>
+ </tr>
+ [end]
+
+ [if-any offer_doom]
+ <tr>
+  <td>
+    <input type="submit" name="doombtn" style="width:6em"
+           value="Doom">
+  </td>
+  <td>
+    Immediately archive this project and schedule it for deletion in
+    90 days.  Only a site admin can un-archive the project, not a
+    project owner.  In the meantime, the project will be read-only for
+    project members only, and the reason for deletion will be displayed at the top
+    of each page.
+    <br><br>
+    <b>Reason:</b>
+    <input type="text" name="reason" size="50" value="[default_doom_reason]">
+  </td>
+ </tr>
+ [end]
+
+ [if-any offer_archive offer_delete offer_publish offer_doom offer_move][else]
+ <tr>
+  <td>
+  </td>
+  <td>
+    You are not authorized to change the project state.
+  </td>
+ </tr>
+ [end]
+
+</table>
+
+</div>
+
+<script type="text/javascript" nonce="[nonce]">
+runOnLoad(function() {
+  if ($("delbtn")) {
+    $("delbtn").addEventListener("click", function(event) {
+        var msg = "Really delete the whole project?\nThis operation cannot be undone.";
+        if (!confirm(msg))
+          event.preventDefault();
+    });
+  }
+});
+</script>
diff --git a/templates/project/project-admin-quota-part.ezt b/templates/project/project-admin-quota-part.ezt
new file mode 100644
index 0000000..1649eab
--- /dev/null
+++ b/templates/project/project-admin-quota-part.ezt
@@ -0,0 +1,30 @@
+<h4>Storage quota</h4>
+
+<div class="section">
+
+  <table cellspacing="6" style="padding:6px">
+    <tr>
+      <td>Issue attachments:</td>
+      <td>[include "quota-bar.ezt" attachment_quota]</td>
+    </tr>
+    <tr>
+      <td style="padding:15px 0">
+        [if-any offer_quota_editing]
+          <input type="submit" name="savechanges" value="Update Quota">
+        [end]
+      </td>
+      <td style="padding:15px 0">
+        [if-any offer_quota_editing]
+          <input type="number" name="[attachment_quota.field_name]" value="[attachment_quota.quota_mb]"
+                 size="5" min="1" style="font-size:90%; padding:0">
+          [if-any errors.attachment_quota]
+            <div class="fielderror">[errors.attachment_quota]</div>
+          [end]
+        [else]
+          [attachment_quota.quota_mb]
+        [end]
+        MB
+      </td>
+    </tr>
+  </table>
+</div>
diff --git a/templates/project/project-export-page.ezt b/templates/project/project-export-page.ezt
new file mode 100644
index 0000000..487b051
--- /dev/null
+++ b/templates/project/project-export-page.ezt
@@ -0,0 +1,23 @@
+[define category_css]css/ph_detail.css[end]
+[include "../framework/header.ezt" "showtabs"]
+
+<h3>Project export</h3>
+
+<form action="projectExport/json" method="GET">
+  [# We use xhr_token here because we are doing a GET on a JSON servlet.]
+  <input type="hidden" name="token" value="[xhr_token]">
+  <table cellpadding="3" class="rowmajor vt">
+    <tr>
+     <th>Format</th>
+     <td style="width:90%">JSON</td>
+   </tr>
+   <tr>
+     <th></th>
+     <td><input type="submit" name="btn" value="Submit"></td>
+   </tr>
+ </table>
+</form>
+
+
+
+[include "../framework/footer.ezt"]
diff --git a/templates/project/project-summary-page.ezt b/templates/project/project-summary-page.ezt
new file mode 100644
index 0000000..4c45bd3
--- /dev/null
+++ b/templates/project/project-summary-page.ezt
@@ -0,0 +1,102 @@
+[define category_css]css/ph_detail.css[end]
+[include "../framework/header.ezt" "showtabs"]
+[# TODO: add UI element permissions when I add editing elements to this page. ]
+[define show_star][if-any project_stars_enabled][if-any logged_in_user][if-any read_only][else]yes[end][end][end][end]
+
+<h4>Project: [projectname]</h4>
+<div class="section">
+  <div><i>[project_summary]</i></div>
+
+  [if-any show_star]
+  <div>
+   <a class="star" id="star"
+    style="color:[if-any is_project_starred]cornflowerblue[else]gray[end];"
+    title="[if-any is_project_starred]Un-s[else]S[end]tar this project">
+   [if-any is_project_starred]&#9733;[else]&#9734;[end]
+   </a>
+   Starred by [num_stars] user[plural]
+   </div>
+  [end]
+</div>
+
+
+<h4>Project description</h4>
+<div class="section">
+  [format "raw"][formatted_project_description][end]
+</div>
+
+<h4>Project access</h4>
+<div class="section">
+  [access_level.name]
+</div>
+
+
+[if-any home_page]
+  <h4>Project home page</h4>
+  <div class="section">
+    <a href="[home_page]">[home_page]</a>
+  </div>
+[end]
+
+[if-any docs_url]
+  <h4>Project documentation</h4>
+  <div class="section">
+    <a href="[docs_url]">[docs_url]</a>
+  </div>
+[end]
+
+[if-any source_url]
+  <h4>Project source browser</h4>
+  <div class="section">
+    <a href="[source_url]">[source_url]</a>
+  </div>
+[end]
+
+<!--  TODO(jrobbins): expose this later when it is more fully baked.
+
+<h4>Issue tracking process</h4>
+<div class="section">
+  Brief paragraph about how you intend this issue tracker to be used.
+
+</div>
+
+
+<h4>Ground rules</h4>
+  <ul>
+    <li>Non-members may enter new issues, but they will be moderated...</li>
+    <li>Please keep to the facts of the issue, don't try to advocate.</li>
+    <li>We are not currently looking for feature requests from non-members.</li>
+  </ul>
+
+
+
+<h4>Guidelines</h4>
+  <ul>
+    <li>Make sure the defect is verified with the latest build</li>
+    <li>Another bullet item describing how to collaborate in this project</li>
+    <li>A few more</li>
+    <li>And going into a little detail</li>
+    <li>But not too much... also need good defaults and examples</li>
+  </ul>
+
+
+<h4>For more information</h4>
+  <ul>
+    <li>Link to external docs</li>
+    <li>And discussion forums</li>
+  </ul>
+
+-->
+
+<script type="text/javascript" nonce="[nonce]">
+runOnLoad(function() {
+  if ($("star")) {
+    [# The user viewing this page wants to star the project *on* this page]
+    $("star").addEventListener("click", function () {
+       _TKR_toggleStar($("star"), "[projectname]");
+    });
+  }
+});
+</script>
+
+[include "../framework/footer.ezt"]
diff --git a/templates/project/project-updates-page.ezt b/templates/project/project-updates-page.ezt
new file mode 100644
index 0000000..c3ee3b4
--- /dev/null
+++ b/templates/project/project-updates-page.ezt
@@ -0,0 +1,7 @@
+[define page_css]css/d_updates_page.css[end]
+
+[include "../framework/header.ezt" "hidetabs"]
+
+[include "../features/updates-page.ezt"]
+
+[include "../framework/footer.ezt"]
diff --git a/templates/project/quota-bar.ezt b/templates/project/quota-bar.ezt
new file mode 100644
index 0000000..e89a6e2
--- /dev/null
+++ b/templates/project/quota-bar.ezt
@@ -0,0 +1,22 @@
+[# Display a little HTML bar and labels. This is not really a bar chart.
+   For comparison, see the bars in the top half of
+   https://www.google.com/accounts/ManageStorage
+
+arg0: an EZTItem with quota info for one component.
+]
+
+<table border="0" cellpadding="0" cellspacing="0">
+  <tr>
+    <td style="width:200px">
+      <table border="0" cellpadding="0" cellspacing="0" style="width:100%; border:1px solid #345BA6">
+        <tr>
+          <td style="background:#345BA6; width:[arg0.used_percent]%"> &nbsp; </td>
+          <td style="background:#EBF0FA; width:[arg0.avail_percent]%"> &nbsp; </td>
+        </tr>
+      </table>
+    </td>
+    <td style="padding-left:.7em">
+      [arg0.used] ([arg0.used_percent]%) in use
+    </td>
+  </tr>
+</table>
diff --git a/templates/project/rules-deleted-notification-email.ezt b/templates/project/rules-deleted-notification-email.ezt
new file mode 100644
index 0000000..2155004
--- /dev/null
+++ b/templates/project/rules-deleted-notification-email.ezt
@@ -0,0 +1,6 @@
+The following Filter rules were deleted for the [project_name] project.
+
+[for rules] <div>[rules]</div> [end]
+
+If these rules need to be added back please re-create them <a href="[rules_url]">here</a>
+<br>
diff --git a/templates/project/untrusted-user-groups-part.ezt b/templates/project/untrusted-user-groups-part.ezt
new file mode 100644
index 0000000..474cfe7
--- /dev/null
+++ b/templates/project/untrusted-user-groups-part.ezt
@@ -0,0 +1,14 @@
+[if-any perms.EditProject]
+  <div class="help" style="background: #ddf8cc;">
+       <b>Important:</b> Users could be given indirect
+       roles in this project without your knowledge.
+       The following user groups either have group managers
+       who are not project owners in this project, or they allow anyone to
+       join the group:
+       <ul style="list-style-type: none">
+         [for untrusted_user_groups]
+         <li>[untrusted_user_groups.email]</li> [# TODO(jrobbins): hyperlink]
+         [end]
+       </ul>
+  </div>
+[end]
diff --git a/templates/sitewide/403-page.ezt b/templates/sitewide/403-page.ezt
new file mode 100644
index 0000000..8d05784
--- /dev/null
+++ b/templates/sitewide/403-page.ezt
@@ -0,0 +1,12 @@
+[define category_css]css/ph_detail.css[end]
+[include "../framework/header.ezt" "hidetabs"]
+
+<h3>Permission denied</h3>
+
+<h4>What happened?</h4>
+
+<p>You do not have permission to view the requested page.</p>
+
+[if-any reason]<p>Reason: [reason].</p>[end]
+
+[include "../framework/footer.ezt"]
diff --git a/templates/sitewide/group-admin-page.ezt b/templates/sitewide/group-admin-page.ezt
new file mode 100644
index 0000000..ccef3ed
--- /dev/null
+++ b/templates/sitewide/group-admin-page.ezt
@@ -0,0 +1,18 @@
+[define title]User Group: [groupname][end]
+[define category_css]css/ph_list.css[end]
+[include "../framework/header.ezt" "showusergrouptabs"]
+
+
+<form action="groupadmin.do" method="POST" autocomplete="off">
+ <input type="hidden" name="token" value="[form_token]">
+
+	<h4>Group membership visibility</h4>
+
+	The group members may be viewed by:
+	[include "../framework/group-setting-fields.ezt"]
+	<br>
+
+  <input type="submit" id="savechanges" name="btn" value="Save changes" class="submit">
+</form>
+
+[include "../framework/footer.ezt"]
diff --git a/templates/sitewide/group-create-page.ezt b/templates/sitewide/group-create-page.ezt
new file mode 100644
index 0000000..707f380
--- /dev/null
+++ b/templates/sitewide/group-create-page.ezt
@@ -0,0 +1,40 @@
+[define category_css]css/ph_detail.css[end]
+[include "../framework/header.ezt" "hidetabs"]
+
+<h2>Create a new user group</h2>
+
+[if-any read_only][include "../framework/read-only-rejection.ezt"]
+[else]
+
+<form action="createGroup.do" method="POST" id="create_group_form"
+      style="margin:1em">
+  <input type="hidden" name="token" value="[form_token]">
+
+  Group email address:<br>
+  <input size="30" type="text" id="groupname" name="groupname" value="[initial_name]">
+  <span class="graytext">Example: group-name@example.com</span>
+  <div class="fielderror">
+    <span id="groupnamefeedback"></span>
+    [if-any errors.groupname][errors.groupname][end]
+  </div>
+  <br>
+
+  Members viewable by:
+  [include "../framework/group-setting-fields.ezt"]
+  <br>
+
+  <input type="submit" id="submit_btn" name="btn" value="Create group">
+</form>
+
+<script type="text/javascript" nonce="[nonce]">
+runOnLoad(function() {
+  $("create_group_form").addEventListener("submit", function() {
+    $("submit_btn").value = "Creating group...";
+    $("submit_btn").disabled = "disabled";
+  });
+});
+</script>
+
+[end][# not read-only]
+
+[include "../framework/footer.ezt"]
diff --git a/templates/sitewide/group-detail-page.ezt b/templates/sitewide/group-detail-page.ezt
new file mode 100644
index 0000000..4b19aca
--- /dev/null
+++ b/templates/sitewide/group-detail-page.ezt
@@ -0,0 +1,105 @@
+[define title]User Group: [groupname][end]
+[define category_css]css/ph_list.css[end]
+[include "../framework/header.ezt" "showusergrouptabs"]
+[include "../framework/js-placeholders.ezt"]
+
+<form method="POST" action="edit.do">
+<input type="hidden" name="token" value="[form_token]">
+<div id="colcontrol">
+   <div class="list">
+     [if-any pagination.visible]
+       <div class="pagination">
+          [if-any pagination.prev_url]<a href="[pagination.prev_url]"><b>&lsaquo;</b> Prev</a>[end]
+          Members [pagination.start] - [pagination.last] of [pagination.total_count]
+          [if-any pagination.next_url]<a href="[pagination.next_url]">Next <b>&rsaquo;</b></a>[end]
+       </div>
+     [end]
+     <b>User Group: [groupname]</b>
+     [if-any offer_membership_editing]
+     <input type="button" value="Add members" style="font-size:80%; margin-left:1em"
+            id="add_members_button">
+     <input type="submit" value="Remove members" style="font-size:80%; margin-left:1em"
+            id="removebtn" name="removebtn" disabled="disabled">
+     [# TODO(jrobbins): extra confirmation when removing yourself as group owner.]
+     [end]
+   </div>
+
+  <p>Group type: [group_type]</p>
+
+  <table cellspacing="0" cellpadding="2" border="0" class="results striped vt" id="resultstable" width="100%">
+  <tbody>
+   <tr id="headingrow">
+     [if-any offer_membership_editing]
+       <th style="border-right:0; padding-right:2px">&nbsp;</th>
+     [end]
+     <th style="white-space:nowrap">Member</th>
+     <th style="white-space:nowrap">Role</th>
+   </tr>
+
+   [if-any pagination.visible_results]
+      [for pagination.visible_results]
+        <tr>
+          [if-any offer_membership_editing]
+            <td style="padding-right:2px">
+              <input type="checkbox" name="remove"
+                     value="[pagination.visible_results.email]">
+          </td>
+         [end]
+          <td class="id" style="text-align:left">
+            [include "../framework/user-link.ezt" pagination.visible_results]
+          </td>
+          <td style="text-align:left" width="90%">
+            <a href="[pagination.visible_results.profile_url]">[pagination.visible_results.role]</a>
+          </td>
+        </tr>
+      [end]
+   [else]
+       <tr><td colspan="40">
+            This user group has no members.
+        </td></tr>
+   [end]
+
+
+  </tbody>
+  </table>
+</div>
+
+[include "../project/people-add-members-form.ezt" "group"]
+
+</form>
+
+
+[if-any offer_membership_editing]
+  <script type="text/javascript" nonce="[nonce]">
+runOnLoad(function() {
+    function _countChecked(opt_className) {
+      var numChecked = 0;
+      var inputs = document.getElementsByTagName('input');
+      for (var i = 0; i < inputs.length; i++) {
+        var el = inputs[[]i];
+        if (el.type == 'checkbox' && el.name == 'remove' && el.checked &&
+            (!opt_className || opt_className == el.className)) {
+          numChecked++;
+        }
+      }
+      return numChecked;
+    }
+
+   function _enableRemoveButton() {
+     var removeButton = document.getElementById('removebtn');
+     if (_countChecked() > 0) {
+       removeButton.disabled = false;
+     } else {
+       removeButton.disabled = true;
+     }
+   }
+
+   setInterval(_enableRemoveButton, 700);
+
+   $("add_members_button").addEventListener("click", _openAddMembersForm);
+});
+  </script>
+[end]
+
+
+[include "../framework/footer.ezt"]
diff --git a/templates/sitewide/group-list-page.ezt b/templates/sitewide/group-list-page.ezt
new file mode 100644
index 0000000..14bdf57
--- /dev/null
+++ b/templates/sitewide/group-list-page.ezt
@@ -0,0 +1,95 @@
+[define title]User Groups[end]
+[define category_css]css/ph_list.css[end]
+[include "../framework/header.ezt" "hidetabs"]
+[include "../framework/js-placeholders.ezt"]
+
+<form method="POST" action='/hosting/deleteGroup.do'>
+<input type="hidden" name="token" value="[form_token]">
+<div id="colcontrol">
+   <div class="list">
+     <b>User Groups</b>
+     [if-any offer_group_deletion]
+     <input type="submit" value="Delete Groups" style="margin-left:1em"
+            id="removebtn" name="removebtn" disabled="disabled">
+     [end]
+     [if-any offer_group_creation]
+     <a href="/hosting/createGroup" class="buttonify" style="margin-left:1em">Create Group</a>
+     [end]
+   </div>
+
+  <table cellspacing="0" cellpadding="2" border="0" class="results striped" id="resultstable" width="100%">
+  <tbody>
+   [if-any groups]
+
+      <tr id="headingrow">
+        [if-any offer_group_deletion]
+          <th style="border-right:0; padding-right:2px" width="2%">&nbsp;</th>
+        [end]
+        <th style="white-space:nowrap">Name</th>
+        <th style="white-space:nowrap">Size</th>
+        <th style="white-space:nowrap">Member list visibility</th>
+      </tr>
+
+      [for groups]
+        <tr>
+          [if-any offer_group_deletion]
+            <td style="padding-right:2px" width="2%">
+              <input type="checkbox" name="remove"
+                     value="[groups.group_id]">
+            </td>
+          [end]
+          <td class="id" style="text-align:left"><a href="[groups.detail_url]">[groups.name]</a></td>
+          <td><a href="[groups.detail_url]">[groups.num_members]</a></td>
+          <td><a href="[groups.detail_url]">[groups.who_can_view_members]</a></td>
+        </tr>
+      [end]
+
+   [else]
+    <tr>
+     <td colspan="40" class="id">
+      <div style="padding: 3em; text-align: center">
+       No user groups have been defined.
+      </div>
+     </td>
+    </tr>
+   [end]
+
+
+  </tbody>
+  </table>
+</div>
+
+</form>
+
+[if-any offer_group_deletion]
+  <script type="text/javascript" nonce="[nonce]">
+runOnLoad(function() {
+    function _countChecked(opt_className) {
+      var numChecked = 0;
+      var inputs = document.getElementsByTagName('input');
+      for (var i = 0; i < inputs.length; i++) {
+        var el = inputs[[]i];
+        if (el.type == 'checkbox' && el.name == 'remove' && el.checked &&
+            (!opt_className || opt_className == el.className)) {
+          numChecked++;
+        }
+      }
+      return numChecked;
+    }
+
+   function _enableRemoveButton() {
+     var removeButton = document.getElementById('removebtn');
+     if (_countChecked() > 0) {
+       removeButton.disabled = false;
+     } else {
+       removeButton.disabled = true;
+     }
+   }
+
+   setInterval(_enableRemoveButton, 700);
+
+});
+  </script>
+[end]
+
+[include "../framework/footer.ezt"]
diff --git a/templates/sitewide/hosting-home-page.ezt b/templates/sitewide/hosting-home-page.ezt
new file mode 100644
index 0000000..6f066fb
--- /dev/null
+++ b/templates/sitewide/hosting-home-page.ezt
@@ -0,0 +1,85 @@
+[define show_search_metadata]True[end]
+[define robots_no_index]true[end]
+[define category_css]css/ph_list.css[end]
+
+[include "../framework/header.ezt" "hidesearch"]
+
+[define prod_hosting_base_url]/hosting/[end]
+
+[if-any read_only][else]
+  [if-any can_create_project learn_more_link]
+    <div style="margin-top:3em; text-align:center;">
+      <div style="text-align:center;margin:1em">
+        [if-any can_create_project]
+          <a href="/hosting/createProject">Create a new project</a>
+        [end]
+
+        [if-any learn_more_link]
+          <a href="[learn_more_link]">Learn more about [site_name]</a>
+        [end]
+      </div>
+    </div>
+  [end]
+[end]
+
+<a href="/projects" style="display: block; padding: 0.5em 8px; width: 50%;
+  text-align: center; margin: auto; border: var(--chops-normal-border);
+  border-radius: 8px;">
+Preview a new project list for Monorail.
+</a>
+
+<div id="controls">
+  [include "../sitewide/project-list-controls.ezt" arg1]
+</div>
+
+<div id="project_list">
+  [if-any projects]
+    <table id="resultstable" class="resultstable results" width="100%" border="0" cellspacing="0" cellpadding="18">
+      <tr>
+        [if-any logged_in_user]<th></th>[end]
+        <th style="text-align:left">Name</th>
+        [if-any logged_in_user]<th style="text-align:left; white-space:nowrap">Your role</th>[end]
+        <th style="text-align:left">Stars</th>
+        <th style="text-align:left">Updated</th>
+        <th style="text-align:left">Summary</th>
+      </tr>
+      [for projects]
+        <tr data-url="[projects.relative_home_url]">
+          [include "project-list-row.ezt"]
+        </tr>
+      [end]
+    </table>
+  [else]
+   <p style="text-align:center;padding:0; margin:2em">
+     There were no visible projects found.
+   </p>
+  [end]
+</div>
+
+
+<script type="text/javascript" nonce="[nonce]">
+runOnLoad(function() {
+   var stars = document.getElementsByClassName("star");
+   for (var i = 0; i < stars.length; ++i) {
+     var star = stars[[]i];
+     star.addEventListener("click", function (event) {
+         var projectName = event.target.getAttribute("data-project-name");
+         _TKR_toggleStar(event.target, projectName);
+     });
+   }
+
+  function _handleResultsClick(event) {
+    var target = event.target;
+    if (target.tagName == "A" || target.type == "checkbox" || target.className == "cb")
+      return;
+    while (target && target.tagName != "TR") target = target.parentNode;
+    _go(target.attributes[[]"data-url"].value,
+        (event.metaKey || event.ctrlKey || event.button == 1));
+  };
+  _addClickListener($("resultstable"), _handleResultsClick);
+
+
+});
+</script>
+
+[include "../framework/footer.ezt"]
diff --git a/templates/sitewide/moved-page.ezt b/templates/sitewide/moved-page.ezt
new file mode 100644
index 0000000..391f957
--- /dev/null
+++ b/templates/sitewide/moved-page.ezt
@@ -0,0 +1,24 @@
+[include "../framework/header.ezt" "hidetabs"]
+
+<h3>Project has moved</h3>
+
+<h4>What happened?</h4>
+
+<p>Project "[project_name]" has moved to another location on the Internet.</p>
+
+<div style="margin:2em" class="help">
+  <b style="margin:0.5em">Your options:</b>
+
+  <ul>
+   [if-any moved_to_url]
+    <li>View the project at:
+     <a href="[moved_to_url]">[moved_to_url]</a></li>
+   [end]
+   <li><a href="http://www.google.com/search?q=[project_name]">Search the web</a>
+       for pages about "[project_name]".
+   </li>
+
+  </ul>
+</div>
+
+[include "../framework/footer.ezt"]
diff --git a/templates/sitewide/project-404-page.ezt b/templates/sitewide/project-404-page.ezt
new file mode 100644
index 0000000..9bc2878
--- /dev/null
+++ b/templates/sitewide/project-404-page.ezt
@@ -0,0 +1,6 @@
+[define category_css]css/ph_detail.css[end]
+[include "../framework/header.ezt" "showtabs"]
+
+<center style="margin-top: 4em;">The page you asked for does not exist.</center>
+
+[include "../framework/footer.ezt"]
diff --git a/templates/sitewide/project-create-page.ezt b/templates/sitewide/project-create-page.ezt
new file mode 100644
index 0000000..9bcb447
--- /dev/null
+++ b/templates/sitewide/project-create-page.ezt
@@ -0,0 +1,117 @@
+[define category_css]css/ph_detail.css[end]
+[include "../framework/header.ezt" "hidetabs"]
+
+<h2>Create a new project</h2>
+
+[if-any read_only][include "../framework/read-only-rejection.ezt"]
+[else]
+
+<form action="createProject.do" method="POST" id="create_project_form"
+      style="margin:1em" enctype="multipart/form-data">
+  <input type="hidden" name="token" value="[form_token]">
+
+
+  Project name:<br>
+  <input size="30" type="text" id="projectname" name="projectname" autocomplete="off"
+         value="[initial_name]">
+  <span class="graytext">Example: my-project-name</span>
+  <div class="fielderror">&nbsp;
+    <span id="projectnamefeedback">
+       [if-any errors.projectname][errors.projectname][end]
+    </span>
+  </div>
+
+  [include "../framework/project-descriptive-fields.ezt"]
+  <br>
+
+  Viewable by:
+  [include "../framework/project-access-part.ezt" "checksubmit"]
+  <br>
+
+  <input type="submit" id="submit_btn" name="btn" value="Create project">
+</form>
+
+
+<script type="text/javascript" nonce="[nonce]">
+runOnLoad(function() {
+  [# TODO(jrobbins): move this to compiled Javascript. ]
+  var submit = document.getElementById('submit_btn');
+  submit.disabled = 'disabled';
+  var projectname = document.getElementById('projectname');
+  var access = document.getElementById('access');
+  var summary = document.getElementById('summary');
+  var description = document.getElementById('description');
+  var cg = document.getElementById('cg');
+  var oldName = '';
+  projectname.focus();
+  var solelyDigits = /^[[]-0-9]+$/
+  var hasUppercase = /[[]A-Z]/
+  var projectRE = /^[[]a-z0-9][[]-a-z0-9]*$/
+
+  function checkprojectname() {
+    name = projectname.value;
+    if (name != oldName) {
+      oldName = name;
+      feedback = document.getElementById('projectnamefeedback');
+      submit.disabled='disabled';
+      if (name == '') {
+        feedback.textContent = '';
+      } else if (hasUppercase.test(name)) {
+        feedback.textContent = 'Must be all lowercase';
+      } else if (solelyDigits.test(name)) {
+        feedback.textContent = 'Must include a lowercase letter';
+      } else if (!projectRE.test(name)) {
+        feedback.textContent = 'Invalid project name';
+      } else if (name.length > [max_project_name_length]) {
+        feedback.textContent = 'Project name is too long';
+      } else if(name[[]name.length - 1] == '-') {
+        feedback.textContent = "Project name cannot end with a '-'";
+      } else {
+        feedback.textContent = '';
+        checkname();
+        checksubmit();
+      }
+    }
+  }
+
+  var checkname = debounce(function() {
+    _CP_checkProjectName(projectname.value);
+  });
+
+  function checkempty(elemId) {
+    var elem = document.getElementById(elemId);
+    feedback = document.getElementById(elemId + 'feedback');
+    if (elem.value.length == 0) {
+      feedback.textContent = 'Please enter a ' + elemId;
+    } else {
+      feedback.textContent = ' ';
+    }
+    checksubmit();
+  }
+
+  function checksubmit() {
+  feedback = document.getElementById('projectnamefeedback');
+   submit.disabled='disabled';
+   if (projectname.value.length > 0 &&
+       summary.value.length > 0 &&
+       description.value.length > 0 &&
+       (cg == undefined || cg.value.length > 1) &&
+       feedback.textContent == '') {
+     submit.disabled='';
+   }
+  }
+  setInterval(checkprojectname, 700); [# catch changes that were not keystrokes.]
+  $("projectname").addEventListener("keyup", checkprojectname);
+  $("summary").addEventListener("keyup", function() { checkempty("summary"); });
+  $("description").addEventListener("keyup", function() { checkempty("description"); });
+  $("create_project_form").addEventListener("submit", function () {
+      $("submit_btn").value = "Creating project...";
+      $("submit_btn").disabled = "disabled";
+  });
+
+});
+</script>
+
+[end][# not read-only]
+
+[include "../framework/footer.ezt"]
diff --git a/templates/sitewide/project-list-controls.ezt b/templates/sitewide/project-list-controls.ezt
new file mode 100644
index 0000000..7a88cee
--- /dev/null
+++ b/templates/sitewide/project-list-controls.ezt
@@ -0,0 +1,10 @@
+<div class="list">
+  <h4 style="display:inline">List of Projects</h4>
+  [if-any projects]
+    <div class="pagination">
+        [if-any pagination.prev_url]<a href="[pagination.prev_url]"><b>&lsaquo;</b> Prev</a>[end]
+        [pagination.start] - [pagination.last] of [pagination.total_count]
+        [if-any pagination.next_url]<a href="[pagination.next_url]">Next <b>&rsaquo;</b></a>[end]
+    </div>
+  [end]
+</div>
\ No newline at end of file
diff --git a/templates/sitewide/project-list-row.ezt b/templates/sitewide/project-list-row.ezt
new file mode 100644
index 0000000..a1ce4ae
--- /dev/null
+++ b/templates/sitewide/project-list-row.ezt
@@ -0,0 +1,54 @@
+[# This displays one list row of the project search results.
+
+No parameters are used, but it expects the "projects" loop variable to
+hold the current project.]
+
+[if-any logged_in_user]
+  [# Display star for logged in user to star this project]
+  <td>
+    [if-any logged_in_user]
+      <a class="star"
+       style="color:[if-any projects.starred]cornflowerblue[else]gray[end]"
+       title="[if-any projects.starred]Un-s[else]S[end]tar this project" data-project-name="[projects.project_name]">
+      [if-any projects.starred]&#9733;[else]&#9734;[end]
+      </a>
+    [end]
+  </td>
+[end]
+
+[# Project name link to this project]
+<td style="white-space:nowrap" class="id">
+  <a href="[projects.relative_home_url]/" style="font-size:medium">
+    [projects.project_name]
+  </a>
+</td>
+
+[# Display membership and star only if user is logged in]
+[if-any logged_in_user]
+  [# User's membership status of this project]
+  <td>
+    [if-any projects.membership_desc][projects.membership_desc][end]
+  </td>
+[end]
+
+[# Display how many have starred this project]
+<td style="white-space:nowrap">
+  [is projects.num_stars "0"]
+  [else]
+    <span id="star_count-[projects.project_name]">[projects.num_stars]</span>
+  [end]
+</td>
+
+[# When project was last updated]
+<td style="white-space:nowrap">
+  [if-any projects.last_updated_exists]
+    [projects.recent_activity]
+  [end]
+</td>
+
+[# The short summary of this project]
+<td style="width:100%">
+  [is projects.limited_summary ""][else]
+    [projects.limited_summary]<br>
+  [end]
+</td>
diff --git a/templates/sitewide/unified-settings.ezt b/templates/sitewide/unified-settings.ezt
new file mode 100644
index 0000000..1f2c79c
--- /dev/null
+++ b/templates/sitewide/unified-settings.ezt
@@ -0,0 +1,93 @@
+[# common form fields for changing user settings ]
+<input type="hidden" name="token" value="[form_token]">
+
+
+<h4>Privacy</h4>
+<div style="margin:0 0 2em 2em">
+ <input type="checkbox" name="obscure_email" id="obscure_email" value="1"
+        [if-any read_only]disabled="disabled"[end]
+        [if-any settings_user_pb.obscure_email_bool]checked="checked"[end] >
+ <label for="obscure_email">
+   When [if-any self]I participate[else]this user participates[end]
+   in projects, show non-members [if-any self]my[else]this user's[end] email address as
+   "[settings_user.obscured_username]...@[settings_user.domain]", instead of
+   showing the full address.
+ </label>
+
+ <br><br>
+</div>
+
+<h4>Notifications</h4>
+<div style="margin:0 0 2em 2em">
+  [# TODO(jrobbins): re-implement issue preview on hover in polymer.]
+
+ <p>
+  Whenever an issue is changed by another user, send
+  [if-any self]me[else]this user[end] an email:
+ </p>
+ <input type="checkbox" name="notify" id="notify" value="1"
+        [if-any read_only]disabled="disabled"[end]
+        [if-any settings_user_pb.notify_issue_change_bool]checked="checked"[end] >
+ <label for="notify">
+   If [if-any self]I am[else]this user is[end] in the issue's <b>owner</b> or <b>CC</b> fields.
+ </label><br>
+ <input type="checkbox" name="notify_starred" id="notify_starred" value="1"
+        [if-any read_only]disabled="disabled"[end]
+        [if-any settings_user_pb.notify_starred_issue_change_bool]checked="checked"[end]  >
+ <label for="notify_starred">
+  If [if-any self]I[else]this user[end] <b>starred</b> the issue.
+ </label>
+
+ <p>
+  When a date specified in an issue arrives, and that date field is configured to notify
+  issue participants:
+ </p>
+ <input type="checkbox" name="notify_starred_ping" id="notify_starred_ping" value="1"
+        [if-any read_only]disabled="disabled"[end]
+        [if-any settings_user_pb.notify_starred_ping_bool]checked="checked"[end] >
+ <label for="notify_starred_ping">
+   Also send a notification if [if-any self]I[else]this user[end] <b>starred</b> the issue.
+ </label><br>
+
+ <p>
+  Email notifications sent to me should:
+ </p>
+ <input type="checkbox" name="email_compact_subject" id="email_compact_subject" value="1"
+        [if-any read_only]disabled="disabled"[end]
+        [if-any settings_user_pb.email_compact_subject_bool]checked="checked"[end] >
+ <label for="email_compact_subject">
+   Format the subject line compactly
+ </label><br>
+ <input type="checkbox" name="email_view_widget" id="email_view_widget" value="1"
+        [if-any read_only]disabled="disabled"[end]
+        [if-any settings_user_pb.email_view_widget_bool]checked="checked"[end]  >
+ <label for="email_view_widget">
+   Include a "View Issue" button in Gmail
+ </label><br>
+ <br>
+</div>
+
+<h4>Community interactions</h4>
+<div style="margin:0 0 2em 2em">
+ <input type="checkbox" name="restrict_new_issues" id="restrict_new_issues" value="1"
+        [if-any read_only]disabled="disabled"[end]
+        [if-any settings_user_prefs.restrict_new_issues]checked="checked"[end] >
+ <label for="restrict_new_issues">
+   When entering a new issue, add Restrict-View-Google to the form.
+ </label><br>
+
+ <input type="checkbox" name="public_issue_notice" id="public_issue_notice" value="1"
+        [if-any read_only]disabled="disabled"[end]
+        [if-any settings_user_prefs.public_issue_notice]checked="checked"[end] >
+ <label for="public_issue_notice">
+   When viewing a public issue, display a banner.
+ </label><br>
+</div>
+
+<h4>Availability</h4>
+<div style="margin:0 0 2em 2em">
+ Vacation message:
+ <input type="text" size="50" name="vacation_message" id="vacation_message"
+        value="[settings_user_pb.vacation_message]"
+        [if-any read_only]disabled="disabled"[end] >
+</div>
diff --git a/templates/sitewide/user-clear-bouncing-page.ezt b/templates/sitewide/user-clear-bouncing-page.ezt
new file mode 100644
index 0000000..5c9123d
--- /dev/null
+++ b/templates/sitewide/user-clear-bouncing-page.ezt
@@ -0,0 +1,26 @@
+[include "../framework/header.ezt" "showusertabs" "t1"]
+
+<div id="colcontrol">
+<h2>Reset bouncing email</h2>
+
+[if-any last_bounce_str]
+  <p>
+    <b>Email to this user bounced:</b>
+    [last_bounce_str]
+  </p>
+[end]
+
+
+<p>If you believe that email sent to this user will no longer bounce,
+   press the button below to clear the email bouncing status.</p>
+
+<form action="clearBouncing.do" method="POST">
+  <input type="hidden" name="token" value="[form_token]">
+  <input id="submit_btn" type="submit" name="btn"
+         value="Clear bouncing status">
+</form>
+
+</div>
+
+
+[include "../framework/footer.ezt"]
diff --git a/templates/sitewide/user-profile-page-polymer.ezt b/templates/sitewide/user-profile-page-polymer.ezt
new file mode 100644
index 0000000..fe10704
--- /dev/null
+++ b/templates/sitewide/user-profile-page-polymer.ezt
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<title>Monorail: Polymer Profile Page</title>
+
+[include "header-shared.ezt"]
+
+[include "../webpack-out/mr-profile-page.ezt"]
+
+<mr-profile-page
+  viewedUserId="[viewed_user_id]"
+  viewedUser="[viewed_user_display_name]" [if-any logged_in_user]
+  user="[logged_in_user.email]"[end]
+  loginUrl="[login_url]"
+  logoutUrl="[logout_url]"
+  lastVisitStr="[last_visit_str]"
+  starredUsers="[starred_users_json]"
+></mr-profile-page>
+
+[include "../framework/polymer-footer.ezt"]
diff --git a/templates/sitewide/user-profile-page.ezt b/templates/sitewide/user-profile-page.ezt
new file mode 100644
index 0000000..7517d15
--- /dev/null
+++ b/templates/sitewide/user-profile-page.ezt
@@ -0,0 +1,474 @@
+[define category_css]css/ph_list.css[end]
+[include "../framework/header.ezt" "showusertabs" "t1"]
+[include "../framework/js-placeholders.ezt"]
+<div id="colcontrol">
+
+<h2>
+  [if-any viewing_self][else]
+   [if-any user_stars_enabled]
+    [if-any logged_in_user]
+     [if-any read_only][else]
+          [if-any user_stars_enabled]
+           [if-any logged_in_user]
+            [if-any read_only][else]
+             <a id="user_star"
+              style="color:[if-any is_user_starred]cornflowerblue[else]gray[end]"
+              title="[if-any is_user_starred]Un-s[else]S[end]tar this user">
+             [if-any is_user_starred]&#9733;[else]&#9734;[end]
+             </a>
+            [end]
+           [end]
+          [end]
+     [end]
+    [end]
+   [end]
+ [end]
+
+ [viewed_user_display_name]
+</h2>
+
+<p>
+  <b>Last visit:</b>
+  [last_visit_str]
+</p>
+
+[if-any last_bounce_str]
+  <p>
+    <b>Email to this user bounced:</b>
+    [last_bounce_str]
+    [define offer_clear_bouncing]No[end]
+    [if-any viewing_self][define offer_clear_bouncing]Yes[end][end]
+    [if-any perms._EditOtherUsers][define offer_clear_bouncing]Yes[end][end]
+    [is offer_clear_bouncing "Yes"]
+      <a href="[viewed_user.profile_url]clearBouncing" style="margin-left:2em">Clear</a>
+    [end]
+  </p>
+[end]
+
+[if-any vacation_message]
+  <p>
+    <b>Vacation message:</b>
+    [vacation_message]
+  </p>
+[end]
+
+[if-any linked_parent]
+  <p>
+  <b>Linked parent account:</b>
+    [include "../framework/user-link.ezt" linked_parent]
+    [if-any offer_unlink perms._EditOtherUsers]
+      <input type="button" class="unlink_account secondary"
+             data-parent="[linked_parent.email]"
+             data-child="[viewed_user.email]"
+             value="Unlink">
+    [end]
+  </p>
+[end]
+
+[if-any linked_children]
+  <p>
+  <b>Linked child accounts:</b>
+  [for linked_children]
+    [include "../framework/user-link.ezt" linked_children]
+    [if-any offer_unlink perms._EditOtherUsers]
+      <input type="button" class="unlink_account secondary"
+             data-parent="[viewed_user.email]"
+             data-child="[linked_children.email]"
+             value="Unlink">
+    [end]
+  [end]
+  </p>
+[end]
+
+[if-any incoming_invite_users]
+  <b>Accept linked sub-account:</b>
+    [for incoming_invite_users]
+      <div>
+        [include "../framework/user-link.ezt" incoming_invite_users]
+        [if-any can_edit_invites][# TODO(jrobbins): allow site admin to accept invites for other users.]
+          <input type="button" class="incoming_invite" data-email="[incoming_invite_users.email]" value="Accept">
+          [# TODO(jrobbins): Button to decline invite.]
+        [end]
+      </div>
+    [end]
+[else][if-any outgoing_invite_users]
+  <b>Waiting for acceptance by parent-account:</b>
+    [for outgoing_invite_users]
+      <div>
+        [include "../framework/user-link.ezt" outgoing_invite_users]
+      </div>
+    [end]
+[else][if-any possible_parent_accounts]
+  <b>Link this account to:</b>
+  <select id="parent_to_invite">
+    <option value="" selected="selected">----</option>
+    [for possible_parent_accounts]
+      <option value="[possible_parent_accounts]">[possible_parent_accounts]</option>
+    [end]
+  </select>
+  [if-any can_edit_invites][# TODO(jrobbins): allow site admin to create invites for other users.]
+    <button id="create_linked_account_invite" disabled="disabled">Link</button>
+  [end]
+[end][end][end]
+
+
+[if-any user_stars_enabled]
+<p>
+<b>Starred developers:</b>
+[if-any starred_users]
+[for starred_users]
+  [include "../framework/user-link.ezt" starred_users][if-index starred_users last][else], [end]
+[end]
+[else]<i>None</i>[end]
+</p>
+[end]
+<br>
+
+<div class="list">
+  <table style="width: 100%;" cellspacing="0" cellpadding="0">
+  <tbody><tr>
+     <th style="text-align: left;">Projects
+     </th>
+  </tr></tbody>
+  </table>
+</div>
+
+<table cellspacing="0" cellpadding="2" border="0" class="results striped" id="projecttable" width="100%">
+    <tbody>
+      <tr id="headingrow">
+        [if-any logged_in_user]
+        <th style="white-space:nowrap; width:3%;"></th>
+        [end]
+        <th style="white-space:nowrap; width:15%;">Role</th>
+        <th style="white-space:nowrap; width:25%;">Project</th>
+        <th style="white-space:nowrap; width:57%;">Summary</th>
+      </tr>
+ [if-any owner_of_projects committer_of_projects contributor_to_projects]
+      [if-any owner_of_projects]
+        [for owner_of_projects]
+        <tr data-url="[owner_of_projects.relative_home_url]" data-project-name="[owner_of_projects.project_name]">
+        [if-any logged_in_user]
+        <td class="rowwidgets">
+         <a class="star"
+          style="color:[if-any owner_of_projects.starred]cornflowerblue[else]gray[end]"
+          title="[if-any owner_of_projects.starred]Un-s[else]S[end]tar this project"
+          data-project-name="[owner_of_projects.project_name]">
+         [if-any owner_of_projects.starred]&#9733;[else]&#9734;[end]
+         </a>
+        </td>
+        [end]
+        <td>Owner</td>
+        <td class="id" name="owner">
+        <a href="[owner_of_projects.relative_home_url]/">[owner_of_projects.project_name]</a>
+          [is owner_of_projects.state_name "HIDDEN"]<span style="color:red"> - hidden</span>[end]
+        </td>
+        <td>[owner_of_projects.summary]</td>
+        </tr>
+        [end]
+      [end]
+      [if-any committer_of_projects]
+        [for committer_of_projects]
+        <tr data-url="[committer_of_projects.relative_home_url]" data-project-name="[committer_of_projects.project_name]">
+        [if-any logged_in_user]
+        <td class="rowwidgets">
+         <a class="star"
+          style="color:[if-any committer_of_projects.starred]cornflowerblue[else]gray[end]"
+          title="[if-any committer_of_projects.starred]Un-s[else]S[end]tar this project"
+          data-project-name="[committer_of_projects.project_name]">
+         [if-any committer_of_projects.starred]&#9733;[else]&#9734;[end]
+         </a>
+        </td>
+        [end]
+        <td>Committer</td>
+        <td class="id" name="committer">
+          <a href="[committer_of_projects.relative_home_url]/">[committer_of_projects.project_name]
+          </a>
+        </td>
+        <td>
+        [committer_of_projects.summary]
+        </td>
+        </tr>
+        [end]
+      [end]
+
+      [if-any contributor_to_projects]
+        [for contributor_to_projects]
+        <tr data-url="[contributor_to_projects.relative_home_url]" data-project-name="[contributor_to_projects.project_name]">
+        [if-any logged_in_user]
+        <td class="rowwidgets">
+         <a class="star"
+          style="color:[if-any contributor_to_projects.starred]cornflowerblue[else]gray[end]"
+          title="[if-any contributor_to_projects.starred]Un-s[else]S[end]tar this project"
+          data-project-name="[contributor_to_projects.project_name]">
+         [if-any contributor_to_projects.starred]&#9733;[else]&#9734;[end]
+         </a>
+        </td>
+        [end]
+        <td>Contributor</td>
+        <td class="id" name="contributor">
+          <a href="[contributor_to_projects.relative_home_url]/">[contributor_to_projects.project_name]
+          </a>
+        [is contributor_to_projects.state_name "HIDDEN"]<span style="color:red"> - hidden</span>[end]</td>
+        <td>
+        [contributor_to_projects.summary]
+        </td>
+        </tr>
+        [end]
+      [end]
+
+ [else]
+      <tr>
+      <td colspan="4"><i>No projects.</i></td>
+      <tr>
+ [end]
+  </tbody>
+</table>
+
+
+[if-any starred_projects]
+<br>
+<div class="list">
+  <table style="width: 100%;" cellspacing="0" cellpadding="0">
+  <tbody><tr>
+     <th style="text-align: left;">
+      Starred by [if-any viewing_self]me[else]
+      [viewed_user_display_name]
+      [end]
+     </th>
+  </tr></tbody>
+  </table>
+</div>
+<table cellspacing="0" cellpadding="2" border="0" class="results striped" id="starredtable" width="100%">
+    <tbody>
+      <tr id="headingrow">
+        [if-any logged_in_user]
+        <th style="white-space:nowrap; width:3%;"></th>
+        [end]
+        <th style="white-space:nowrap; width:25%;">Name</th>
+        <th style="white-space:nowrap; width:57%;">Summary</th>
+      </tr>
+
+      [for starred_projects]
+      <tr data-url="[starred_projects.relative_home_url]" data-project-name="[starred_projects.project_name]">
+      [if-any logged_in_user]
+      <td class="rowwidgets">
+        <a class="star"
+         style="color:[if-any starred_projects.starred]cornflowerblue[else]gray[end]"
+         title="[if-any starred_projects.starred]Un-s[else]S[end]tar this project"
+         data-project-name="[starred_projects.project_name]">
+        [if-any starred_projects.starred]&#9733;[else]&#9734;[end]
+        </a>
+      </td>
+      [end]
+      <td class="id" name="starred_project">
+        <a href="[starred_projects.relative_home_url]/">[starred_projects.project_name]</a>
+        [is starred_projects.state_name "HIDDEN"]<span style="color:red"> - hidden</span>[end]
+      </td>
+      <td>
+      [starred_projects.summary]
+      </td>
+      </tr>
+      [end]
+
+</table>
+[end]
+
+[if-any owner_of_archived_projects]
+<br>
+<div class="list">
+  <table style="width: 100%;" cellspacing="0" cellpadding="0">
+  <tbody><tr>
+     <th style="text-align: left;">Archived projects
+     </th>
+  </tr></tbody>
+  </table>
+</div>
+<table cellspacing="0" cellpadding="2" border="0" class="results striped" id="archivedtable" width="100%">
+    <tbody>
+      <tr id="headingrow">
+        <th style="white-space:nowrap; width:25%;">Name</th>
+        <th style="white-space:nowrap; width:60%;">Summary</th>
+      </tr>
+        [for owner_of_archived_projects]
+        <tr data-url="[owner_of_archived_projects.relative_home_url]/adminAdvanced">
+        <td class="id" name="deleted_project">[owner_of_archived_projects.project_name] -
+          <a href="[owner_of_archived_projects.relative_home_url]/adminAdvanced">Unarchive or delete</a>
+        </td>
+        <td>
+        [owner_of_archived_projects.summary]
+        </td>
+        </tr>
+        [end]
+</table>
+[end]
+
+[if-any user_groups]
+<br>
+<div class="list">
+  <table style="width: 100%;" cellspacing="0" cellpadding="0">
+  <tbody><tr>
+     <th style="text-align: left;">User groups
+     </th>
+  </tr></tbody>
+  </table>
+</div>
+<table cellspacing="0" cellpadding="2" border="0" class="results striped" id="usergrouptable" width="100%">
+ <tbody>
+  <tr id="headingrow">
+   <th style="white-space:nowrap; width:25%;">Name</th>
+  </tr>
+  [for user_groups]
+   <tr data-url="[user_groups.profile_url]">
+    <td class="id">
+     <a href="[user_groups.profile_url]">[user_groups.email]</a>
+    </td>
+   </tr>
+  [end]
+ </tbody>
+</table>
+[end]
+
+[if-any can_ban]
+ <form action="ban.do" method="POST">
+  <input type="hidden" name="token" value="[ban_token]">
+  <h4>Banned for abuse</h4>
+  <div style="margin:0 0 2em 2em">
+   <input type="checkbox" name="banned" id="banned" value="1"
+          [if-any settings_user_is_banned]checked="checked"[end] >
+   <label for="banned">This user is banned because:</label>
+   <input type="text" size="50" name="banned_reason" id="banned_reason" value="[settings_user_pb.banned]">
+  </div>
+
+  <div style="margin:0 0 2em 2em">
+   <input id="submit_btn" type="submit" name="btn"
+          value="Update banned status">
+  </div>
+
+ </form>
+
+  [if-any viewed_user_is_spammer]
+   <form action="banSpammer.do" method="POST">
+    <input type="hidden" name="token" value="[ban_spammer_token]">
+    <input type="hidden" size="50" name="banned_reason" id="banned_reason" value="">
+    <input type="submit" name="undoBanSpammerButton" id="undo_ban_spammer_btn" value="Un-ban this user as a spammer">
+   </form>
+  [end]
+
+
+  [if-any viewed_user_may_be_spammer]
+   <form action="banSpammer.do" method="POST">
+    <input type="hidden" name="token" value="[ban_spammer_token]">
+    <input type="hidden" name="banned" value="True">
+    <input type="hidden" size="50" name="banned_reason" id="banned_reason" value="Spam">
+    <input type="submit" name="banSpammerButton" id="ban_spammer_btn" value="Ban this user as a spammer">
+   </form>
+  [end]
+
+[end]
+
+[if-any perms._EditOtherUsers]
+<h3 style="clear:both">Edit user</h3>
+ <form action="edit.do" method="POST">
+  <input type="hidden" name="token" value="[form_token]">
+  <h4>Site administration</h4>
+  <div style="margin:0 0 2em 2em">
+   <input type="checkbox" name="site_admin" id="site_admin" value="1" [if-any viewed_user_pb.is_site_admin_bool]checked="checked"[end] >
+   <label for="site_admin">This user is a site administrator (a super user)</label>
+  </div>
+
+  [include "unified-settings.ezt"]
+
+  <div style="margin:0 0 2em 2em">
+   <input id="submit_btn" type="submit" name="btn"
+          value="Save changes">
+  </div>
+
+ </form>
+[end]
+
+[if-any can_delete_user]
+<h3 style="clear:both">Delete user account</h3>
+  <p>Deleting a user account deletes the user and most user owned items from the site.
+     The user's email will be removed from any issues that the user participated in.
+     Hotlists owned by the user will either be transferred to another editor or get deleted.
+     Any Project Rules that the user is involved in will get deleted.
+  </p>
+  <div style="margin:0 0 2em 2em">
+    <input id="delete_btn" type="submit" name="btn" value="Delete user account">
+    <div id="delete_error" class="fielderror"></div>
+  </div>
+[end]
+
+</div>
+</div>
+
+<script type="text/javascript" nonce="[nonce]">
+runOnLoad(function() {
+  if ($("user_star")) {
+    [# The user viewing this page wants to star the user *on* this page]
+    $("user_star").addEventListener("click", function () {
+       _TKR_toggleStar($("user_star"), null, null, "[viewed_user_id]", null, null);
+    });
+  }
+
+  var stars = document.getElementsByClassName("star");
+  for (var i = 0; i < stars.length; ++i) {
+    var star = stars[[]i];
+    star.addEventListener("click", function (event) {
+        var projectName = event.target.getAttribute("data-project-name");
+        _TKR_toggleStar(event.target, projectName);
+    });
+  }
+
+  function _handleProjectClick(event) {
+    var target = event.target;
+    if (target.tagName == "A")
+      return;
+
+    if (target.classList.contains("rowwidgets") || target.parentNode.classList.contains("rowwidgets"))
+      return;
+    if (target.tagName != "TR") target = target.parentNode;
+    _go(target.attributes[[]"data-url"].value,
+        (event.metaKey || event.ctrlKey || event.button == 1));
+  };
+  $("projecttable").addEventListener("click", _handleProjectClick);
+  if ($("starredtable")) {
+    $("starredtable").addEventListener("click", _handleProjectClick);
+  }
+  if ($("archivedtable")) {
+    $("archivedtable").addEventListener("click", _handleProjectClick);
+  }
+
+  if ($("banned_reason")) {
+    $("banned_reason").addEventListener("keyup", function() {
+      $("banned").checked = $("banned_reason").value != "";
+    });
+  }
+
+  if ($("ban_spammer_btn")) {
+    $("ban_spammer_btn").addEventListener("click", function(evt) {
+       var ok = window.confirm("This will remove all issues and comments " +
+          "created by this user. Continue?");
+       if (!ok) {
+         evt.preventDefault();
+       }
+     });
+   }
+
+   if ($("delete_btn")) {
+     $("delete_btn").addEventListener("click", async function(event) {
+       const expungeCall = window.prpcClient.call(
+         'monorail.Users', 'ExpungeUser', {email: "[viewed_user_display_name]"});
+       expungeCall.then((resp) => {
+         location.replace(location.origin);
+       }).catch((reason) => {
+         $("delete_error").textContent = reason;
+       });
+     });
+   }
+});
+</script>
+<script type="module" defer src="[version_base]/static/js/sitewide/linked-accounts.js" nonce="[nonce]"></script>
+
+
+[include "../framework/footer.ezt"]
diff --git a/templates/sitewide/user-settings-page.ezt b/templates/sitewide/user-settings-page.ezt
new file mode 100644
index 0000000..5e8ef2c
--- /dev/null
+++ b/templates/sitewide/user-settings-page.ezt
@@ -0,0 +1,17 @@
+[define category_css]css/ph_detail.css[end]
+[include "../framework/header.ezt" "showusertabs" "t1"]
+
+<div style="max-width:50em">
+
+<h3>User Preferences</h3>
+
+<form action="settings.do" method="POST">
+  [include "unified-settings.ezt"]
+  [if-any read_only][else]
+   <input id="submit_btn" type="submit" name="btn" value="Save preferences">
+  [end]
+</form>
+
+</div>
+
+[include "../framework/footer.ezt"]
diff --git a/templates/sitewide/user-updates-page.ezt b/templates/sitewide/user-updates-page.ezt
new file mode 100644
index 0000000..70aaca9
--- /dev/null
+++ b/templates/sitewide/user-updates-page.ezt
@@ -0,0 +1,7 @@
+[define page_css]css/d_updates_page.css[end]
+
+[include "../framework/header.ezt" "showusertabs" "t3"]
+
+[include "../features/updates-page.ezt"]
+
+[include "../framework/footer.ezt"]
diff --git a/templates/sitewide/usergrouptabs.ezt b/templates/sitewide/usergrouptabs.ezt
new file mode 100644
index 0000000..1f29b43
--- /dev/null
+++ b/templates/sitewide/usergrouptabs.ezt
@@ -0,0 +1,15 @@
+[# Display a row of tabs for servlets with URLs starting with /u/username.
+
+  Args:
+     arg0: String like "t1", "t2", "t3" to identify the currently active tab.
+]
+
+<div class="[admin_tab_mode]">
+	<div class="at isf">
+		<span class="inst1"><a href="/g/[groupid]/">People</a></span>
+		[if-any offer_membership_editing]
+			<span class="inst2"><a href="/g/[groupid]/groupadmin">Administer</a></span>
+		[end]
+	</div>
+</div>
+
diff --git a/templates/sitewide/usertabs.ezt b/templates/sitewide/usertabs.ezt
new file mode 100644
index 0000000..8216bcb
--- /dev/null
+++ b/templates/sitewide/usertabs.ezt
@@ -0,0 +1,32 @@
+[# Display a row of tabs for servlets with URLs starting with /u/username.
+
+  Args:
+     arg0: String like "t1", "t2", "t3" to identify the currently active tab.
+]
+
+<div class="at isf [user_tab_mode]">
+  <span class="inst2">
+    <a href="[viewed_user.profile_url]">[if-any viewing_self]My Profile[else]User Profile[end]</a>
+  </span>
+
+  <span class="inst5">
+    <a href="[viewed_user.profile_url]updates">Updates</a>
+  </span>
+
+  [if-any viewing_self]
+  <span class="inst3">
+    <a href="/hosting/settings">Settings</a>
+  </span>
+  [end]
+
+  [if-any offer_saved_queries_subtab]
+  <span class="inst4">
+    <a href="[viewed_user.profile_url]queries">Saved Queries</a>
+  </span>
+  [end]
+
+  <span class="inst6">
+    <a href="[viewed_user.profile_url]hotlists">Hotlists</a>
+  </span>
+
+</div>
diff --git a/templates/tracker/admin-components-page.ezt b/templates/tracker/admin-components-page.ezt
new file mode 100644
index 0000000..0c9d2d1
--- /dev/null
+++ b/templates/tracker/admin-components-page.ezt
@@ -0,0 +1,203 @@
+[define category_css]css/ph_detail.css[end]
+[include "../framework/header.ezt" "showtabs"]
+
+[if-any read_only][include "../framework/read-only-rejection.ezt"]
+[else]
+
+<form action="adminComponents.do" id="adminComponents" method="POST">
+ <input type="hidden" name="token" value="form_token]">
+
+ <h4>Issue components</h4>
+ [if-any perms.EditProject]
+   <span style="margin:0 .7em">Show:
+    <select id="rowfilter">
+     <option label="All components" value="all">
+     <option label="Active components" value="active" selected=true>
+     <option label="Top-level components" value="toplevel">
+     <option label="Components I administer" value="myadmin">
+     <option label="Components I am CC'd on" value="mycc">
+     <option label="Deprecated components" value="deprecated">
+    </select>
+   </span>
+   <span style="margin:0 .7em">Select:
+     <a id="selectall" href="#">All</a>
+     <a id="selectnone" href="#">None</a>
+   </span>
+ [end]
+
+ <div class="list-foot"></div>
+ [if-any perms.EditProject]
+   <form action="adminComponents.do" method="POST">
+     <a href="/p/[projectname]/components/create" class="buttonify primary">Create component</a>
+     <input type="hidden" name="delete_components">
+     <input type="hidden" name="token" value="[form_token]">
+     <input type="submit" class="secondary" name="deletebtn" value="Delete Component(s)" disabled>
+   </form>
+   <div id="deletebtnsfeedback" class="fielderror" style="margin-left:1em">
+     [if-any failed_perm]
+       You do not have permission to delete the components:
+       [failed_perm]<br/>
+     [end]
+     [if-any failed_subcomp]
+       Can not delete the following components because they have subcomponents:
+       [failed_subcomp]<br/>
+     [end]
+     [if-any failed_templ]
+       Can not delete the following components because they are listed in templates:
+       [failed_templ]<br/>
+     [end]
+   </div>
+ [end]
+
+ <div class="section">
+   <table cellspacing="0" cellpadding="2" border="0" class="comptable results striped vt active" id="resultstable" width="100%">
+   <tbody>
+     <tr>
+       [if-any perms.EditProject]<th></th>[end]
+       <th>ID</th>
+       <th>Name</th>
+       <th>Administrators</th>
+       <th>Auto Cc</th>
+       <th>Add Labels</th>
+       <th>Description</th>
+     </tr>
+     [if-any component_defs][else]
+       <tr>
+         <td colspan="5">
+           <div style="padding: 3em; text-align: center">
+             This project has not defined any components.
+           </div>
+         </td>
+       </tr>
+     [end]
+     [for component_defs]
+       [define detail_url]/p/[projectname]/components/detail?component=[format "url"][component_defs.path][end][end]
+       <tr data-url="[detail_url]" class="comprow [component_defs.classes]">
+         [if-any perms.EditProject]
+           <td class="cb rowwidgets">
+             <input type="checkbox" data-path="[component_defs.path]" class="checkRangeSelect">
+           </td>
+         [end]
+         <td>
+            [component_defs.component_id]
+         </td>
+         <td class="id">
+           <a style="white-space:nowrap" href="[detail_url]">[component_defs.path]</a>
+         </td>
+         <td>
+           [for component_defs.admins]
+             [include "../framework/user-link.ezt" component_defs.admins][if-index component_defs.admins last][else],[end]
+           [end]
+         </td>
+         <td>
+           [for component_defs.cc]
+             [include "../framework/user-link.ezt" component_defs.cc][if-index component_defs.cc last][else],[end]
+           [end]
+         </td>
+         <td>
+           [for component_defs.labels]
+             [component_defs.labels][if-index component_defs.labels last][else],[end]
+           [end]
+         </td>
+         <td>
+             [component_defs.docstring_short]
+         </td>
+       </tr>
+     [end]
+   </tbody>
+   </table>
+ </div>[# section]
+
+ <div class="list-foot"></div>
+ [if-any perms.EditProject]
+   <form action="adminComponents.do" method="POST">
+     <a href="/p/[projectname]/components/create" class="buttonify primary">Create component</a>
+     <input type="hidden" name="delete_components">
+     <input type="hidden" name="token" value="[form_token]">
+     <input type="submit" class="secondary" name="deletebtn" value="Delete Component(s)" disabled>
+   </form>
+ [end]
+
+</form>
+
+[end]
+
+<script type="text/javascript" nonce="[nonce]">
+runOnLoad(function() {
+  if ($("selectall")) {
+    $("selectall").addEventListener("click", function() {
+        _selectAllIssues();
+        setDisabled(false);
+    });
+  }
+  if ($("selectnone")) {
+    $("selectnone").addEventListener("click", function() {
+        _selectNoneIssues();
+        setDisabled(true);
+    });
+  }
+
+  var checkboxNodes = document.getElementsByClassName("checkRangeSelect");
+  var checkboxes = Array();
+  for (var i = 0; i < checkboxNodes.length; ++i) {
+    var checkbox = checkboxNodes.item(i);
+    checkboxes.push(checkbox);
+    checkbox.addEventListener("click", function (event) {
+      _checkRangeSelect(event, event.target);
+      _highlightRow(event.target);
+      updateEnabled();
+    });
+  }
+
+  function updateEnabled() {
+    var anySelected = checkboxes.some(function(checkbox) {
+      return checkbox.checked;
+    });
+    setDisabled(!anySelected);
+   }
+
+  var deleteButtons = document.getElementsByName("deletebtn");
+  function setDisabled(disabled) {
+    for (var i = 0; i < deleteButtons.length; ++i) {
+      deleteButtons.item(i).disabled = disabled;
+    }
+  }
+
+  for (var i = 0; i < deleteButtons.length; ++i) {
+    deleteButtons.item(i).addEventListener("click", function(event) {
+      var componentsToDelete = [];
+      for (var i = 0; i< checkboxes.length; ++i) {
+        var checkbox = checkboxes[[]i];
+        if (checkbox.checked)
+          componentsToDelete.push(checkbox.getAttribute("data-path"));
+      }
+      var fields = document.getElementsByName("delete_components");
+      for (var i = 0; i< fields.length; ++i) {
+        fields.item(i).value = componentsToDelete.join();
+      }
+      if (!confirm("Are you sure you want to delete the selected components ?\nThis operation cannot be undone."))
+        event.preventDefault();
+     });
+  }
+
+  function _handleResultsClick(event) {
+    var target = event.target;
+    if (target.tagName == "A" || target.type == "checkbox" || target.className == "cb")
+      return;
+    while (target && target.tagName != "TR") target = target.parentNode;
+    _go(target.attributes[[]"data-url"].value,
+        (event.metaKey || event.ctrlKey || event.button == 1));
+  };
+  _addClickListener($("resultstable"), _handleResultsClick);
+
+
+  function _handleRowFilterChange(event) {
+    $("resultstable").classList.remove('all', 'active', 'toplevel', 'myadmin', 'mycc', 'deprecated');
+    $("resultstable").classList.add(event.target.value);
+  };
+  $("rowfilter").addEventListener("change", _handleRowFilterChange);
+});
+</script>
+
+
+[include "../framework/footer.ezt"]
diff --git a/templates/tracker/admin-labels-page.ezt b/templates/tracker/admin-labels-page.ezt
new file mode 100644
index 0000000..e8cb7ae
--- /dev/null
+++ b/templates/tracker/admin-labels-page.ezt
@@ -0,0 +1,138 @@
+[define category_css]css/ph_list.css[end]
+[include "../framework/header.ezt" "showtabs"]
+[include "../framework/js-placeholders.ezt"]
+
+[if-any read_only][include "../framework/read-only-rejection.ezt"]
+[else]
+
+<form action="adminLabels.do" id="adminLabels" method="POST">
+ <input type="hidden" name="token" value="[form_token]">
+
+ <h4>Predefined issue labels</h4>
+ <div class="section">
+  [if-any perms.EditProject]
+    <table class="vt">
+     <tr><td>
+       <textarea name="predefinedlabels" rows="12" cols="75" style="tab-size:18">[labels_text]</textarea>
+       [if-any errors.label_defs]
+         <div class="fielderror">[errors.label_defs]</div>
+       [end]
+       <div>
+         Each issue may have <b>at most one</b> label with each of these prefixes:<br>
+         <input type="text" size="75" name="excl_prefixes"
+                value="[for config.excl_prefixes][config.excl_prefixes][if-index config.excl_prefixes last][else], [end][end]">
+       </div>
+      </td>
+      <td style="padding-left:.7em">
+       <div class="tip">
+           <b>Instructions:</b><br> List one label per line in desired sort-order.<br><br>
+           Optionally, use an equals-sign to document the meaning of each label.
+       </div>
+      </td>
+     </tr>
+    </table>
+  [else]
+    <table cellspacing="0" cellpadding="2" border="0" class="results striped" width="100%">
+     <tr>
+       <th style="min-width:14em">Label</th>
+       <th width="100%">Meaning</th>
+     </tr>
+     [for config.issue_labels]
+       <tr>
+         <td style="white-space:nowrap; padding-right:2em; color:#363">[config.issue_labels.name]</td>
+         <td>[config.issue_labels.docstring]</td>
+       </tr>
+     [end]
+    </table>
+  [end]
+ </div>
+
+ [if-any perms.EditProject]
+   <input type="submit" id="savechanges" name="btn" value="Save changes" class="submit">
+ [end]
+
+ <br>
+ <br>
+
+ <h4>Custom fields</h4>
+ <div class="section">
+  <table cellspacing="0" cellpadding="2" border="0" class="results striped vt" id="resultstable" width="100%">
+  <tbody>
+    <tr>
+      <th>ID</th>
+      <th>Name</th>
+      <th>Type</th>
+      <th>Required</th>
+      <th>Multivalued</th>
+      <th>Applicable to</th>
+      <th>Description</th>
+    </tr>
+    [if-any field_defs][else]
+      <tr>
+        <td colspan="40">
+          <div style="padding: 3em; text-align: center">
+            This project has not defined any custom fields.
+          </div>
+        </td>
+      </tr>
+    [end]
+    [for field_defs]
+      [define detail_url]/p/[projectname]/fields/detail?field=[field_defs.field_name][end]
+      [is field_defs.type_name "INT_TYPE"][define pretty_type_name]Integer[end][end]
+      [is field_defs.type_name "ENUM_TYPE"][define pretty_type_name]Enum[end][end]
+      [is field_defs.type_name "USER_TYPE"][define pretty_type_name]User[end][end]
+      [is field_defs.type_name "STR_TYPE"][define pretty_type_name]String[end][end]
+      [is field_defs.type_name "DATE_TYPE"][define pretty_type_name]Date[end][end]
+      [is field_defs.type_name "URL_TYPE"][define pretty_type_name]Url[end][end]
+      [is field_defs.type_name "APPROVAL_TYPE"][define pretty_type_name]Approval[end][end]
+      <tr data-url="[detail_url]">
+        <td>
+          [field_defs.field_def.field_id]
+        </td>
+        <td class="id" style="white-space:nowrap">
+          <a href="[detail_url]">[field_defs.field_name]</a></td>
+        <td style="white-space:nowrap">
+          [pretty_type_name]
+        </td>
+        <td style="white-space:nowrap">
+          [if-any field_defs.is_required_bool]Required[else]Optional[end]
+        </td>
+        <td style="white-space:nowrap">
+          [if-any field_defs.is_multivalued_bool]Multiple[else]Single[end]
+        </td>
+        <td style="white-space:nowrap">
+          [if-any field_defs.applicable_type][field_defs.applicable_type][else]Any issue[end]
+        </td>
+        <td>
+           [field_defs.docstring_short]
+        </td>
+      </tr>
+    [end]
+  </tbody>
+  </table>
+  <div class="list-foot"></div>
+  [if-any perms.EditProject]
+    <p><a href="/p/[projectname]/fields/create" class="buttonify primary">Add field</a></p>
+  [end]
+ </div>
+
+</form>
+
+[end]
+
+
+<script type="text/javascript" nonce="[nonce]">
+runOnLoad(function() {
+  function _handleResultsClick(event) {
+    var target = event.target;
+    if (target.tagName == "A")
+      return;
+    while (target && target.tagName != "TR") target = target.parentNode;
+    _go(target.attributes[[]"data-url"].value,
+        (event.metaKey || event.ctrlKey || event.button == 1));
+  };
+  _addClickListener($("resultstable"), _handleResultsClick);
+});
+</script>
+
+[include "../framework/footer.ezt"]
diff --git a/templates/tracker/admin-rules-page.ezt b/templates/tracker/admin-rules-page.ezt
new file mode 100644
index 0000000..2ffc3fa
--- /dev/null
+++ b/templates/tracker/admin-rules-page.ezt
@@ -0,0 +1,17 @@
+[define category_css]css/ph_detail.css[end]
+[include "../framework/header.ezt" "showtabs"]
+
+[if-any read_only][include "../framework/read-only-rejection.ezt"]
+[else]
+
+<form action="adminRules.do" id="adminRules" method="POST">
+ <input type="hidden" name="token" value="[form_token]">
+
+ [include "../framework/filter-rule-admin-part.ezt" "with_tracking_actions"]
+
+ <input type="submit" id="savechanges" name="btn" value="Save changes" class="submit">
+</form>
+
+[end]
+
+[include "../framework/footer.ezt"]
diff --git a/templates/tracker/admin-statuses-page.ezt b/templates/tracker/admin-statuses-page.ezt
new file mode 100644
index 0000000..2d2d936
--- /dev/null
+++ b/templates/tracker/admin-statuses-page.ezt
@@ -0,0 +1,82 @@
+[define category_css]css/ph_detail.css[end]
+[include "../framework/header.ezt" "showtabs"]
+
+[if-any read_only][include "../framework/read-only-rejection.ezt"]
+[else]
+
+<form action="adminStatuses.do" id="adminStatuses" method="POST">
+ <input type="hidden" name="token" value="[form_token]">
+
+  [if-any perms.EditProject]
+    <table class="vt">
+     <tr><td>
+       <h4>Open Issue Status Values</h4>
+       <div class="section">
+         <textarea name="predefinedopen" rows="6" cols="75"  style="tab-size:18">[open_text]</textarea>
+         [if-any errors.open_statuses]
+           <div class="fielderror">[errors.open_statuses]</div>
+         [end]
+       </div>
+       <h4>Closed Issue Status Values</h4>
+       <div class="section">
+         <textarea name="predefinedclosed" rows="6" cols="75"  style="tab-size:18">[closed_text]</textarea><br><br>
+         [if-any errors.closed_statuses]
+           <div class="fielderror">[errors.closed_statuses]</div>
+         [end]
+
+         If an issue's status is being set to one of these values, offer to merge issues:<br>
+         <input type="text" size="75" name="statuses_offer_merge"
+                value="[for config.statuses_offer_merge][config.statuses_offer_merge][if-index config.statuses_offer_merge last][else], [end][end]">
+       </div>
+      </td>
+      <td style="padding-left:.7em">
+       <div class="tip">
+           <b>Instructions:</b><br> List one status value per line in desired sort-order.<br><br>
+           Optionally, use an equals-sign to document the meaning of each status value.
+       </div>
+      </td>
+     </tr>
+    </table>
+  [else]
+    <h4>Open Issue Status Values</h4>
+    <div class="section">
+    <table cellspacing="0" cellpadding="2" border="0" class="results striped" width="100%">
+      <tr>
+        <th style="min-width:14em">Status</th>
+        <th width="100%">Meaning</th>
+      </tr>
+      [for config.open_statuses]
+        <tr>
+          <td style="white-space:nowrap; padding-right:2em;">[config.open_statuses.name]</td>
+          <td>[config.open_statuses.docstring]</td>
+        </tr>
+      [end]
+    </table>
+    </div>
+
+    <h4>Closed Issue Status Values</h4>
+    <div class="section">
+    <table cellspacing="0" cellpadding="2" border="0" class="results striped" width="100%">
+      <tr>
+        <th style="min-width:14em">Status</th>
+        <th width="100%">Meaning</th>
+      </tr>
+      [for config.closed_statuses]
+        <tr>
+          <td  style="white-space:nowrap; padding-right:2em;">[config.closed_statuses.name]</td>
+          <td>[config.closed_statuses.docstring]</td>
+        </tr>
+      [end]
+    </table>
+    </div>
+  [end]
+
+
+ [if-any perms.EditProject]
+   <input type="submit" id="savechanges" name="btn" value="Save changes" class="submit">
+ [end]
+</form>
+
+[end]
+
+[include "../framework/footer.ezt"]
diff --git a/templates/tracker/admin-templates-page.ezt b/templates/tracker/admin-templates-page.ezt
new file mode 100644
index 0000000..2ef36f3
--- /dev/null
+++ b/templates/tracker/admin-templates-page.ezt
@@ -0,0 +1,76 @@
+[define category_css]css/ph_detail.css[end]
+[include "../framework/header.ezt" "showtabs"]
+
+[if-any read_only][include "../framework/read-only-rejection.ezt"]
+[else]
+
+[if-any perms.EditProject]
+  <h4>Default templates</h4>
+  <div class="section" style="padding-top:0">
+    <form action="adminTemplates.do" id="adminTemplates" method="POST">
+      <input type="hidden" name="token" value="[form_token]">
+
+      <div style="margin: 2em 0 1em 0">
+        Default template for project members:
+        <select name="default_template_for_developers" id="default_template_for_developers">
+          [for config.templates]
+            <option value="[config.templates.name]" [is config.templates.template_id config.default_template_for_developers]selected[end]>[config.templates.name]</option>
+          [end]
+        </select>
+        <br><br>
+
+        Default template for non-members:
+        <select name="default_template_for_users" id="default_template_for_users">
+           [for config.templates]
+             [define offer_template_in_users_menu]No[end]
+             [is config.templates.template_id config.default_template_for_users][define offer_template_in_users_menu]Yes[end][end]
+             [if-any config.templates.members_only][else][define offer_template_in_users_menu]Yes[end][end]
+             [is offer_template_in_users_menu "Yes"]
+               <option value="[config.templates.name]" [is config.templates.template_id config.default_template_for_users]selected[end]>[config.templates.name]</option>
+             [end]
+           [end]
+         </select>
+       </div>
+
+       <input type="submit" id="savechanges" name="btn" value="Save changes" class="submit" style="margin-left:0">
+    </form>
+  </div>
+[end]
+
+<h4>Issue templates</h4>
+<div class="section">
+  <table cellspacing="0" cellpadding="2" border="0" class="results striped vt" id="resultstable" width="100%">
+    <tbody>
+      <tr>
+        <th>Name</th>
+      </tr>
+      [if-any config.templates][else]
+        <tr>
+          <td colspan="40">
+            <div style="padding: 3em; text-align: center">
+              This project has not defined any issue templates.
+            </div>
+          </td>
+        </tr>
+      [end]
+      [for config.templates]
+        [if-any config.templates.can_view perms.EditProject]
+          [define detail_url]/p/[projectname]/templates/detail?template=[format "url"][config.templates.name][end][end]
+            <tr data-url="detail_url">
+              <td style="white-space:nowrap" class="id">
+                <a href="[detail_url]">[config.templates.name]</a></td>
+              </td>
+            </tr>
+        [end]
+      [end]
+    </tbody>
+  </table>
+
+  [if-any perms.EditProject]
+    <p><a href="/p/[projectname]/templates/create" class="buttonify primary">Add template</a></p>
+  [end]
+</div>
+
+[end][# end if not read_only]
+
+[include "../framework/footer.ezt"]
diff --git a/templates/tracker/admin-views-page.ezt b/templates/tracker/admin-views-page.ezt
new file mode 100644
index 0000000..5ec5d15
--- /dev/null
+++ b/templates/tracker/admin-views-page.ezt
@@ -0,0 +1,70 @@
+[define category_css]css/ph_detail.css[end]
+[include "../framework/header.ezt" "showtabs"]
+
+[if-any read_only][include "../framework/read-only-rejection.ezt"]
+[else]
+
+<form action="adminViews.do" id="adminViews" method="POST">
+ <input type="hidden" name="token" value="[form_token]">
+
+ [include "../framework/artifact-list-admin-part.ezt" "with_grid"]
+
+<h4 id="queries">Saved queries</h4>
+<div class="section">
+
+ <div class="closed">
+  <div>Saved queries help project visitors easily view relevant issue lists.
+   <a class="ifClosed toggleHidden" href="#"
+      style="font-size:90%; margin-left:.5em">Learn more</a>
+  </div>
+
+  <div id="filterhelp" class="ifOpened help">
+      Project owners can set up saved queries to make it easier for team members to
+      quickly run common queries.  More importantly, project owners can use saved
+      queries to focus the team's attention on the issue lists that are most important
+      for the project's success.  The project's saved queries are shown in the middle
+      section of the search dropdown menu that is next to the issue search box.
+  </div>
+  <br>
+
+  [if-any perms.EditProject]
+    [include "../framework/saved-queries-admin-part.ezt" "project"]
+  [else]
+    <table cellspacing="0" cellpadding="2" border="0" class="results striped">
+      <tr>
+        <th align="left">Saved query name</th>
+        <th align="left">Search in</th>
+        <th align="left">Query</th>
+      </tr>
+      [for canned_queries]
+        <tr>
+          <td>[canned_queries.name]</td>
+          <td>
+            [define can][canned_queries.base_query_id][end]
+            [is can "1"]All issues[end]
+            [is can "2"]Open issues[end]
+            [is can "3"]Open and owned by me[end]
+            [is can "4"]Open and reported by me[end]
+            [is can "5"]Open and starred by me[end]
+            [is can "6"]New issues[end]
+            [is can "7"]Issues to verify[end]
+            [is can "8"]Open with comment by me[end]
+          </td>
+          <td>
+            [canned_queries.query]
+          </td>
+        </tr>
+      [end]
+    </table>
+  [end]
+ </div>
+</div>
+
+ [if-any perms.EditProject]
+   <input type="submit" id="savechanges" name="btn" value="Save changes" class="submit">
+ [end]
+</form>
+
+[end]
+
+[include "../framework/footer.ezt"]
diff --git a/templates/tracker/approval-change-notification-email.ezt b/templates/tracker/approval-change-notification-email.ezt
new file mode 100644
index 0000000..c6a81ea
--- /dev/null
+++ b/templates/tracker/approval-change-notification-email.ezt
@@ -0,0 +1,23 @@
+[if-any comment.amendments][#
+  ]Updates:
+[#][for comment.amendments]        [comment.amendments.field_name]: [format "raw"][comment.amendments.newvalue][end]
+[#][end][#
+  ][end]
+Comment #[comment.sequence] on issue [issue_local_id][#
+  ] by [comment.creator.display_name]: [format "raw"][summary][#
+][end]
+[approval_url]
+
+[if-any comment.content][#
+  ][for comment.text_runs][include "render-plain-text.ezt" comment.text_runs][end][#
+][else](No comment was entered for this change.)[#
+][end]
+[if-any comment.attachments][#
+  ]Attachments:
+[#][for comment.attachments][#
+  ]        [comment.attachments.filename]: [domain_url][comment.attachments.downloadurl]&inline=1[end]
+[end]
+
+You are receiving this message because you are listed as the TL/PM
+on this issue or you are/were listed as an approver for this
+issue's approval.
diff --git a/templates/tracker/component-create-page.ezt b/templates/tracker/component-create-page.ezt
new file mode 100644
index 0000000..b818435
--- /dev/null
+++ b/templates/tracker/component-create-page.ezt
@@ -0,0 +1,129 @@
+[define title]Add a Component[end]
+[define category_css]css/ph_detail.css[end]
+[include "../framework/header.ezt" "showtabs"]
+
+<a href="/p/[projectname]/adminComponents">&lsaquo; Back to component list</a><br><br>
+
+
+<h4>Add a component</h4>
+
+<form action="create.do" method="POST">
+<input type="hidden" name="token" value="[form_token]">
+
+<table cellspacing="8" class="rowmajor vt">
+
+  <tr>
+    <th width="1%">Parent:</th>
+    <td>
+      <select name="parent_path" id="parent_path">
+        <option value="">Top level</option>
+        [for component_defs]
+          <option value="[component_defs.path]" [if-any component_defs.selected]selected=true[end]>[component_defs.path]</option>
+        [end]
+      </select>
+    </td>
+    <td rowspan="10">
+      <div class="tip">
+        <p>Components should describe the structure of the software being
+          built so that issues can be related to the correct parts.</p>
+
+        <p>Please use labels instead for releases,
+           milestones, task forces, types of issues, etc.</p>
+
+        <p>Deprecated components won't be shown in autocomplete.</p>
+      </div>
+    </td>
+  </tr>
+
+  <tr>
+    <th width="1%">Name:</th>
+    <td>
+      <input id="leaf_name" name="leaf_name" size="30" value="[initial_leaf_name]"
+             class="acob">
+      <span id="leafnamefeedback" class="fielderror" style="margin-left:1em">
+          [if-any errors.leaf_name][errors.leaf_name][end]
+      </span>
+    </td>
+  </tr>
+
+  <tr>
+    <th>Description:</th>
+    <td>
+      <textarea name="docstring" rows="4" cols="75">[initial_docstring]</textarea>
+    </td>
+  </tr>
+
+  <tr>
+    <th>Admins:</th>
+    <td>
+        <textarea id="member_admins" name="admins" rows="3" cols="75">[for initial_admins][initial_admins], [end]</textarea>
+        <span id="memberadminsfeedback" class="fielderror" style="margin-left:1em">
+            [if-any errors.member_admins][errors.member_admins][end]
+        </span>
+    </td>
+  </tr>
+
+  <tr>
+    <th>Auto Cc:</th>
+    <td>
+        <textarea id="member_cc" name="cc" rows="3" cols="75">[for initial_cc][initial_cc], [end]</textarea>
+        <span id="memberccfeedback" class="fielderror" style="margin-left:1em">
+            [if-any errors.member_cc][errors.member_cc][end]
+        </span>
+    </td>
+  </tr>
+
+  <tr>
+    <th>Add Labels:</th>
+    <td>
+        <textarea id="labels" name="labels" rows="3" cols="75">[for initial_labels][initial_labels], [end]</textarea>
+        <span id="labelsfeedback" class="fielderror" style="margin-left:1em">
+            [if-any errors.labels][errors.labels][end]
+        </span>
+    </td>
+  </tr>
+
+  <tr>
+    <th>Deprecated:</th>
+    <td>
+        <input type="checkbox" id="deprecated" name="deprecated">
+    </td>
+  </tr>
+
+  <tr>
+    <td></td>
+    <td>
+      <input id="submit_btn" type="submit" name="submit" value="Create component">
+    </td>
+  </tr>
+
+</table>
+</form>
+
+<script type="text/javascript" nonce="[nonce]">
+runOnLoad(function() {
+  document.getElementById('submit_btn').disabled = 'disabled';
+  document.getElementById('leaf_name').focus();
+
+  function checkSubmit() {
+    _checkLeafName(
+        '[projectname]',
+        document.getElementById('parent_path').value,
+        '', CS_env.token);
+  }
+  setInterval(checkSubmit, 700);
+
+  var acobElements = document.getElementsByClassName("acob");
+  for (var i = 0; i < acobElements.length; ++i) {
+     var el = acobElements[[]i];
+     el.addEventListener("focus", function(event) {
+         _acrob(null);
+         _acof(event);
+     });
+  }
+});
+</script>
+
+
+
+[include "../framework/footer.ezt"]
diff --git a/templates/tracker/component-detail-page.ezt b/templates/tracker/component-detail-page.ezt
new file mode 100644
index 0000000..01757d1
--- /dev/null
+++ b/templates/tracker/component-detail-page.ezt
@@ -0,0 +1,169 @@
+[# Use raw format because the title variable will be escaped when used.]
+[define title]Component [format "raw"][component_def.path][end][end]
+[define category_css]css/ph_detail.css[end]
+[include "../framework/header.ezt" "showtabs"]
+
+<a href="/p/[projectname]/adminComponents">&lsaquo; Back to component list</a><br><br>
+
+
+<h4>Component</h4>
+[if-any creator]
+  Created by <a href="[creator.profile_url]">[creator.display_name]</a> [created]<br/>
+[end]
+[if-any modifier]
+  Last modified by <a href="[modifier.profile_url]">[modifier.display_name]</a> [modified]<br/>
+[end]
+
+<br/>
+<form action="detail.do" method="POST">
+<input type="hidden" name="token" value="[form_token]">
+<input type="hidden" name="component" value="[component_def.path]">
+<table cellspacing="8" class="rowmajor vt">
+  <tr>
+    <th width="1%">Name:</th>
+    <td>
+      [if-any allow_edit]
+        [if-any component_def.parent_path][component_def.parent_path]&gt;[end]
+        <input id="leaf_name" name="leaf_name" value="[initial_leaf_name]" size="30" class="acob">
+        <span id="leafnamefeedback" class="fielderror" style="margin-left:1em">
+          [if-any errors.leaf_name][errors.leaf_name][end]
+        </span>
+      [else]
+        [component_def.path]
+      [end]
+    </td>
+  </tr>
+
+  <tr>
+    <th>Description:</th>
+    <td>
+      [if-any allow_edit]
+        <textarea name="docstring" rows="4" cols="75">[initial_docstring]</textarea>
+      [else]
+        [component_def.docstring]
+      [end]
+    </td>
+  </tr>
+
+  <tr>
+    <th>Admins:</th>
+    <td>
+      [if-any allow_edit]
+        <textarea id="member_admins" name="admins" rows="3" cols="75">[for initial_admins][initial_admins], [end]</textarea>
+        <span id="memberadminsfeedback" class="fielderror" style="margin-left:1em">
+          [if-any errors.member_admins][errors.member_admins][end]
+        </span>
+      [else]
+        [for component_def.admins]
+          <div>[include "../framework/user-link.ezt" component_def.admins]</div>
+        [end]
+      [end]
+    </td>
+  </tr>
+
+  <tr>
+    <th>Auto Cc:</th>
+    <td>
+      [if-any allow_edit]
+        <textarea id="member_cc" name="cc" rows="3" cols="75">[for initial_cc][initial_cc], [end]</textarea>
+        <span id="memberccfeedback" class="fielderror" style="margin-left:1em">
+          [if-any errors.member_cc][errors.member_cc][end]
+        </span>
+      [else]
+        [for component_def.cc]
+          <div>[include "../framework/user-link.ezt" component_def.cc]</div>
+        [end]
+      [end]
+    </td>
+  </tr>
+
+  <tr>
+    <th>Add Labels:</th>
+    <td>
+      [if-any allow_edit]
+        <textarea id="labels" name="labels" rows="3" cols="75">[for initial_labels][initial_labels], [end]</textarea>
+        <span id="labelsfeedback" class="fielderror" style="margin-left:1em">
+          [if-any errors.labels][errors.labels][end]
+        </span>
+      [else]
+        [for component_def.labels]
+          <div>[component_def.labels]</div>
+        [end]
+      [end]
+    </td>
+  </tr>
+
+  <tr>
+    <th>Deprecated:</th>
+    <td>
+        <input type="checkbox" id="deprecated" name="deprecated" [if-any initial_deprecated]checked="checked"[end]
+               [if-any allow_edit][else]disabled[end]>
+    </td>
+  </tr>
+
+  <tr>
+    <td></td>
+    <td>
+      [if-any allow_edit]
+        <div>
+          <span style="float:left;">
+            <input type="submit" name="submit" id="submit_btn" value="Submit changes">
+            <input type="submit" class="secondary" name="deletecomponent" value="Delete component"
+                   [if-any allow_delete][else]disabled[end]
+                   id="deletecomponent">
+          </span>
+          <span style="float:right;">
+            <a href="/p/[projectname]/components/create?component=[component_def.path]">Create new subcomponent</a>
+          </span>
+          <div style="clear:both;"></div>
+        </div>
+        [if-any allow_delete][else]
+          <br/><br/>
+          <b>Note:</b>
+          [if-any subcomponents]
+            <br/>
+            Can not delete this component because it has the following subcomponents:<br/>
+            [for subcomponents]<div style="margin-left:1em">[subcomponents.path]</div>[end]
+          [end]
+          [if-any templates]
+            <br/>
+            Can not delete this component because it is listed in the following templates:<br/>
+            [for templates]<div style="margin-left:1em">[templates.name]</div>[end]
+          [end]
+        [end]
+      [end]
+    </td>
+  </tr>
+
+</table>
+</form>
+
+[if-any allow_edit]
+<script type="text/javascript" nonce="[nonce]">
+runOnLoad(function() {
+  function checkSubmit() {
+    _checkLeafName('[format "js"][projectname][end]', '[format "js"][component_def.parent_path][end]', '[format "js"][component_def.leaf_name][end]', CS_env.token);
+  }
+  setInterval(checkSubmit, 700);
+
+  if ($("deletecomponent")) {
+    $("deletecomponent").addEventListener("click", function(event) {
+        if (!confirm("Are you sure you want to delete [component_def.path]?\nThis operation cannot be undone."))
+          event.preventDefault();
+     });
+  }
+
+  var acobElements = document.getElementsByClassName("acob");
+  for (var i = 0; i < acobElements.length; ++i) {
+     var el = acobElements[[]i];
+     el.addEventListener("focus", function(event) {
+         _acrob(null);
+         _acof(event);
+     });
+  }
+});
+</script>
+[end]
+
+
+[include "../framework/footer.ezt"]
diff --git a/templates/tracker/field-create-page.ezt b/templates/tracker/field-create-page.ezt
new file mode 100644
index 0000000..9b0bc11
--- /dev/null
+++ b/templates/tracker/field-create-page.ezt
@@ -0,0 +1,402 @@
+[define title]Add a Field[end]
+[define category_css]css/ph_detail.css[end]
+[include "../framework/header.ezt" "showtabs"]
+
+<a href="/p/[projectname]/adminLabels">&lsaquo; Back to field list</a><br><br>
+
+
+<h4>Add a custom field</h4>
+
+<form action="create.do" method="POST">
+<input type="hidden" name="token" value="[form_token]">
+
+<table cellspacing="8" class="rowmajor vt">
+  <tr>
+    <th width="1%">Name:</th>
+    <td>
+      <input id="fieldname" name="name" size="30" value="[initial_field_name]" class="acob">
+      <span id="fieldnamefeedback" class="fielderror" style="margin-left:1em">
+          [if-any errors.field_name][errors.field_name][end]
+      </span>
+    </td>
+  </tr>
+
+  <tr>
+    <th>Description:</th>
+    <td>
+      <textarea name="docstring" rows="4" cols="75">[initial_field_docstring]</textarea>
+    </td>
+  </tr>
+
+  <tr>
+    <th>Type:</th>
+    <td>
+      <select id="field_type" name="field_type">
+        <option value="enum_type" [is initial_type "enum_type"]selected="selected"[end]>Enum</option>
+        <option value="int_type" [is initial_type "int_type"]selected="selected"[end]>Integer</option>
+        <option value="str_type" [is initial_type "str_type"]selected="selected"[end]>String</option>
+        <option value="user_type" [is initial_type "user_type"]selected="selected"[end]>User</option>
+        <option value="date_type" [is initial_type "date_type"]selected="selected"[end]>Date</option>
+        <option value="url_type" [is initial_type "url_type"]selected="selected"[end]>URL</option>
+        <option value="approval_type" [is initial_type "approval_type"]selected="selected"[end]>Approval</option>
+      </select>
+    </td>
+  </tr>
+
+  <tr class="js-make_phase_subfield">
+    <th>Issue Gate field:</th>
+    <td>
+      <input id="phase_input" type="checkbox" name="is_phase_field" class="acob"
+             [if-any initial_is_phase_field]checked="checked"[end]>
+      <label for="phase_input">This field can only belong to issue gates.</label>
+    </td>
+  </tr>
+
+  [if-any approval_names]
+  <tr class="js-make_approval_subfield">
+    <th>Parent Approval:</th>
+    <td>
+      <select id="parent_input" name="parent_approval_name">
+        <option value="" [is initial_parent_approval_name ""]selected[end]>Not an approval's subfield</option>
+        [for approval_names]
+          <option value="[approval_names]"
+                  [is initial_parent_approval_name approval_names]selected[end]
+                  >[approval_names]</option>
+        [end]
+      </select>
+    </td>
+  </tr>
+  [end]
+
+  [# TODO(jojwang): monorail:3241, evaluate how to use applicable/importance for approval subfields]
+  <tr id="applicable_row">
+    <th>Applicable:</th>
+    <td>When issue type is:
+      <select id="applicable_type" name="applicable_type">
+        <option value="" [is initial_applicable_type ""]selected="selected"[end]>Anything</option>
+        <option disabled="disabled">----</option>
+        [for well_known_issue_types]
+          <option value="[well_known_issue_types]" [is initial_applicable_type well_known_issue_types]selected="selected"[end]>[well_known_issue_types]</option>
+        [end]
+      </select>
+      [# TODO(jrobbins): AND with free-form applicability predicate.]
+    </td>
+  </tr>
+
+  <tr id="importance_row">
+    <th>Importance:</th>
+    <td>
+      <select id="importance" name="importance">
+        <option value="required" [is initial_importance "required"]selected[end]>Required when applicable</option>
+        <option value="normal" [is initial_importance "normal"]selected[end]>Offered when applicable</option>
+        <option value="niche" [is initial_importance "niche"]selected[end]>Under "Show all fields" when applicable</option>
+      </select>
+    </td>
+  </tr>
+
+  <tr id="multi_row">
+    <th>Multivalued:</th>
+    <td>
+      <input type="checkbox" name="is_multivalued" class="acob"
+             [if-any initial_is_multivalued]checked="checked"[end]>
+    </td>
+  </tr>
+
+  <tr id="choices_row" style="display:none">
+    <th>Choices:</th>
+    <td>
+      <textarea id="choices" name="choices" rows="10" cols="75" style="tab-size:12"
+                >[initial_choices]</textarea>
+    </td>
+  </tr>
+
+  <tr id="int_row" style="display:none">
+    <th>Validation:</th>
+    <td>
+      Min value: <input type="number" name="min_value" style="text-align:right; width: 4em">
+      Max value: <input type="number" name="max_value" style="text-align:right; width: 4em"><br>
+      <span class="fielderror" style="margin-left: 1em">
+          [if-any errors.min_value][errors.min_value][end]</span><br>
+    </td>
+  </tr>
+
+  <tr id="str_row" style="display:none">
+    <th>Validation:</th>
+    <td>
+      Regex: <input type="text" name="regex" size="30"><br>
+    </td>
+  </tr>
+
+  <tr id="user_row" style="display:none">
+    <th>Validation:</th>
+    <td>
+      <input type="checkbox" name="needs_member" id="needs_member" class="acob"
+             [if-any initial_needs_member]checked[end]>
+      <label for="needs_member">User must be a project member</label><br>
+      <span id="needs_perm_span" style="margin-left:1em">
+        Required permission:
+        <input type="text" name="needs_perm" id="needs_perm" size="20"
+               value="[initial_needs_perm]" class="acob">
+      </span><br>
+    </td>
+  </tr>
+  <tr id="user_row2" style="display:none">
+    <th>Permissions:</th>
+    <td>
+      The users named in this field is granted this permission on this issue:<br>
+      [# TODO(jrobbins): one-click way to specify View vs. EditIssue vs. any custom perm.]
+      <input type="text" name="grants_perm" id="grants_perm" class="acob"
+             size="20" value="[initial_grants_perm]" autocomplete="off">
+    </td>
+  </tr>
+  <tr id="user_row3" style="display:none">
+    <th>Notification:</th>
+    <td>
+      The users named in this field will be notified via email whenever:<br>
+      <select name="notify_on">
+        <option value="never" [is initial_notify_on "0"]selected="selected"[end]
+                >No notifications</option>
+        <option value="any_comment" [is initial_notify_on "1"]selected="selected"[end]
+                >Any change or comment is added</option>
+      </select>
+    </td>
+  </tr>
+
+  <tr id="date_row" style="display:none">
+    <th>Action:</th>
+    <td>
+      When this date arrives:
+      <select name="date_action">
+        <option value="no_action" [is initial_date_action "no_action"]selected="selected"[end]
+                >No action</option>
+        [# TODO(jrobbins): owner-only option.]
+        <option value="ping_participants" [is initial_date_action "ping_participants"]selected="selected"[end]
+                >Post a "ping" comment and notify all issue participants</option>
+      </select>
+    </td>
+  </tr>
+
+  <tr id="approval_row" style="display:none">
+    <th>Approvers:</th>
+    <td>
+      <input id="member_approvers" name="approver_names" size="75" value="[initial_approvers]"
+          autocomplete="off">
+      <span class="fielderror" style="margin-left:1em">
+        [if-any errors.approvers][errors.approvers][end]
+      </span>
+    </td>
+  </tr>
+
+  <tr id="approval_row2" style="display:none">
+    <th>Survey:</th>
+    <td>
+      Any information feature owners need to provide for the approval team should be requested here.
+      <textarea name="survey" rows="4" cols="75">[initial_survey]</textarea>
+    </td>
+  </tr>
+
+  <tr>
+    <th>Admins:</th>
+    <td>
+      <input id="member_admins" name="admin_names" size="75" value="[initial_admins]"
+             autocomplete="off" class="acob">
+      <span class="fielderror" style="margin-left:1em">
+          [if-any errors.field_admins][errors.field_admins][end]
+      </span>
+    </td>
+  </tr>
+
+  <tr id="field_restriction">
+    <th>Restriction
+      <i id="editors_tooltip" class="material-icons inline-icon" style="font-size:14px; vertical-align: text-bottom"
+        title="Project owners and field admins can always edit the values of a custom field.">
+      info_outline</i> :
+    </th>
+    <td style="display:flex; align-items:center">
+      <input id="editors_checkbox" type="checkbox" name="is_restricted_field" class="acob"
+             [if-any initial_is_restricted_field]checked="checked"[end]>
+      Restrict users that can edit values of this custom field.
+    </td>
+  </tr>
+  <tr id="editors_input" style="display:none">
+    <th>Editors:</th>
+    <td>
+      <input id="member_editors" name="editor_names" size="75" value="[initial_editors]"
+             autocomplete="off" class="acob" disabled>
+      <span class="fielderror" style="margin-left:1em">
+          [if-any errors.field_editors][errors.field_editors][end]
+      </span>
+    </td>
+  </tr>
+
+  <tr>
+    <td></td>
+    <td>
+      <input id="submit_btn" type="submit" name="submit" value="Create field">
+    </td>
+  </tr>
+
+</table>
+</form>
+
+<script type="text/javascript" nonce="[nonce]">
+runOnLoad(function() {
+  var submit = document.getElementById('submit_btn');
+  submit.disabled = 'disabled';
+  var fieldname = document.getElementById('fieldname');
+  var oldName = '';
+  fieldname.focus();
+
+  var fieldNameRE = /^[[]a-z]([[]-_]?[[]a-z0-9])*$/i;
+
+  function checkFieldName() {
+    name = fieldname.value;
+    if (name != oldName) {
+      oldName = name;
+      feedback = document.getElementById('fieldnamefeedback');
+      submit.disabled = 'disabled';
+      if (name == '') {
+        feedback.textContent = 'Please choose a field name';
+      } else if (!fieldNameRE.test(name)) {
+        feedback.textContent = 'Invalid field name';
+      } else if (name.length > 30) {
+        feedback.textContent = 'Field name is too long';
+      } else {
+        _checkFieldNameOnServer('[projectname]', name, CS_env.token);
+      }
+    }
+  }
+
+  setInterval(checkFieldName, 700);
+
+  function updateForm(new_type) {
+    let choices_row = document.getElementById('choices_row');
+    choices_row.style.display = (new_type == 'enum_type') ? '' : 'none';
+
+    // Approval fields cannot be subfields of approvals.
+    let approval_subfield_display = (new_type == 'approval_type') ? 'none' : '';
+    let approval_subfield_rows = document.getElementsByClassName('js-make_approval_subfield');
+    Array.prototype.forEach.call(approval_subfield_rows, row => {
+      row.style.display = approval_subfield_display;
+    });
+
+    // Enum and Approval fields cannot be gate subfields.
+    let gate_subfield_display = (new_type == 'enum_type' || new_type == 'approval_type') ? 'none': '';
+    let phase_subfield_rows = document.getElementsByClassName('js-make_phase_subfield');
+    Array.prototype.forEach.call(phase_subfield_rows, row => {
+      row.style.display = gate_subfield_display;
+    });
+
+    // Prevent users from making a field a Gate and Approval subfield.
+    if ($('parent_input')) {
+      let phase_input = $('phase_input');
+      let parent_input = $('parent_input');
+      parent_input.addEventListener('change', () => {
+        if (parent_input.value === '') {
+          phase_input.disabled = false;
+        } else {
+          phase_input.disabled = true;
+        }
+      });
+      phase_input.addEventListener('change', () => {
+        if (phase_input.checked) {
+          parent_input.disabled = true;
+        } else {
+          parent_input.disabled = false;
+        }
+      });
+    };
+
+    let int_row = document.getElementById('int_row');
+    int_row.style.display = (new_type == 'int_type') ? '' : 'none';
+
+    let str_row = document.getElementById('str_row');
+    str_row.style.display = (new_type == 'str_type') ? '' : 'none';
+
+    let user_row_display = (new_type == 'user_type') ? '' : 'none';
+    document.getElementById('user_row').style.display = user_row_display;
+    document.getElementById('user_row2').style.display = user_row_display;
+    document.getElementById('user_row3').style.display = user_row_display;
+
+    let date_row_display = (new_type == 'date_type') ? '' : 'none';
+    document.getElementById('date_row').style.display = date_row_display;
+
+    let approval_row_display = (new_type == 'approval_type') ? '' : 'none';
+    let approval_row_hide = (new_type == 'approval_type') ? 'none' : '';
+    let new_type_is_approval = (new_type == 'approval_type');
+    document.getElementById(
+        'multi_row').style.display = approval_row_hide;
+    document.getElementById(
+        'importance_row').style.display = approval_row_hide;
+    document.getElementById(
+        'applicable_row').style.display = approval_row_hide;
+    document.getElementById(
+        'field_restriction').style.display = approval_row_hide;
+    if (new_type_is_approval) {
+      document.getElementById('editors_input').style.display = 'none';
+    } else {
+      if (document.getElementById('editors_checkbox').checked) {
+        document.getElementById('editors_input').style.display = '';
+      } else {
+        document.getElementById('editors_input').style.display = 'none';
+      }
+    }
+    document.getElementById(
+        'editors_checkbox').disabled = new_type_is_approval;
+    document.getElementById(
+        'member_editors').disabled = new_type_is_approval || !document.getElementById('editors_checkbox').checked;
+    document.getElementById('approval_row').style.display = approval_row_display;
+    document.getElementById('approval_row2').style.display = approval_row_display;
+  }
+
+  let type_select = document.getElementById('field_type');
+  updateForm(type_select.value);
+  type_select.addEventListener("change", function() {
+       updateForm(type_select.value);
+  });
+
+  let needs_perm_span = document.getElementById('needs_perm_span');
+  let needs_perm = document.getElementById('needs_perm');
+  function enableNeedsPerm(enable) {
+    needs_perm_span.style.color = enable ? 'inherit' : '#999';
+    needs_perm.disabled = enable ? '' : 'disabled';
+    if (!enable) needs_perm.value = '';
+  }
+  enableNeedsPerm(false);
+
+  //Enable editors input only when restricting the field.
+  document.getElementById('editors_checkbox').onchange = function() {
+    let member_editors = document.getElementById('member_editors');
+    let editors_input = document.getElementById('editors_input');
+    if (this.checked) {
+      editors_input.style.display = '';
+    } else {
+      editors_input.style.display = 'none';
+    }
+    member_editors.disabled = !this.checked;
+  };
+
+  let needs_member = document.getElementById("needs_member");
+  if (needs_member)
+    needs_member.addEventListener("change", function() {
+       enableNeedsPerm(needs_member.checked);
+    });
+
+  let acobElements = document.getElementsByClassName("acob");
+  for (let i = 0; i < acobElements.length; ++i) {
+     let el = acobElements[[]i];
+     el.addEventListener("focus", function(event) {
+         _acrob(null);
+         _acof(event);
+     });
+  }
+
+  $('member_approvers').addEventListener("focus", function(event) {
+    _acof(event);
+  });
+
+});
+</script>
+
+
+[include "../framework/footer.ezt"]
diff --git a/templates/tracker/field-detail-page.ezt b/templates/tracker/field-detail-page.ezt
new file mode 100644
index 0000000..75c09fa
--- /dev/null
+++ b/templates/tracker/field-detail-page.ezt
@@ -0,0 +1,421 @@
+[define title]Field [field_def.field_name][end]
+[define category_css]css/ph_detail.css[end]
+[include "../framework/header.ezt" "showtabs"]
+
+<a href="/p/[projectname]/adminLabels">&lsaquo; Back to field list</a><br><br>
+
+
+<h4>Custom field</h4>
+
+<form action="detail.do" method="POST">
+<input type="hidden" name="token" value="[form_token]">
+<input type="hidden" name="field" value="[field_def.field_name]">
+
+<table cellspacing="8" class="rowmajor vt">
+  <tr>
+    <th width="1%">Name:</th>
+    <td>
+      [if-any uneditable_name]
+        <input type="hidden" name="name" value="[field_def.field_name]">
+        [field_def.field_name]
+      [else][if-any allow_edit]
+        <input name="name" value="[field_def.field_name]" size="30" class="acob">
+      [else]
+        [field_def.field_name]
+      [end][end]
+    </td>
+  </tr>
+
+  <tr>
+    <th>Description:</th>
+    <td>
+      [if-any allow_edit]
+        <textarea name="docstring" rows="4" cols="75">[field_def.docstring]</textarea>
+      [else]
+        [field_def.docstring]
+      [end]
+    </td>
+  </tr>
+
+  <tr>
+    <th>Type:</th>
+    [# TODO(jrobbins): make field types editable someday.]
+    <td>[field_def.type_name]</td>
+  </tr>
+
+  [is field_def.type_name "APPROVAL_TYPE"]
+    <tr>
+      <th>Approvers:</th>
+      <td>
+        [if-any allow_edit]
+          <input id="member_approvers" name="approver_names" size="75" value="[initial_approvers]"
+            autocomplete="off">
+          <span class="fielderror" style="margin-left:1em">
+          [if-any errors.approvers][errors.approvers][end]
+          </span>
+        [else]
+          [for field_def.approvers]
+            <div>[include "../framework/user-link.ezt" field_def.approvers]</div>
+          [end]
+        [end]
+      </td>
+    </tr>
+    <tr>
+      <th>Survey:</th>
+      <td>
+        [if-any allow_edit]
+          <textarea name="survey" rows="4" cols="75">[field_def.survey]</textarea>
+        [else]
+          <table cellspacing="4" cellpadding="0" style="padding: 2px; border:2px solid #eee">
+            [for field_def.survey_questions]
+              <tr><td>[field_def.survey_questions]</td></tr>
+            [end]
+          </table>
+        [end]
+      </td>
+    </tr>
+
+    [if-any approval_subfields]
+      <tr>
+        <th>Subfields:</th>
+        <td>
+          [for approval_subfields]
+            <div><a href="/p/[projectname]/fields/detail?field=[approval_subfields.field_name]">
+              [approval_subfields.field_name]
+            </a></div>
+          [end]
+        </td>
+      </tr>
+    [end]
+  [else]
+
+    <tr>
+      <th>Issue Gate field:</th>
+      <td>
+        [if-any field_def.is_phase_field]Yes[else]No[end]
+      </td>
+    </tr>
+
+    [is field_def.field_name "Type"][else]
+    <tr>
+      <th>Applicable:</th>
+      <td>When issue type is:
+        [if-any allow_edit]
+         [define oddball_applicability]Yes[end]
+          <select id="applicable_type" name="applicable_type">
+            <option value=""
+              [is initial_applicable_type ""]
+                selected="selected"
+                [define oddball_applicability]No[end]
+              [end]
+            >Anything</option>
+            <option disabled="disabled">----</option>
+            [for well_known_issue_types]
+              <option value="[well_known_issue_types]"
+                [is initial_applicable_type well_known_issue_types]
+                  selected="selected"
+                  [define oddball_applicability]No[end]
+                [end]
+              >[well_known_issue_types]</option>
+            [end]
+            [# If an oddball type was used, keep it.]
+            [is oddball_applicability "Yes"]
+              <option value="[initial_applicable_type]" selected="selected"
+              >[initial_applicable_type]</option>
+            [end]
+          </select>
+        [else]
+          [initial_applicable_type]
+        [end]
+        [# TODO(jrobbins): editable applicable_predicate.]
+      </td>
+    </tr>
+    [end]
+
+    <tr>
+      <th>Importance:</th>
+      <td>
+        [if-any allow_edit]
+          <select id="importance" name="importance">
+            <option value="required" [is field_def.importance "required"]selected[end]>Required when applicable</option>
+            <option value="normal" [is field_def.importance "normal"]selected[end]>Offered when applicable</option>
+            <option value="niche" [is field_def.importance "niche"]selected[end]>Under "Show all fields" when applicable</option>
+          </select>
+        [else]
+          [is field_def.importance "required"]Required when applicable[end]
+          [is field_def.importance "normal"]Offered when applicable[end]
+          [is field_def.importance "niche"]Under "Show all fields" when applicable[end]
+        [end]
+      </td>
+    </tr>
+
+    <tr>
+      <th>Multivalued:</th>
+      <td>
+        [if-any allow_edit]
+          <input type="checkbox" name="is_multivalued" class="acob"
+                 [if-any field_def.is_multivalued_bool]checked="checked"[end]>
+        [else]
+          [if-any field_def.is_multivalued_bool]Yes[else]No[end]
+        [end]
+      </td>
+    </tr>
+  [end]
+
+  [# TODO(jrobbins): dynamically display validation info as field type is edited.]
+  [is field_def.type_name "ENUM_TYPE"]
+    <tr>
+      <th>Choices:</th>
+      <td>
+        [if-any allow_edit]
+          <textarea name="choices" rows="10" cols="75" style="tab-size:18" [if-any allow_edit][else]disabled="disabled"[end]
+          >[initial_choices]</textarea>
+        [else]
+          <table cellspacing="4" cellpadding="0" style="padding: 2px; border:2px solid #eee">
+            [for field_def.choices]
+              <tr>
+                <td>[field_def.choices.name]</td>
+                <td>[if-any field_def.choices.docstring]= [end][field_def.choices.docstring]</td>
+              </tr>
+            [end]
+          </table>
+        [end]
+      </td>
+    </tr>
+  [end]
+
+  [is field_def.type_name "INT_TYPE"]
+    <tr id="int_row">
+      <th>Validation:</th>
+      <td>
+        Min value:
+        <input type="number" name="min_value" style="text-align:right; width: 4em"
+               value="[field_def.min_value]" class="acob"
+               [if-any allow_edit][else]disabled="disabled"[end]>
+
+        Max value:
+        <input type="number" name="max_value" style="text-align:right; width: 4em"
+               value="[field_def.max_value]" class="acob"
+               [if-any allow_edit][else]disabled="disabled"[end]>
+        <span class="fielderror" style="margin-left:1em">
+          [if-any errors.min_value][errors.min_value][end]</span><br>
+      </td>
+    </tr>
+  [end]
+
+  [is field_def.type_name "STR_TYPE"]
+    <tr id="str_row">
+      <th>Validation:</th>
+      <td>
+        Regex: <input type="text" name="regex" size="30" value="[field_def.regex]" class="acob"><br>
+        <span class="fielderror" style="margin-left:1em"
+            >[if-any errors.regex][errors.regex][end]</span>
+      </td>
+    </tr>
+  [end]
+
+  [is field_def.type_name "USER_TYPE"]
+    <tr id="user_row">
+      <th>Validation:</th>
+      <td>
+        <input type="checkbox" name="needs_member" id="needs_member" class="acob"
+               [if-any allow_edit][else]disabled="disabled"[end]
+               [if-any field_def.needs_member_bool]checked="checked"[end]>
+        <label for="needs_member">User must be a project member</label><br>
+        <span id="needs_perm_span" style="margin-left:1em">Required permission:
+          <input type="text" name="needs_perm" id="needs_perm" size="20"
+                 value="[field_def.needs_perm]" autocomplete="off" class="acob"
+                 [if-any allow_edit][else]disabled="disabled"[end]></span><br>
+      </td>
+    </tr>
+    <tr id="user_row2">
+      <th>Permissions:</th>
+      <td>
+        The users named in this field is granted this permission on this issue:<br>
+        [# TODO(jrobbins): one-click way to specify View vs. EditIssue vs. any custom perm.]
+        <input type="text" name="grants_perm" id="grants_perm" class="acob"
+               size="20" value="[field_def.grants_perm]" autocomplete="off"
+               [if-any allow_edit][else]disabled[end]>
+      </td>
+    </tr>
+    <tr id="user_row3">
+      <th>Notification:</th>
+      <td>
+        The users named in this field will be notified via email whenever:<br>
+        <select name="notify_on" [if-any allow_edit][else]disabled[end]
+                class="acrob">
+          <option value="never" [is field_def.notify_on "0"]selected="selected"[end]
+                  >No notifications</option>
+          <option value="any_comment" [is field_def.notify_on "1"]selected="selected"[end]
+                  >Any change or comment is added</option>
+        </select>
+      </td>
+    </tr>
+  [end]
+
+  [is field_def.type_name "DATE_TYPE"]
+    <tr id="date_row">
+      <th>Action:</th>
+      <td>
+        [if-any allow_edit]
+          <select name="date_action">
+            <option value="no_action" [is field_def.date_action_str "no_action"]selected="selected"[end]
+                    >No action</option>
+            [# TODO(jrobbins): owner-only option.]
+            <option value="ping_participants" [is field_def.date_action_str "ping_participants"]selected="selected"[end]
+                    >Post a comment and notify all issue participants</option>
+          </select>
+        [else]
+          [is field_def.date_action_str "no_action"]No action[end]
+          [# TODO(jrobbins): owner-only option.]
+          [is field_def.date_action_str "ping_participants"]Post a comment and notify all issue participants[end]
+        [end]
+      </td>
+    </tr>
+  [end]
+
+  [if-any field_def.is_approval_subfield]
+    <tr>
+      <th>Parent Approval:</th>
+      <td>
+        <a href="/p/[projectname]/fields/detail?field=[field_def.parent_approval_name]">
+          [field_def.parent_approval_name]
+        </a>
+      </td>
+    </tr>
+  [end]
+
+  <th>Admins:</th>
+    <td>
+      [if-any allow_edit]
+        <input id="member_admins" name="admin_names" size="75" value="[initial_admins]"
+               autocomplete="off" class="acob">
+        <span class="fielderror" style="margin-left:1em">
+            [if-any errors.field_admins][errors.field_admins][end]
+        </span>
+      [else]
+        [for field_def.admins]
+          <div>[include "../framework/user-link.ezt" field_def.admins]</div>
+        [end]
+      [end]
+    </td>
+  </tr>
+
+  [is field_def.type_name "APPROVAL_TYPE"][else]
+
+  <tr id="editors_restriction">
+    <th>Restriction
+      <i id="editors_tooltip" class="material-icons inline-icon" style="font-size:14px; vertical-align: text-bottom"
+        title="Project owners and field admins can always edit the values of a custom field.">
+      info_outline</i> :
+    </th>
+    <td style="display:flex; align-items:center">
+      [if-any allow_edit]
+        <input id="editors_checkbox" type="checkbox" name="is_restricted_field" class="acob"
+               [if-any field_def.is_restricted_field]checked="checked"[end]>
+        Restrict users that can edit values of this custom field.
+      [else]
+        [if-any field_def.is_restricted_field]Yes[else]No[end]
+      [end]
+    </td>
+  </tr>
+  <tr id="editors_input"
+      [if-any field_def.is_restricted_field][else]style="display:none"[end]>
+    <th>Editors:</th>
+    <td>
+      [if-any allow_edit]
+        <input id="member_editors" name="editor_names" size="75" value="[initial_editors]"
+               autocomplete="off" class="acob"
+               [if-any field_def.is_restricted_field][else]disabled[end]>
+        <span class="fielderror" style="margin-left:1em">
+            [if-any errors.field_editors][errors.field_editors][end]
+        </span>
+      [else]
+        [for field_def.editors]
+          <div>[include "../framework/user-link.ezt" field_def.editors]</div>
+        [end]
+      [end]
+    </td>
+  </tr>
+
+  [end]
+
+  <tr>
+    <td></td>
+    <td>
+      [if-any allow_edit]
+        <input type="submit" name="submit" value="Save changes">
+        <input type="submit" class="secondary" name="deletefield" value="Delete Field"
+               id="deletefield">
+      [end]
+    </td>
+  </tr>
+
+</table>
+</form>
+
+
+<script type="text/javascript" nonce="[nonce]">
+runOnLoad(function() {
+  var needs_perm_span = document.getElementById('needs_perm_span');
+  var needs_perm = document.getElementById('needs_perm');
+  var needs_member = document.getElementById('needs_member');
+  function enableNeedsPerm(enable) {
+    needs_perm_span.style.color = enable ? 'inherit' : '#999';
+    needs_perm.disabled = enable ? '' : 'disabled';
+    if (!enable) needs_perm.value = '';
+  }
+  [if-any allow_edit]
+    if (needs_perm)
+      enableNeedsPerm(needs_member.checked);
+  [end]
+
+  if ($("deletefield")) {
+    $("deletefield").addEventListener("click", function(event) {
+        var msg = ("Are you sure you want to delete [field_def.field_name]?\n" +
+                   "This operation cannot be undone. " +
+                   "[if-any approval_subfields]\nAll subfields will also be deleted.[end]" +
+                   "[is field_def.type_name "ENUM_TYPE"]\nEnum values will be retained on issues as labels.[end]");
+        if (!confirm(msg))
+          event.preventDefault();
+     });
+  }
+
+  [is field_def.type_name "APPROVAL_TYPE"][else]
+  //Enable editors input only when restricting the field.
+  document.getElementById('editors_checkbox').onchange = function() {
+    var member_editors = document.getElementById('member_editors');
+    var editors_input = document.getElementById('editors_input');
+    if (this.checked) {
+      editors_input.style.display = '';
+    } else {
+      editors_input.style.display = 'none';
+    }
+    member_editors.disabled = !this.checked;
+  };
+  [end]
+
+  var acobElements = document.getElementsByClassName("acob");
+  for (var i = 0; i < acobElements.length; ++i) {
+     var el = acobElements[[]i];
+     el.addEventListener("focus", function(event) {
+         _acrob(null);
+         _acof(event);
+     });
+  }
+
+  [is field_def.type_name "APPROVAL_TYPE"]
+  $('member_approvers').addEventListener("focus", function(event) {
+    _acof(event);
+  });
+  [end]
+
+  if ($("needs_member")) {
+    $("needs_member").addEventListener("change", function(event) {
+       enableNeedsPerm($("needs_member").checked);
+    });
+  }
+});
+</script>
+
+[include "../framework/footer.ezt"]
diff --git a/templates/tracker/field-value-multi-date.ezt b/templates/tracker/field-value-multi-date.ezt
new file mode 100644
index 0000000..2e5e657
--- /dev/null
+++ b/templates/tracker/field-value-multi-date.ezt
@@ -0,0 +1,41 @@
+[if-any fields.values]
+  [for fields.values]
+    <input type="date" name="custom_[fields.field_id][is arg2 ""][else]_[arg2][end]" value="[fields.values.val]"
+           [if-any fields.field_def.min_value]min="[fields.field_def.min_value]"[end]
+           [if-any fields.field_def.max_value]max="[fields.field_def.max_value]"[end]
+           [if-index fields.values first]
+             [is arg0 "hidden"][else]
+               [if-any arg1]required="required"[end]
+             [end]
+           [end]
+           style="text-align:right; width:12em" class="multivalued customfield"
+           aria-labelledby="[fields.field_id]_label">
+    [if-index fields.values first][else]
+      <u class="removeMultiFieldValueWidget">X</u>
+    [end]
+    [if-index fields.values last]
+      <u class="addMultiFieldValueWidget" data-field-id="[fields.field_id]" data-field-type="date"
+         data-validate-1="[fields.field_def.min_value]" data-validate-2="[fields.field_def.max_value]"
+         data-phase-name="[arg2]"
+         >Add a value</u>
+    [end]
+  [end]
+[else]
+    <input type="date" name="custom_[fields.field_id][is arg2 ""][else]_[arg2][end]" value=""
+           [if-any fields.field_def.min_value]min="[fields.field_def.min_value]"[end]
+           [if-any fields.field_def.max_value]max="[fields.field_def.max_value]"[end]
+           [is arg0 "hidden"][else]
+             [if-any arg1]required="required"[end]
+           [end]
+           style="text-align:right; width:12em" class="multivalued customfield"
+           aria-labelledby="[fields.field_id]_label">
+    <u class="addMultiFieldValueWidget" data-field-id="[fields.field_id]" data-field-type="date"
+       data-validate-1="[fields.field_def.min_value]" data-validate-2="[fields.field_def.max_value]"
+       data-phase-name="[arg2]">Add a value</u>
+[end]
+
+[for fields.derived_values]
+  <input type="date" disabled="disabled" value="[fields.derived_values.val]"
+         style="font-style:italic; text-align:right; width:12em" class="multivalued"
+         aria-labelledby="[fields.field_id]_label">
+[end]
diff --git a/templates/tracker/field-value-multi-enum.ezt b/templates/tracker/field-value-multi-enum.ezt
new file mode 100644
index 0000000..004b5ac
--- /dev/null
+++ b/templates/tracker/field-value-multi-enum.ezt
@@ -0,0 +1,66 @@
+[for fields.field_def.choices]
+  [define checked]No[end]
+  [define derived]No[end]
+  [for fields.values]
+    [is fields.values.val fields.field_def.choices.name]
+      [define checked]Yes[end]
+    [end]
+  [end]
+  [for fields.derived_values]
+    [is fields.derived_values.val fields.field_def.choices.name]
+      [define checked]Yes[end]
+      [define derived]Yes[end]
+    [end]
+  [end]
+
+  <label id="[fields.field_id]_[fields.field_def.choices.name]_label" class="enum_checkbox"
+         title="[is derived "Yes"]derived: [end][fields.field_def.choices.name][if-any fields.field_def.choices.docstring]: [fields.field_def.choices.docstring][end]"
+         [is derived "Yes"]style="font-style:italic"[end]>
+    <input type="checkbox" name="custom_[fields.field_id][is arg2 ""][else]_[arg2][end]"
+           value="[fields.field_def.choices.name]"
+           id="[arg0]_custom_[fields.field_id]_[fields.field_def.choices.idx]"
+           [is checked "Yes"]checked="checked"[end] [is derived "Yes"]disabled="disabled"[end]
+           aria-labelledby="[fields.field_id]_label [fields.field_id]_[fields.field_def.choices.name]_label">
+      [fields.field_def.choices.name]
+  </label>
+
+[end]
+
+
+[# Also include any oddball values as plain text with an _X_ icon.]
+[for fields.values]
+  [define already_shown]No[end]
+  [for fields.field_def.choices]
+    [is fields.field_def.choices.name fields.values.val]
+      [define already_shown]Yes[end]
+    [end]
+  [end]
+  [is already_shown "No"]
+    <span class="enum_checkbox"
+          title="This is not a defined choice for this field"
+          id="span_[arg0]_oddball_[fields.values.idx]">
+      <a id="[arg0]_oddball_[fields.values.idx]" class="remove_oddball x_icon"></a>[fields.values.val]
+      [# Below hidden input contains the value of the field for tracker_helpers._ParseIssueRequestFields ]
+      <input type="text" class="labelinput" id="input_[arg0]_oddball_[fields.values.idx]" size="20" name="label"
+             value="[fields.field_name]-[fields.values.val]" hidden>
+    </span>
+  [end]
+[end]
+
+<script type="text/javascript" nonce="[nonce]">
+runOnLoad(function() {
+  var oddballAnchors = document.getElementsByClassName("remove_oddball");
+  for (var i = 0; i < oddballAnchors.length; ++i) {
+    var oddballAnchor = oddballAnchors[[]i];
+
+    oddballAnchor.addEventListener("click", function(event) {
+      var oddballSpan = $("span_" + this.id);
+      oddballSpan.style.display = "none";
+      var oddballInput = $("input_" + this.id);
+      oddballInput.value = "";
+      event.preventDefault();
+    });
+  }
+});
+</script>
+
diff --git a/templates/tracker/field-value-multi-int.ezt b/templates/tracker/field-value-multi-int.ezt
new file mode 100644
index 0000000..f17c2cf
--- /dev/null
+++ b/templates/tracker/field-value-multi-int.ezt
@@ -0,0 +1,41 @@
+[if-any fields.values]
+  [for fields.values]
+    <input type="number" name="custom_[fields.field_id][is arg2 ""][else]_[arg2][end]" value="[fields.values.val]"
+           [if-any fields.field_def.min_value]min="[fields.field_def.min_value]"[end]
+           [if-any fields.field_def.max_value]max="[fields.field_def.max_value]"[end]
+           [if-index fields.values first]
+             [is arg0 "hidden"][else]
+               [if-any arg1]required="required"[end]
+             [end]
+           [end]
+           style="text-align:right; width:12em" class="multivalued customfield"
+           aria-labelledby="[fields.field_id]_label">
+    [if-index fields.values first][else]
+      <u class="removeMultiFieldValueWidget">X</u>
+    [end]
+    [if-index fields.values last]
+      <u class="addMultiFieldValueWidget" data-field-id="[fields.field_id]" data-field-type="int"
+         data-validate-1="[fields.field_def.min_value]" data-validate-2="[fields.field_def.max_value]"
+         data-phase-name="[arg2]"
+         >Add a value</u>
+    [end]
+  [end]
+[else]
+    <input type="number" name="custom_[fields.field_id][is arg2 ""][else]_[arg2][end]" value=""
+           [if-any fields.field_def.min_value]min="[fields.field_def.min_value]"[end]
+           [if-any fields.field_def.max_value]max="[fields.field_def.max_value]"[end]
+           [is arg0 "hidden"][else]
+             [if-any arg1]required="required"[end]
+           [end]
+           style="text-align:right; width:12em" class="multivalued customfield"
+           aria-labelledby="[fields.field_id]_label">
+    <u class="addMultiFieldValueWidget" data-field-id="[fields.field_id]" data-field-type="int"
+       data-validate-1="[fields.field_def.min_value]" data-validate-2="[fields.field_def.max_value]"
+       data-phase-name="[arg2]">Add a value</u>
+[end]
+
+[for fields.derived_values]
+  <input type="number" disabled="disabled" value="[fields.derived_values.val]"
+         style="font-style:italic; text-align:right; width:12em" class="multivalued"
+         aria-labelledby="[fields.field_id]_label">
+[end]
diff --git a/templates/tracker/field-value-multi-str.ezt b/templates/tracker/field-value-multi-str.ezt
new file mode 100644
index 0000000..62c72d7
--- /dev/null
+++ b/templates/tracker/field-value-multi-str.ezt
@@ -0,0 +1,33 @@
+[if-any fields.values]
+  [for fields.values]
+    <input name="custom_[fields.field_id][is arg2 ""][else]_[arg2][end]" value="[fields.values.val]"
+           [# TODO(jrobbins): string validation]
+           [if-index fields.values first]
+             [is arg0 "hidden"][else]
+               [if-any arg1]required="required"[end]
+             [end]
+           [end]
+           style="width: 12em" class="multivalued customfield"
+           aria-labelledby="[fields.field_id]_label">
+    [if-index fields.values first][else]
+      <u class="removeMultiFieldValueWidget">X</u>
+    [end]
+    [if-index fields.values last]
+      <u class="addMultiFieldValueWidget" data-field-id="[fields.field_id]" data-field-type="str" data-phase-name="[arg2]">Add a value</u>
+    [end]
+  [end]
+[else]
+  <input name="custom_[fields.field_id][is arg2 ""][else]_[arg2][end]" value=""
+         [# TODO(jrobbins): string validation]
+         [is arg0 "hidden"][else]
+           [if-any arg1]required="required"[end]
+         [end]
+         style="width: 12em" class="multivalued customfield" aria-labelledby="[fields.field_id]_label">
+      <u class="addMultiFieldValueWidget"  data-field-id="[fields.field_id]" data-field-type="str" data-phase-name="[arg2]">Add a value</u>
+[end]
+
+[for fields.derived_values]
+  <input disabled="disabled" value="[fields.derived_values.val]"
+         style="font-style:italic" style="width: 12em" class="multivalued"
+         aria-labelledby="[fields.field_id]_label">
+[end]
diff --git a/templates/tracker/field-value-multi-url.ezt b/templates/tracker/field-value-multi-url.ezt
new file mode 100644
index 0000000..de8a3e1
--- /dev/null
+++ b/templates/tracker/field-value-multi-url.ezt
@@ -0,0 +1,29 @@
+[if-any fields.values]
+  [for fields.values]
+    <input type="text" name="custom_[fields.field_id][is arg2 ""][else]_[arg2][end]" value="[fields.values.val]"
+    [if-index fields.values first]
+      [is arg0 "hidden"][else]
+        [if-any arg1]required="required"[end]
+      [end]
+    [end]
+    style="width: 12em" class="multivalued customfield" aria-labelledby="[fields.field_id]_label">
+    [if-index fields.values first][else]
+      <u class="removeMultiFieldValueWidget">X</u>
+    [end]
+    [if-index fields.values last]
+      <u class="addMultiFieldValueWidget" data-field-id="[fields.field_id]" data-field-type="url" data-phase-name="[arg2]">Add a value</u>
+    [end]
+  [end]
+[else]
+  <input name="custom_[fields.field_id][is arg2 ""][else]_[arg2][end]" value=""
+         [is arg0 "hidden"][else]
+           [if-any arg1]required="required"[end]
+         [end]
+         style="width: 12em" class="multivalued customfield" aria-labelledby="[fields.field_id]_label">
+  <u class="addMultiFieldValueWidget" data-field-id="[fields.field_id]" data-field-type="url" data-phase-name="[arg2]">Add a value</u>
+[end]
+
+[for fields.derived_values]
+  <input disabled="disabled" value="[fields.derived_values.val]"
+         style="font-style:italic" style="width: 12em" class="multivalued" aria-labelledby="[fields.field_id]_label">
+[end]
\ No newline at end of file
diff --git a/templates/tracker/field-value-multi-user.ezt b/templates/tracker/field-value-multi-user.ezt
new file mode 100644
index 0000000..678cd79
--- /dev/null
+++ b/templates/tracker/field-value-multi-user.ezt
@@ -0,0 +1,32 @@
+[if-any fields.values]
+  [for fields.values]
+    <input type="text" name="custom_[fields.field_id][is arg2 ""][else]_[arg2][end]" value="[fields.values.val]"
+           [# TODO(jrobbins): include fields.min_value and fields.max_value attrs]
+           [if-index fields.values first]
+             [is arg0 "hidden"][else]
+               [if-any arg1]required="required"[end]
+             [end]
+           [end]
+           style="width:12em" class="multivalued userautocomplete customfield" autocomplete="off"
+           data-ac-type="owner" aria-labelledby="[fields.field_id]_label">
+        [if-index fields.values first][else]
+          <u class="removeMultiFieldValueWidget">X</u>
+        [end]
+        [if-index fields.values last]
+          <u class="addMultiFieldValueWidget" data-field-id="[fields.field_id]" data-field-type="user" data-phase-name="[arg2]">Add a value</u>
+        [end]
+  [end]
+[else]
+  <input type="text" name="custom_[fields.field_id][is arg2 ""][else]_[arg2][end]" value=""
+         [is arg0 "hidden"][else]
+           [if-any arg1]required="required"[end]
+         [end]
+         style="width:12em" class="multivalued userautocomplete customfield" autocomplete="off"
+         data-ac-type="owner" aria-labelledby="[fields.field_id]_label">
+    <u class="addMultiFieldValueWidget" data-field-id="[fields.field_id]" data-field-type="user" data-phase-name="[arg2]">Add a value</u>
+[end]
+
+[for fields.derived_values]
+  <input type="text" disabled="disabled" value="[fields.derived_values.val]"
+         style="width:12em" class="multivalued" aria-labelledby="[fields.field_id]_label">
+[end]
diff --git a/templates/tracker/field-value-single-date.ezt b/templates/tracker/field-value-single-date.ezt
new file mode 100644
index 0000000..d9f344a
--- /dev/null
+++ b/templates/tracker/field-value-single-date.ezt
@@ -0,0 +1,43 @@
+[# Even though this field definition says it is single-valued, the issue might have
+   multiple values if the field definition was previously multi-valued.  In such a situation
+   values other than the first value are shown read-only and must be explicitly removed
+   before the comment can be submitted. ]
+
+[# If the field has no explicit values, then show an empty form element.]
+[if-any fields.values][else]
+    <input type="date" name="custom_[fields.field_id][is arg2 ""][else]_[arg2][end]" id="[arg0]_custom_[fields.field_id]" value=""
+           [is arg0 "hidden"][else]
+             [if-any arg1] required="required"[end]
+           [end]
+           [if-any fields.field_def.min_value]min="[fields.field_def.min_value]"[end]
+           [if-any fields.field_def.max_value]max="[fields.field_def.max_value]"[end]
+           style="text-align:right; width:12em" class="multivalued customfield"
+           aria-labelledby="[fields.field_id]_label">
+[end]
+
+
+[for fields.values]
+  [if-index fields.values first]
+    <input type="date" name="custom_[fields.field_id][is arg2 ""][else]_[arg2][end]" value="[fields.values.val]"
+           [is arg0 "hidden"][else]
+             [if-any arg1]required="required"[end]
+           [end]
+           [if-any fields.field_def.min_value]min="[fields.field_def.min_value]"[end]
+           [if-any fields.field_def.max_value]max="[fields.field_def.max_value]"[end]
+           style="text-align:right; width:12em" class="multivalued customfield"
+           aria-labelledby="[fields.field_id]_label">
+  [else]
+    <span>
+      <input type="date" disabled="disabled" value="[fields.values.val]"
+             style="text-align:right; width: 12em" class="multivalued customfield"
+             aria-labelledby="[fields.field_id]_label">
+      <u class="removeMultiFieldValueWidget">X</u>
+    </span>
+  [end]
+[end]
+
+[for fields.derived_values]
+  <input type="date" disabled="disabled" value="[fields.derived_values.val]"
+         style="font-style:italic; text-align:right; width:12em" class="multivalued"
+	 aria-labelledby="[fields.field_id]_label">
+[end]
diff --git a/templates/tracker/field-value-single-enum.ezt b/templates/tracker/field-value-single-enum.ezt
new file mode 100644
index 0000000..eb575a6
--- /dev/null
+++ b/templates/tracker/field-value-single-enum.ezt
@@ -0,0 +1,81 @@
+[if-any fields.values fields.derived_values]
+
+  [# TODO(jrobbins): a better UX for undesired values would be to replace the current
+     --/value slect widget with a plain-text display of the value followed by an _X_
+     link to delete it.  There would be a hidden field with the value.  Validation would
+     fail in JS and on the server if each such _X_ had not already been clicked.]
+
+  [# There could be more than one if this field used to be multi-valued.]
+  [for fields.values]
+      <select name="custom_[fields.field_id][is arg2 ""][else]_[arg2][end]" id="[arg0]_custom_[fields.field_id]"
+              class="custom_field_value_menu" aria-labelledby="[fields.field_id]_label">
+          [define show_no_value_choice]No[end]
+          [# Non-required fields can have any value removed.]
+          [if-any fields.field_def.is_required_bool][else]
+            [define show_no_value_choice]Yes[end]
+          [end]
+          [# Formerly multi-valued fields need -- to narrow down to being singled valued.]
+          [if-index fields.values first][else]
+            [define show_no_value_choice]Yes[end]
+          [end]
+          [is show_no_value_choice "Yes"]
+            <option value="--"
+                    [is fields.values.val ""]selected="selected"[end]
+                    title="No value">--</option>
+          [end]
+
+          [define value_is_shown]No[end]
+          [for fields.field_def.choices]
+            [define show_choice]No[end]
+            [# Always show the current value]
+            [is fields.values.val fields.field_def.choices.name]
+              [define value_is_shown]Yes[end]
+              [define show_choice]Yes[end]
+            [end]
+            [# Formerly multi-valued fields extra values can ONLY be removed.]
+            [if-index fields.values first]
+              [define show_choice]Yes[end]
+            [end]
+            [is show_choice "Yes"]
+              <option value="[fields.field_def.choices.name]"
+                      [is fields.values.val fields.field_def.choices.name]selected="selected"[end]>
+                [fields.field_def.choices.name]
+		[if-any fields.field_def.choices.docstring]= [fields.field_def.choices.docstring][end]
+              </option>
+            [end]
+          [end]
+
+          [is value_is_shown "No"]
+            [# This is an oddball label, force the user to explicitly remove it.]
+              <option value="[fields.values.val]" selected="selected"
+                      title="This value is not a defined choice for this field">
+                [fields.values.val]
+              </option>
+          [end]
+      </select><br>
+  [end]
+
+  [for fields.derived_values]
+    <div title="Derived: [fields.derived_values.docstring]" class="rolloverzone">
+      <i>[fields.derived_values.val]</i>
+    </div>
+  [end]
+
+[else][# No current values, just give all choices.]
+
+   <select name="custom_[fields.field_id][is arg2 ""][else]_arg2[end]" id="[arg0]_custom_[fields.field_id]"
+           class="custom_field_value_menu" aria-labelledby="[fields.field_id]_label">
+       [if-any fields.field_def.is_required_bool]
+         <option value="" disabled="disabled" selected="selected">Select value&hellip;</option>
+       [else]
+          <option value="--" selected="selected" title="No value">--</option>
+       [end]
+       [for fields.field_def.choices]
+         <option value="[fields.field_def.choices.name]">
+           [fields.field_def.choices.name]
+           [if-any fields.field_def.choices.docstring]= [fields.field_def.choices.docstring][end]
+         </option>
+       [end]
+   </select><br>
+
+[end]
diff --git a/templates/tracker/field-value-single-int.ezt b/templates/tracker/field-value-single-int.ezt
new file mode 100644
index 0000000..944ba8b
--- /dev/null
+++ b/templates/tracker/field-value-single-int.ezt
@@ -0,0 +1,43 @@
+[# Even though this field definition says it is single-valued, the issue might have
+   multiple values if the field definition was previously multi-valued.  In such a situation
+   values other than the first value are shown read-only and must be explicitly removed
+   before the comment can be submitted. ]
+
+[# If the field has no explicit values, then show an empty form element.]
+[if-any fields.values][else]
+    <input type="number" name="custom_[fields.field_id][is arg2 ""][else]_[arg2][end]" id="[arg0]_custom_[fields.field_id]" value=""
+           [is arg0 "hidden"][else]
+             [if-any arg1] required="required"[end]
+           [end]
+           [if-any fields.field_def.min_value]min="[fields.field_def.min_value]"[end]
+           [if-any fields.field_def.max_value]max="[fields.field_def.max_value]"[end]
+           style="text-align:right; width:12em" class="multivalued customfield"
+           aria-labelledby="[fields.field_id]_label">
+[end]
+
+
+[for fields.values]
+  [if-index fields.values first]
+    <input type="number" name="custom_[fields.field_id][is arg2 ""][else]_[arg2][end]" value="[fields.values.val]"
+           [is arg0 "hidden"][else]
+             [if-any arg1]required="required"[end]
+           [end]
+           [if-any fields.field_def.min_value]min="[fields.field_def.min_value]"[end]
+           [if-any fields.field_def.max_value]max="[fields.field_def.max_value]"[end]
+           style="text-align:right; width:12em" class="multivalued customfield"
+           aria-labelledby="[fields.field_id]_label">
+  [else]
+    <span>
+      <input type="number" disabled="disabled" value="[fields.values.val]"
+             style="text-align:right; width: 12em" class="multivalued customfield"
+             aria-labelledby="[fields.field_id]_label">
+      <u class="removeMultiFieldValueWidget">X</u>
+    </span>
+  [end]
+[end]
+
+[for fields.derived_values]
+  <input type="number" disabled="disabled" value="[fields.derived_values.val]"
+         style="font-style:italic; text-align:right; width:12em" class="multivalued"
+	 aria-labelledby="[fields.field_id]_label">
+[end]
diff --git a/templates/tracker/field-value-single-str.ezt b/templates/tracker/field-value-single-str.ezt
new file mode 100644
index 0000000..60ff63c
--- /dev/null
+++ b/templates/tracker/field-value-single-str.ezt
@@ -0,0 +1,41 @@
+[# Even though this field definition says it is single-valued, the issue might have
+   multiple values if the field definition was previously multi-valued.  In such a situation
+   values other than the first value are shown read-only and must be explicitly removed
+   before the comment can be submitted. ]
+
+[# If the field has no explicit values, then show an empty form element.]
+[if-any fields.values][else]
+    <input name="custom_[fields.field_id][is arg2 ""][else]_[arg2][end]" id="[arg0]_custom_[fields.field_id]" value=""
+           [is arg0 "hidden"][else]
+             [if-any arg1]required="required"[end]
+           [end]
+           [# TODO(jrobbins): validation]
+           class="multivalued customfield" style="width: 12em"
+           aria-labelledby="[fields.field_id]_label">
+[end]
+
+
+[for fields.values]
+  [if-index fields.values first]
+    <input name="custom_[fields.field_id][is arg2 ""][else]_[arg2][end]" value="[fields.values.val]"
+           class="multivalued customfield"
+           [is arg0 "hidden"][else]
+             [if-any arg1]required="required"[end]
+           [end]
+           [# TODO(jrobbins): validation]
+           style="width: 12em"
+           aria-labelledby="[fields.field_id]_label"><br>
+  [else]
+    <span>
+      <input disabled="disabled" value="[fields.values.val]"
+             class="multivalued" style="width: 12em" aria-labelledby="[fields.field_id]_label">
+      <a href="#" class="removeMultiFieldValueWidget">X</a>
+    </span>
+  [end]
+[end]
+
+[for fields.derived_values]
+  <input disabled="disabled" value="[fields.derived_values.val]"
+         style="font-style:italic" class="multivalued" style="width: 12em"
+         aria-labelledby="[fields.field_id]_label"><br>
+[end]
diff --git a/templates/tracker/field-value-single-url.ezt b/templates/tracker/field-value-single-url.ezt
new file mode 100644
index 0000000..27bd9fe
--- /dev/null
+++ b/templates/tracker/field-value-single-url.ezt
@@ -0,0 +1,31 @@
+[# Even though this field definition says it is single-valued, the issue might have
+   multiple values if the field definition was previously multi-valued.  In such a situation
+   values other than the first value are shown read-only and must be explicitly removed
+   before the comment can be submitted. ]
+
+[# If the field has no explicit values, then show an empty form element.]
+[if-any fields.values][else]
+  <input name="custom_[fields.field_id][is arg2 ""][else]_[arg2][end]" id="[arg0]_custom_[fields.field_id]" value=""
+         class="multivalued customfield"
+         [is arg0 "hidden"][else]
+           [if-any arg1]required="required"[end]
+         [end]
+         style="width: 12em" aria-labelledby="[fields.field_id]_label">
+[end]
+
+[for fields.values]
+  [if-index fields.values first]
+    <input name="custom_[fields.field_id][is arg2 ""][else]_[arg2][end]" value="[fields.values.val]"
+           class="multivalued customfield"
+           [is arg0 "hidden"][else]
+             [if-any arg1]required="required"[end]
+           [end]
+           style="width: 12em" aria-labelledby="[fields.field_id]_label"><br>
+  [else]
+    <span>
+      <input disabled="disabled" value="[fields.values.val]"
+             class="multivalued" style="width: 12em" aria-labelledby="[fields.field_id]_label">
+      <a href="#" class="removeMultiFieldValueWidget">X</a>
+    </span>
+  [end]
+[end]
\ No newline at end of file
diff --git a/templates/tracker/field-value-single-user.ezt b/templates/tracker/field-value-single-user.ezt
new file mode 100644
index 0000000..5a09d00
--- /dev/null
+++ b/templates/tracker/field-value-single-user.ezt
@@ -0,0 +1,39 @@
+[# Even though this field definition says it is single-valued, the issue might have
+   multiple values if the field definition was previously multi-valued.  In such a situation
+   values other than the first value are shown read-only and must be explicitly removed
+   before the comment can be submitted. ]
+
+[# If the field has no explicit values, then show an empty form element.]
+[if-any fields.values][else]
+    <input type="text" name="custom_[fields.field_id][is arg2 ""][else]_[arg2][end]" id="[arg0]_custom_[fields.field_id]" value=""
+           [is arg0 "hidden"][else]
+             [if-any arg1]required="required"[end]
+           [end]
+           style="width:12em" class="multivalued userautocomplete customfield" autocomplete="off"
+           data-ac-type="owner" aria-labelledby="[fields.field_id]_label">
+[end]
+
+
+[for fields.values]
+  [if-index fields.values first]
+    <input type="text" name="custom_[fields.field_id][is arg2 ""][else]_[arg2][end]" value="[fields.values.val]"
+           [is arg0 "hidden"][else]
+             [if-any arg1]required="required"[end]
+           [end]
+           style="width:12em" class="multivalued userautocomplete customfield" autocomplete="off"
+           data-ac-type="owner" aria-labelledby="[fields.field_id]_label">
+  [else]
+    <span>
+      <input type="text" disabled="disabled" value="[fields.values.val]"
+             style="width:12em" class="multivalued userautocomplete customfield" autocomplete="off"
+             data-ac-type="owner" aria-labelledby="[fields.field_id]_label">
+      <a href="#" class="removeMultiFieldValueWidget">X</a>
+    </span>
+  [end]
+[end]
+
+[for fields.derived_values]
+  <input type="text" disabled="disabled" value="[fields.derived_values.val]"
+         style="font-style:italic; width:12em" class="multivalued"
+         data-ac-type="owner" aria-labelledby="[fields.field_id]_label">
+[end]
diff --git a/templates/tracker/field-value-widgets-js.ezt b/templates/tracker/field-value-widgets-js.ezt
new file mode 100644
index 0000000..127d85c
--- /dev/null
+++ b/templates/tracker/field-value-widgets-js.ezt
@@ -0,0 +1,35 @@
+<script type="text/javascript" nonce="[nonce]">
+runOnLoad(function() {
+  var removeMFVElements = document.getElementsByClassName("removeMultiFieldValueWidget");
+  for (var i = 0; i < removeMFVElements.length; ++i) {
+     var el = removeMFVElements[[]i];
+     el.addEventListener("click", function(event) {
+         _removeMultiFieldValueWidget(event.target);
+     });
+  }
+
+  var addMFVElements = document.getElementsByClassName("addMultiFieldValueWidget");
+  for (var i = 0; i < addMFVElements.length; ++i) {
+     var el = addMFVElements[[]i];
+     el.addEventListener("click", function(event) {
+          var target = event.target;
+          var fieldID = target.getAttribute("data-field-id");
+          var fieldType = target.getAttribute("data-field-type");
+          var fieldValidate1 = target.getAttribute("data-validate-1");
+          var fieldValidate2 = target.getAttribute("data-validate-2");
+	  var fieldPhaseName = target.getAttribute("data-phase-name");
+         _addMultiFieldValueWidget(
+             event.target, fieldID, fieldType, fieldValidate1, fieldValidate2, fieldPhaseName);
+     });
+  }
+
+  var customFieldElements = document.getElementsByClassName("customfield");
+  for (var i = 0; i < customFieldElements.length; ++i) {
+     var el = customFieldElements[[]i];
+     el.addEventListener("focus", function(event) {
+         _acrob(null);
+         _acof(event);
+     });
+  }
+});
+</script>
diff --git a/templates/tracker/field-value-widgets.ezt b/templates/tracker/field-value-widgets.ezt
new file mode 100644
index 0000000..3593fa2
--- /dev/null
+++ b/templates/tracker/field-value-widgets.ezt
@@ -0,0 +1,56 @@
+[# Display widgets for editing one custom field.
+   The variable "fields" must already refer to a FieldValueView object.
+   arg0: True if the field is multi-valued.
+   arg1: Prefix for IDs
+   arg2: True if the field should be required
+   arg3: Parent phase name suffix if any.
+]
+[is fields.field_def.type_name "ENUM_TYPE"]
+  [if-any arg0]
+    [include "field-value-multi-enum.ezt" arg1 arg2 arg3]
+  [else]
+    [include "field-value-single-enum.ezt" arg1 arg2 arg3]
+  [end]
+[end]
+
+[is fields.field_def.type_name "INT_TYPE"]
+  [if-any arg0]
+    [include "field-value-multi-int.ezt" arg1 arg2 arg3]
+  [else]
+    [include "field-value-single-int.ezt" arg1 arg2 arg3]
+  [end]
+[end]
+
+[is fields.field_def.type_name "STR_TYPE"]
+  [if-any arg0]
+    [include "field-value-multi-str.ezt" arg1 arg2 arg3]
+  [else]
+    [include "field-value-single-str.ezt" arg1 arg2 arg3]
+  [end]
+[end]
+
+[is fields.field_def.type_name "USER_TYPE"]
+  [if-any arg0]
+    [include "field-value-multi-user.ezt" arg1 arg2 arg3]
+  [else]
+    [include "field-value-single-user.ezt" arg1 arg2 arg3]
+  [end]
+[end]
+
+[is fields.field_def.type_name "DATE_TYPE"]
+  [if-any arg0]
+    [include "field-value-multi-date.ezt" arg1 arg2 arg3]
+  [else]
+    [include "field-value-single-date.ezt" arg1 arg2 arg3]
+  [end]
+[end]
+
+[is fields.field_def.type_name "URL_TYPE"]
+  [if-any arg0]
+    [include "field-value-multi-url.ezt" arg1 arg2 arg3]
+  [else]
+    [include "field-value-single-url.ezt" arg1 arg2 arg3]
+  [end]
+[end]
+
+[# TODO(jrobbins): more field types. ]
diff --git a/templates/tracker/issue-advsearch-page.ezt b/templates/tracker/issue-advsearch-page.ezt
new file mode 100644
index 0000000..6fc89fb
--- /dev/null
+++ b/templates/tracker/issue-advsearch-page.ezt
@@ -0,0 +1,82 @@
+[define category_css]css/ph_detail.css[end]
+[include "../framework/header.ezt" "hidetabs"]
+
+<form action="advsearch.do" method="POST" style="margin:6px;margin-top:12px;" autocomplete="false">
+
+[# Note: No need for UI element permission checking here. ]
+
+<table cellspacing="0" cellpadding="4" border="0" class="advquery">
+   <tr class="focus"><td width="25%"><b>&nbsp;Find issues</b></td>
+    <td>Search within</td>
+    <td>
+       <select name="can" style="width:100%">
+        [include "issue-can-widget.ezt" "advsearch"]
+       </select>
+    </td>
+    <td width="25%" align="center" rowspan="3">
+     <input type="submit" name="btn" value="Search" style="font-size:120%">
+    </td>
+   </tr>
+   <tr class="focus"><td width="25%"></td>
+       <td>with <b>all</b> of the words</td><td><input type="text" size="25" name="words" value=""></td>
+   </tr>
+   <tr class="focus"><td></td>
+       <td><b>without</b> the words</td><td><input type="text" size="25" name="without" value=""></td>
+   </tr>
+   <tr><td>&nbsp;</td><td></td><td></td><td></td></tr>
+   [# TODO(jrobbins): allow commas ]
+   <tr><td><b>Restrict search to</b></td><td>Labels</td><td><input type="text" name="labels" id="labelsearch" size="25" value="" placeholder="All the labels" autocomplete="off"></td><td class="eg">e.g., FrontEnd Priority:High</td></tr>
+   <tr><td rowspan="5"><br>
+        <table cellspacing="0" cellpadding="0" border="0"><tr><td>
+        <div class="tip">
+            <b>Tip:</b> Search results can be<br>refined by clicking on
+            the<br>result table headings.<br> <a href="searchtips">More
+            Search Tips</a>
+        </div>
+        </td></tr></table>
+       </td>
+       [# TODO(jrobbins): allow commas ]
+       <td>Statuses</td><td><input type="text" name="statuses" id="statussearch" size="25" value="" placeholder="Any status" autocomplete="off"></td><td class="eg">e.g., Started</td></tr>
+   <tr><td>Components</td><td><input type="text" size="25" name="components" id="componentsearch" value="" placeholder="Any component" autocomplete="off"></td><td class="eg"></td></tr>
+   <tr><td>Reporters</td><td><input type="text" size="25" name="reporters" id="memberreportersearch" value="" placeholder="Any reporter" autocomplete="off"></td><td class="eg"></td></tr>
+   [# TODO(jrobbins): allow commas ]
+   <tr><td>Owners</td><td><input type="text" size="25" name="owners" id="ownersearch" value="" placeholder="Any owner" autocomplete="off"></td><td class="eg">e.g., user@example.com</td></tr>
+   <tr><td>Cc</td><td><input type="text" size="25" name="cc" id="memberccsearch" value="" placeholder="Any cc" autocomplete="off"></td><td class="eg"></td></tr>
+   <tr><td></td><td>Comment by</td><td><input type="text" size="25" name="commentby" id="membercommentbysearch" value="" placeholder="Any commenter"></td><td class="eg"></td></tr>
+   [# TODO(jrobbins): implement search by star counts
+   <tr><td></td><td>Starred by</td>
+       <td>
+           <select name="starcount" style="width:100%">
+            <option value="-1" selected="selected">Any number of users</option>
+            <option value="0">Exactly zero users</option>
+            <option value="1">1 or more users</option>
+            <option value="2">2 or more users</option>
+            <option value="3">3 or more users</option>
+            <option value="4">4 or more users</option>
+            <option value="5">5 or more users</option>
+           </select></td>
+       <td class="eg"></td>
+   </tr>
+   ]
+   [# TODO(jrobbins) search by dates? ]
+   <tr><td></td><td>&nbsp;</td><td></td><td class="eg"></td></tr>
+</table>
+</form>
+
+<script type="text/javascript" nonce="[nonce]">
+runOnLoad(function() {
+  var _idsToAddDefaultListeners = [[]
+      "labelsearch", "statussearch", "componentsearch", "memberreportersearch",
+      "ownersearch", "memberccsearch", "membercommentbysearch"];
+  for (var i = 0; i < _idsToAddDefaultListeners.length; i++) {
+    var id = _idsToAddDefaultListeners[[]i];
+    if ($(id)) {
+      $(id).addEventListener("focus", function(event) {
+        _acof(event);
+      });
+    }
+  }
+});
+</script>
+
+[include "../framework/footer.ezt"]
diff --git a/templates/tracker/issue-attachment-text.ezt b/templates/tracker/issue-attachment-text.ezt
new file mode 100644
index 0000000..98e9cdf
--- /dev/null
+++ b/templates/tracker/issue-attachment-text.ezt
@@ -0,0 +1,43 @@
+[define category_css]css/ph_detail.css[end]
+[define page_css]css/d_sb.css[end]
+[# Use raw format because filename will be escaped when title variable is used.]
+[define title][format "raw"][filename][end] ([filesize])[end]
+[include "../framework/header.ezt" "showtabs"]
+
+<link type="text/css" rel="stylesheet"
+      href="[version_base]/static/css/prettify.css">
+
+<h3 style="margin-bottom: 0">Issue <a href="detail?id=[local_id][#TODO(jrobbins): comment number]">[local_id]</a> attachment: [filename] <small>([filesize])</small>
+</h3>
+
+
+
+<div class="fc">
+  [if-any too_large]
+    <p><em>This file is too large to display.</em></p>
+
+  [else][if-any is_binary]
+
+    <p><em>
+      This file is not plain text (only UTF-8 and Latin-1 text encodings are currently supported).
+    </em></p>
+  [else]
+
+    [include "../framework/file-content-part.ezt"]
+    [include "../framework/file-content-js.ezt"]
+
+  [end][end]
+
+</div>
+
+
+[if-any should_prettify]
+<script src="[version_base]/static/js/prettify.js" nonce="[nonce]"></script>
+<script type="text/javascript" nonce="[nonce]">
+runOnLoad(function() {
+  prettyPrint();
+});
+</script>
+[end]
+
+[include "../framework/footer.ezt"]
diff --git a/templates/tracker/issue-blocking-change-notification-email.ezt b/templates/tracker/issue-blocking-change-notification-email.ezt
new file mode 100644
index 0000000..9e45c69
--- /dev/null
+++ b/templates/tracker/issue-blocking-change-notification-email.ezt
@@ -0,0 +1,7 @@
+Issue [issue.local_id]: [format "raw"][summary][end]
+[detail_url]
+
+[if-any is_blocking]This issue is now blocking issue [downstream_issue_ref].
+See [downstream_issue_url]
+[else]This issue is no longer blocking issue [downstream_issue_ref].
+See [downstream_issue_url][end]
diff --git a/templates/tracker/issue-bulk-change-notification-email.ezt b/templates/tracker/issue-bulk-change-notification-email.ezt
new file mode 100644
index 0000000..8a2d996
--- /dev/null
+++ b/templates/tracker/issue-bulk-change-notification-email.ezt
@@ -0,0 +1,16 @@
+[if-any any_link_only][else][if-any amendments]Updates:
+[amendments]
+[end]
+Comment[if-any commenter] by [commenter.display_name][end]:
+[if-any comment_text][format "raw"][comment_text][end][else](No comment was entered for this change.)[end]
+[end]
+Affected issues:
+[for issues]  Issue [issues.local_id]: [if-any issues.link_only][else][format "raw"][issues.summary][end][end]
+    [format "raw"]http://[hostport][issues.detail_relative_url][end]
+
+[end]
+--
+You received this message because you are listed in the owner
+or CC fields of these issues, or because you starred them.
+You may adjust your issue notification preferences at:
+http://[hostport]/hosting/settings
diff --git a/templates/tracker/issue-bulk-edit-page.ezt b/templates/tracker/issue-bulk-edit-page.ezt
new file mode 100644
index 0000000..d57c0ae
--- /dev/null
+++ b/templates/tracker/issue-bulk-edit-page.ezt
@@ -0,0 +1,483 @@
+[define category_css]css/ph_detail.css[end]
+[include "../framework/header.ezt" "hidetabs"]
+
+[# Note: base permission for this page is EditIssue]
+
+[if-any read_only][include "../framework/read-only-rejection.ezt"]
+[else]
+
+
+<div style="margin-top: 0; padding: 3px;" class="closed">
+ <form action="bulkedit.do" method="POST" style="margin: 0; padding: 0" enctype="multipart/form-data"
+       id="bulk_form">
+
+ <input type="hidden" name="can" value=[can] >
+ <input type="hidden" name="start" value=[start] >
+ <input type="hidden" name="num" value=[num] >
+ <input type="hidden" name="q" value="[query]">
+ <input type="hidden" id="sort" name="sort" value="[sortspec]">
+ <input type="hidden" name="groupby" value="[groupby]">
+ <input type="hidden" name="colspec" value="[colspec]">
+ <input type="hidden" name="x" value="[grid_x_attr]">
+ <input type="hidden" name="y" value="[grid_y_attr]">
+ <input type="hidden" name="mode" value="[if-any grid_mode]grid[end]">
+ <input type="hidden" name="cells" value="[grid_cell_mode]">
+
+ <input type="hidden" name="ids"
+        value="[for issues][issues.local_id][if-index issues last][else], [end][end]">
+ <input type="hidden" name="token" value="[form_token]">
+ <table cellpadding="0" cellspacing="0" border="0">
+  <tr><td>
+
+ <table cellspacing="0" cellpadding="3" border="0" class="rowmajor vt">
+   <tr><th>Issues:</th>
+    <td colspan="2">
+     [for issues]
+      <a href="detail?id=[issues.local_id]" title="[issues.summary]"
+        [if-any issues.closed]class=closed_ref[end]
+        >[if-any issues.closed]&nbsp;[end][issues.local_id][if-any issues.closed]&nbsp;[end]</a>[if-index issues last][else], [end]
+     [end]
+    </td>
+   </tr>
+
+   <tr>
+    <th>Comment:</th>
+    <td colspan="2">
+     <textarea cols="75" rows="6" name="comment" id="comment" class="issue_text">[initial_comment]</textarea>
+       [if-any errors.comment]
+         <div class="fielderror">[errors.comment]</div>
+       [end]
+    </td>
+   </tr>
+
+   <tr><th width="10%"><label for="statusenter">Status:</label></th><td colspan="2">
+        <select id="statusenter" name="status">
+          <option style="display: none" value="[initial_status]"></option>
+        </select>
+        <span id="merge_area" style="margin-left:2em;">
+               Merge into issue:
+               <input type="text" id="merge_into" name="merge_into" style="width: 5em"
+                      value="[is initial_merge_into "0"][else][initial_merge_into][end]">
+        </span>
+        [if-any errors.merge_into_id]
+          <div class="fielderror">[errors.merge_into_id]</div>
+        [end]
+       </td>
+   </tr>
+   <tr><th width="10%">Owner:</th><td colspan="2">
+         [include "issue-bulk-operator-part.ezt" "ownerenter" ""]
+         <input id="ownerenter" type="text" autocomplete="off" style="width: 12em"
+                name="owner" value="[initial_owner]">
+         [if-any errors.owner]
+           <div class="fielderror">[errors.owner]</div>
+         [end]
+       </td>
+   </tr>
+   <tr><th>Cc:</th><td colspan="2">
+         [include "issue-bulk-operator-part.ezt" "memberenter" "multi"]
+         <input type="text" multiple id="memberenter" autocomplete="off" style="width: 30em"
+                name="cc" value="[initial_cc]">
+         [if-any errors.cc]
+           <div class="fielderror">[errors.cc]</div>
+         [end]
+       </td>
+   </tr>
+
+   <tr><th>Components:</th><td colspan="2">
+       [include "issue-bulk-operator-part.ezt" "componententer" "multi"]
+       <input type="text" id="componententer" style="width:30em"
+              name="components" value="[initial_components]">
+       [if-any errors.components]
+         <div class="fielderror">[errors.components]</div>
+       [end]
+   </td></tr>
+
+   <tbody class="collapse">
+   [# Show some field editing elements immediately, others can be revealed.]
+     [define any_fields_to_reveal]No[end]
+     [for fields]
+       [if-any fields.applicable][if-any fields.is_editable]
+         [# TODO(jrobbins): determine applicability dynamically and update fields in JS]
+         <tr [if-any fields.display][else]class="ifExpand"[define any_fields_to_reveal]Yes[end][end]>
+           <th>[fields.field_name]:</th>
+           <td colspan="2">
+             [define widget_id]custom_[fields.field_id][end]
+             [define multi][if-any fields.field_def.is_multivalued_bool]multi[end][end]
+             [include "issue-bulk-operator-part.ezt" widget_id multi]
+             [include "field-value-widgets.ezt" False "" fields.field_def.is_required_bool ""]
+             <div class="fielderror" style="display:none" id="error_custom_[fields.field_id]"></div>
+           </td>
+         <tr>
+       [end][end]
+     [end]
+     [is any_fields_to_reveal "Yes"]
+       <tr class="ifCollapse">
+         <td colspan="2"><a href="#" class="toggleCollapse">Show all fields</a><t/td>
+       </tr>
+     [end]
+   </tbody>
+
+   [for issue_phase_names]
+     [for fields]
+       [is fields.phase_name issue_phase_names][if-any fields.is_editable]
+         [# TODO(jojwang): monorail:5154, bulk-editing single phase values not supported]
+         [if-any fields.field_def.is_multivalued_bool]
+           <tr><th>[issue_phase_names].[fields.field_name]:</th>
+             <td colspan="2">
+               [define widget_id]custom_[fields.field_id]_[issue_phase_names][end]
+               [include "issue-bulk-operator-part.ezt" widget_id "multi"]
+               [include "field-value-widgets.ezt" False "" fields.field_def.is_required_bool issue_phase_names]
+               <div class="fielderror" style="display:none" id="error_custom_[issue_phase_names]_[fields.field_id]"></div>
+             </td>
+           </tr>
+         [end]
+       [end][end]
+     [end]
+   [end]
+
+   <tr><th>Labels:</th>
+       <td colspan="2" class="labelediting">
+        <div id="enterrow1">
+         <input type="text" class="labelinput" id="label0" size="20" autocomplete="off"
+                name="label" value="[label0]">
+         <input type="text" class="labelinput" id="label1" size="20" autocomplete="off"
+                name="label" value="[label1]">
+         <input type="text" class="labelinput" id="label2" size="20" autocomplete="off"
+                data-show-id="enterrow2" data-hide-id="addrow1"
+                name="label" value="[label2]"> <span id="addrow1" class="fakelink" data-instead="enterrow2">Add a row</span>
+        </div>
+        <div id="enterrow2"  style="display:none">
+         <input type="text" class="labelinput" id="label3" size="20" autocomplete="off"
+                name="label" value="[label3]">
+         <input type="text" class="labelinput" id="label4" size="20" autocomplete="off"
+                name="label" value="[label4]">
+         <input type="text" class="labelinput" id="label5" size="20" autocomplete="off"
+                data-show-id="enterrow3" data-hide-id="addrow2"
+                name="label" value="[label5]"> <span id="addrow2" class="fakelink" data-instead="enterrow3">Add a row</span>
+        </div>
+        <div id="enterrow3" style="display:none">
+         <input type="text" class="labelinput" id="label6" size="20" autocomplete="off"
+                name="label" value="[label6]">
+         <input type="text" class="labelinput" id="label7" size="20" autocomplete="off"
+                name="label" value="[label7]">
+         <input type="text" class="labelinput" id="label8" size="20" autocomplete="off"
+                data-show-id="enterrow4" data-hide-id="addrow3"
+                name="label" value="[label8]"> <span id="addrow3" class="fakelink" data-instead="enterrow4">Add a row</span>
+        </div>
+        <div id="enterrow4" style="display:none">
+         <input type="text" class="labelinput" id="label9" size="20" autocomplete="off"
+                name="label" value="[label9]">
+         <input type="text" class="labelinput" id="label10" size="20" autocomplete="off"
+                name="label" value="[label10]">
+         <input type="text" class="labelinput" id="label11" size="20" autocomplete="off"
+                data-show-id="enterrow5" data-hide-id="addrow4"
+                name="label" value="[label11]"> <span id="addrow4" class="fakelink" data-instead="enterrow5">Add a row</span>
+        </div>
+        <div id="enterrow5" style="display:none">
+         <input type="text" class="labelinput" id="label12" size="20" autocomplete="off"
+                name="label" value="[label12]">
+         <input type="text" class="labelinput" id="label13" size="20" autocomplete="off"
+                name="label" value="[label13]">
+         <input type="text" class="labelinput" id="label14" size="20" autocomplete="off"
+                data-show-id="enterrow6" data-hide-id="addrow5"
+                name="label" value="[label14]"> <span id="addrow5" class="fakelink" data-instead="enterrow6">Add a row</span>
+        </div>
+        <div id="enterrow6" style="display:none">
+         <input type="text" class="labelinput" id="label15" size="20" autocomplete="off"
+                name="label" value="[label15]">
+         <input type="text" class="labelinput" id="label16" size="20" autocomplete="off"
+                name="label" value="[label16]">
+         <input type="text" class="labelinput" id="label17" size="20" autocomplete="off"
+                data-show-id="enterrow7" data-hide-id="addrow6"
+                name="label" value="[label17]"> <span id="addrow6" class="fakelink" data-instead="enterrow7">Add a row</span>
+        </div>
+        <div id="enterrow7" style="display:none">
+         <input type="text" class="labelinput" id="label18" size="20" autocomplete="off"
+                name="label" value="[label18]">
+         <input type="text" class="labelinput" id="label19" size="20" autocomplete="off"
+                name="label" value="[label19]">
+         <input type="text" class="labelinput" id="label20" size="20" autocomplete="off"
+                data-show-id="enterrow8" data-hide-id="addrow7"
+                name="label" value="[label20]"> <span id="addrow7" class="fakelink" data-instead="enterrow8">Add a row</span>
+        </div>
+        <div id="enterrow8" style="display:none">
+         <input type="text" class="labelinput" id="label21" size="20" autocomplete="off"
+                name="label" value="[label21]">
+         <input type="text" class="labelinput" id="label22" size="20" autocomplete="off"
+                name="label" value="[label22]">
+         <input type="text" class="labelinput" id="label23" size="20" autocomplete="off"
+                name="label" value="[label23]">
+        </div>
+      </td>
+   </tr>
+
+   <tr><th>Blocked on:</th><td colspan="2">
+         [include "issue-bulk-operator-part.ezt" "blockedonenter" "multi"]
+         <input type="text" multiple id="blockedonenter" style="width: 30em"
+                name="blocked_on" value="[initial_blocked_on]">
+         [if-any errors.blocked_on]
+           <div class="fielderror">[errors.blocked_on]</div>
+         [end]
+       </td>
+   </tr>
+
+   <tr><th>Blocking:</th><td colspan="2">
+         [include "issue-bulk-operator-part.ezt" "blockingenter" "multi"]
+         <input type="text" multiple id="blockingenter" style="width: 30em"
+                name="blocking" value="[initial_blocking]">
+         [if-any errors.blocking]
+           <div class="fielderror">[errors.blocking]</div>
+         [end]
+       </td>
+   </tr>
+
+   [if-any page_perms.DeleteIssue]
+   <tr><th width="10%">Move to project:</th><td colspan="2">
+         <input id="move_toenter" type="text" autocomplete="off" style="width: 12em"
+                name="move_to">
+         [if-any errors.move_to]
+           <div class="fielderror">[errors.move_to]</div>
+         [end]
+       </td>
+   </tr>
+   [end]
+
+   <tr>
+    <td colspan="3"><span id="confirmarea" class="novel" style="padding-top:5px; margin:0">
+      <span id="confirmmsg"></span>
+      [# TODO(jrobbins): <a href="TODO" target="_new">Learn more</a>]
+    </span>
+    </td>
+   </tr>
+ </table>
+
+
+
+[# TODO(jrobbins):     <a class="ifClosed toggleHidden" href="#">More options</a>]
+[#     <a class="ifOpened" href="#" class="toggleHidden" style="background:#ccc; padding: 4px;">Hide options</a>]
+[#     <div  class="ifOpened"  style="background:#ccc; padding: 8px"><a href="#autmatically-generated">Bookmarkable link to these values</a></div>]
+[# <br><br>]
+
+
+
+
+ <div style="padding:6px">
+  <input type="submit" id="submit_btn" name="btn" value="Update [num_issues] Issue[is num_issues "1"][else]s[end]">
+  <input type="button" id="discard" name="nobtn" value="Discard">
+
+  <input type="checkbox" checked="checked" name="send_email" id="send_email" style="margin-left:1em">
+  <label for="send_email" title="Send issue change notifications to interested users">Send email</label>
+
+ </div>
+
+
+
+[if-any show_progress]
+ <div>Note: Updating [num_issues] issues will take approximately [num_seconds] seconds.</div>
+ <div id="progress">
+ </div>
+[end]
+
+   </td>
+   <td>
+     <div class="tip">
+         <b>Usage:</b> This form allows you to update several issues at one
+         time.<br><br>
+         The same comment will be applied to all issues.<br><br>
+
+         If specified, the status or owner you enter will be applied to all
+         issues.<br><br>
+
+         You may append or remove values in multi-valued fields by choosing the += or -= operators.
+         To remove labels, preceed the label with a leading dash.  (You may also use a leading dash
+         to remove individual items when using the += operator.)
+     </div>
+   </td>
+   </tr>
+   </table>
+
+
+ </form>
+</div>
+
+<mr-bulk-approval-update
+  projectName="[projectname]"
+  localIdsStr="[local_ids_str]"
+></mr-bulk-approval-update>
+
+<script type="text/javascript" nonce="[nonce]">
+runOnLoad(function() {
+  document.getElementById('comment').select();
+  _lfidprefix = 'label';
+  setTimeout(_forceProperTableWidth, 100);
+
+  _exposeExistingLabelFields();
+
+  [if-any errors.custom_fields]
+    var field_error;
+    [for errors.custom_fields]
+      field_error = document.getElementById('error_custom_' + [errors.custom_fields.field_id]);
+      field_error.textContent = "[errors.custom_fields.message]";
+      field_error.style.display = "";
+    [end]
+  [end]
+
+  checksubmit();
+  setInterval(checksubmit, 700); [# catch changes that were not keystrokes, e.g., paste menu item.]
+
+
+
+function checksubmit() {
+  var submit = document.getElementById('submit_btn');
+  var cg = document.getElementById('cg');
+  if (cg != undefined) { submit.disabled='disabled'; }
+
+  submit.disabled='disabled';
+  var restrict_to_known = [if-any restrict_to_known]true[else]false[end];
+  var confirmmsg = document.getElementById('confirmmsg');
+  var statusenter = $('statusenter');
+  var merge_area = $('merge_area');
+  var statuses_offer_merge = [[] [for statuses_offer_merge]"[statuses_offer_merge]"[if-index statuses_offer_merge last][else],[end][end] ];
+  if (restrict_to_known && confirmmsg && confirmmsg.textContent.length > 0) {
+    return;
+  }
+  if (cg == undefined || cg.value.length > 1) {
+    submit.disabled='';
+  }
+
+  if (statusenter) {
+     var offer_merge = 'none';
+     for (var i = 0; i < statuses_offer_merge.length; i++) {
+       if (statusenter.value == statuses_offer_merge[[]i]) offer_merge = '';
+     }
+     merge_area.style.display = offer_merge;
+  }
+}
+
+
+function disableFormElement(el) {
+  el.readOnly = 'yes';
+  el.style.background = '#eee';
+  [# TODO(jrobbins): disable auto-complete ]
+}
+
+
+function bulkOnSubmit() {
+  var inputs = document.getElementsByTagName('input');
+  for (var i = 0; i < inputs.length; i++) {
+    disableFormElement(inputs[[]i]);
+  }
+  disableFormElement(document.getElementById('comment'));
+  [if-any show_progress]
+   var progress = document.getElementById('progress');
+   progress.textContent = 'Processing...';
+  [end]
+}
+
+
+function _checkAutoClear(inputEl, selectID) {
+  var val = inputEl.value;
+  var sel = document.getElementById(selectID);
+  if (val.match(/^--+$/)) {
+    sel.value = 'clear';
+    inputEl.value = '';
+  } else if (val) {
+    sel.value = 'set';
+  }
+}
+
+
+$("bulk_form").addEventListener("submit", bulkOnSubmit);
+
+if ($("statusenter")) {
+  _loadStatusSelect("[projectname]", "statusenter", "[initial_status]", isBulkEdit=true);
+  $("statusenter").addEventListener("focus", function(event) {
+    _acrob(null);
+  });
+  $("statusenter").addEventListener("keyup", function(event) {
+    _checkAutoClear(event.target, "op_statusenter");
+    return _confirmNovelStatus(event.target);
+  });
+}
+if ($("ownerenter")) {
+  $("ownerenter").addEventListener("focus", function(event) {
+    _acof(event);
+  });
+  $("ownerenter").addEventListener("keyup", function(event) {
+    _checkAutoClear(event.target, "op_ownerenter");
+    return true;
+  });
+}
+if ($("memberenter")) {
+  $("memberenter").addEventListener("focus", function(event) {
+    _acof(event);
+  });
+}
+if ($("componententer")) {
+  $("componententer").addEventListener("focus", function(event) {
+    _acof(event);
+  });
+}
+
+if ($("move_toenter")) {
+  $("move_toenter").addEventListener("focus", function(event) {
+    _acof(event);
+  });
+}
+
+if ($("submit_btn")) {
+  $("submit_btn").addEventListener("focus", function(event) {
+    _acrob(null);
+  });
+  $("submit_btn").addEventListener("mousedown", function(event) {
+    _acrob(null);
+  });
+  $("submit_btn").addEventListener("click", function(event) {
+    _trimCommas();
+  });
+}
+if ($("discard")) {
+  $("discard").addEventListener("click", function(event) {
+    _confirmDiscardEntry(this);
+    event.preventDefault();
+  });
+}
+
+var labelInputs = document.getElementsByClassName("labelinput");
+for (var i = 0; i < labelInputs.length; ++i) {
+  var labelInput = labelInputs[[]i];
+  labelInput.addEventListener("keyup", function (event) {
+    if (event.target.getAttribute("data-show-id") &&
+        event.target.getAttribute("data-hide-id") &&
+        event.target.value) {
+      _showID(event.target.getAttribute("data-show-id"));
+      _hideID(event.target.getAttribute("data-hide-id"));
+    }
+    return _vallab(event.target);
+  });
+  labelInput.addEventListener("blur", function (event) {
+    return _vallab(event.target);
+  });
+  labelInput.addEventListener("focus", function (event) {
+    return _acof(event);
+  });
+}
+
+var addRowLinks = document.getElementsByClassName("fakelink");
+for (var i = 0; i < addRowLinks.length; ++i) {
+  var rowLink = addRowLinks[[]i];
+  rowLink.addEventListener("click", function (event) {
+      _acrob(null);
+      var insteadID = event.target.getAttribute("data-instead");
+      if (insteadID)
+        _showInstead(insteadID, this);
+  });
+}
+
+});
+</script>
+
+[end]
+
+[include "field-value-widgets-js.ezt"]
+[include "../framework/footer.ezt"]
diff --git a/templates/tracker/issue-bulk-operator-part.ezt b/templates/tracker/issue-bulk-operator-part.ezt
new file mode 100644
index 0000000..8b1f37a
--- /dev/null
+++ b/templates/tracker/issue-bulk-operator-part.ezt
@@ -0,0 +1,29 @@
+[# Display a <select> widget with options to set/append/remove/clear the field.
+   Args:
+    arg0: element ID of widget to disable if Clear is selected.  The form name and ID
+          of the <select> will be "op_" + arg0.
+    arg1: "multi" for multi-valued fields so that "Append" and "Remove" are offered.
+  ]
+<select name="op_[arg0]" id="op_[arg0]" style="width:9em" tabindex="-1">
+  [is arg1 "multi"]
+    <option value="append" selected="selected">Append +=</option>
+    <option value="remove">Remove -=</option>
+    [# TODO(jrobbins): <option value="setexact">Set exactly :=</option>]
+  [else]
+    <option value="set" selected="selected">Set =</option>
+    <option value="clear">Clear</option>
+  [end]
+</select>
+
+[is arg1 "multi"][else]
+<script type="text/javascript" nonce="[nonce]">
+
+runOnLoad(function() {
+  if ($("op_[arg0]")) {
+    $("op_[arg0]").addEventListener("change", function(event) {
+      TKR_ignoreWidgetIfOpIsClear(event.target, '[arg0]');
+    });
+  }
+});
+</script>
+[end]
diff --git a/templates/tracker/issue-can-widget.ezt b/templates/tracker/issue-can-widget.ezt
new file mode 100644
index 0000000..b0f8958
--- /dev/null
+++ b/templates/tracker/issue-can-widget.ezt
@@ -0,0 +1,82 @@
+[# This is used in the issue search form and issue advanced search page.  We want to show the same options in both contexts.]
+[define selected]False[end]
+<option disabled="disabled">Search within:</option>
+<option value="1" [is can "1"]selected=selected [define selected]True[end] [end]
+        title="All issues in the project">&nbsp;All issues</option>
+<option value="2" [is can "2"]selected=selected [define selected]True[end] [end]
+        title="All issues except ones with a closed status">&nbsp;Open issues</option>
+
+[if-any logged_in_user]
+ [define username][logged_in_user.email][end]
+ [is arg0 "admin"][define username]logged-in-user[end][end]
+ <option value="3" [is can "3"]selected=selected [define selected]True[end] [end]
+         title="[[]Open issues] owner=[username]">&nbsp;Open and owned by me</option>
+ <option value="4" [is can "4"]selected=selected [define selected]True[end] [end]
+         title="[[]Open issues] reporter=[username]">&nbsp;Open and reported by me</option>
+ <option value="5" [is can "5"]selected=selected [define selected]True[end] [end]
+         title="[[]Open issues] starredby:[username]">&nbsp;Open and starred by me</option>
+ <option value="8" [is can "8"]selected=selected [define selected]True[end] [end]
+         title="[[]Open issues] commentby:[username]">&nbsp;Open with comment by me</option>
+[end]
+
+[# TODO(jrobbins): deprecate these and tell projects to define canned queries instead.]
+<option value="6" [is can "6"]selected=selected [define selected]True[end] [end]
+         title="[[]Open issues] status=New">&nbsp;New issues</option>
+<option value="7" [is can "7"]selected=selected [define selected]True[end] [end]
+         title="[[]All issues] status=fixed,done">&nbsp;Issues to verify</option>
+
+[is arg0 "admin"][else]
+  [define first]Yes[end]
+  [for canned_queries]
+    [is first "Yes"]
+      [define first]No[end]
+      <option disabled="disabled">----</option>
+    [end]
+    [# TODO(jrobbins): canned query visibility conditions, e.g., members only. ]
+    <option value="[canned_queries.query_id]"
+            [is can canned_queries.query_id]
+                selected=selected
+                [define selected]True[end]
+            [end]
+            title="[canned_queries.docstring]"
+            >&nbsp;[canned_queries.name]</option>
+  [end]
+  [if-any perms.EditProject][if-any is_cross_project][else]
+      [is first "Yes"]
+        [define first]No[end]
+        <option disabled="disabled">----</option>
+      [end]
+      <option value="manageprojectqueries"
+              >&nbsp;Manage project queries...</option>
+  [end][end]
+
+  [if-any logged_in_user]
+    [define first]Yes[end]
+    [for saved_queries]
+      [is first "Yes"]
+        [define first]No[end]
+        <option disabled="disabled">----</option>
+      [end]
+      <option value="[saved_queries.query_id]"
+              [is can saved_queries.query_id]
+                  selected=selected
+                  [define selected]True[end]
+              [end]
+              title="[saved_queries.docstring]"
+              >&nbsp;[saved_queries.name]</option>
+    [end]
+    [is first "Yes"]
+      [define first]No[end]
+      <option disabled="disabled">----</option>
+    [end]
+    <option value="managemyqueries"
+            >&nbsp;Manage my saved queries...</option>
+  [end][# end if logged in]
+
+[end][# end not "admin"]
+
+[is selected "False"]
+  <option value="[can]" selected=selected
+          title="Custom Query"
+          >&nbsp;Custom Query</option>
+[end]
diff --git a/templates/tracker/issue-change-notification-email-link-only.ezt b/templates/tracker/issue-change-notification-email-link-only.ezt
new file mode 100644
index 0000000..e0adf5c
--- /dev/null
+++ b/templates/tracker/issue-change-notification-email-link-only.ezt
@@ -0,0 +1,2 @@
+The following issue was [if-any was_created]created[else]updated[end]:
+[detail_url]
diff --git a/templates/tracker/issue-change-notification-email.ezt b/templates/tracker/issue-change-notification-email.ezt
new file mode 100644
index 0000000..a8b3a93
--- /dev/null
+++ b/templates/tracker/issue-change-notification-email.ezt
@@ -0,0 +1,37 @@
+[is comment.sequence "0"][#
+  ]Status: [is issue.status.name ""]----[else][issue.status.name][end]
+[#]Owner: [is issue.owner.username ""]----[else][issue.owner.display_name][end][#
+  ][if-any issue.cc]
+[#  ]CC: [for issue.cc][issue.cc.display_name][if-index issue.cc last] [else], [end][end][#
+  ][end][#
+  ][if-any issue.labels]
+[#  ]Labels:[for issue.labels] [issue.labels.name][end][#
+  ][end][#
+  ][if-any issue.components]
+[#  ]Components:[for issue.components] [issue.components.path][end][#
+  ][end][#
+  ][if-any issue.blocked_on]
+[#  ]BlockedOn:[for issue.blocked_on] [if-any issue.blocked_on.visible][issue.blocked_on.display_name][end][end][#
+  ][end][#
+  ][for issue.fields][if-any issue.fields.display][if-any issue.fields.values]
+[#  ][issue.fields.field_name]:[for issue.fields.values] [issue.fields.values.val][end][end][#
+  ][end][end]
+[else][if-any comment.amendments][#
+  ]Updates:
+[#][for comment.amendments]	[comment.amendments.field_name]: [format "raw"][comment.amendments.newvalue][end]
+[#][end][#
+  ][end][end]
+[is comment.sequence "0"]New issue [issue.local_id][#
+  ][else]Comment #[comment.sequence] on issue [issue.local_id][end][#
+  ] by [comment.creator.display_name]: [format "raw"][summary][#
+][end]
+[detail_url]
+
+[if-any comment.content][#
+  ][for comment.text_runs][include "render-plain-text.ezt" comment.text_runs][end][#
+][else](No comment was entered for this change.)[#
+][end]
+[if-any comment.attachments]
+Attachments:
+[for comment.attachments]	[comment.attachments.filename]  [comment.attachments.filesizestr]
+[end][end]
diff --git a/templates/tracker/issue-chart-body.ezt b/templates/tracker/issue-chart-body.ezt
new file mode 100644
index 0000000..34ae0f7
--- /dev/null
+++ b/templates/tracker/issue-chart-body.ezt
@@ -0,0 +1,17 @@
+<mr-chart
+  projectName="[projectname]"
+  hotlistId="[if-any hotlist_id][hotlist_id][end]"
+></mr-chart>
+
+<div>
+  <div class="help" style="padding: 1em;">
+    <h2 style="font-size: 1.2em; margin: 0 0 0.5em;">Supported query parameters:</h2>
+    <span style="font-family: monospace;">
+      cc, component, hotlist, label, owner, reporter, status
+    </span>
+    <br /><br />
+    <a href="https://bugs.chromium.org/p/monorail/issues/entry?labels=Feature-Charts">
+      Please file feedback here.
+    </a>
+  </div>
+</div>
diff --git a/templates/tracker/issue-chart-controls-top.ezt b/templates/tracker/issue-chart-controls-top.ezt
new file mode 100644
index 0000000..082affb
--- /dev/null
+++ b/templates/tracker/issue-chart-controls-top.ezt
@@ -0,0 +1,9 @@
+<!-- TODO: make this cleaner by replacing it with web component. -->
+<div class="list">
+  <div class="button_set">
+    <a class="choice_chip" href="[if-any is_hotlist][else]list[end]?can=[can]&amp;q=[query]&amp;colspec=[format "url"][colspec][end]&amp;groupby=[format "url"][groupby][end]&amp;sort=[format "url"][sortspec][end]&amp;x=[grid_x_attr]&amp;y=[grid_y_attr]&amp;cells=[grid_cell_mode]">List</a>
+    <a class="choice_chip" href="[if-any is_hotlist][else]list[end]?can=[can]&amp;q=[query]&amp;colspec=[format "url"][colspec][end]&amp;groupby=[format "url"][groupby][end]&amp;sort=[format "url"][sortspec][end]&amp;x=[grid_x_attr]&amp;y=[grid_y_attr]&amp;cells=[grid_cell_mode]&amp;mode=grid">Grid</a>
+    <a class="choice_chip active_choice" href="[if-any is_hotlist][else]list[end]?can=[can]&amp;q=[query]&amp;colspec=[format "url"][colspec][end]&amp;groupby=[format "url"][groupby][end]&amp;sort=[format "url"][sortspec][end]&amp;x=[grid_x_attr]&amp;y=[grid_y_attr]&amp;cells=[grid_cell_mode]&amp;mode=chart">Chart</a>
+  </div>
+</div>
+
diff --git a/templates/tracker/issue-entry-page.ezt b/templates/tracker/issue-entry-page.ezt
new file mode 100644
index 0000000..b9889d6
--- /dev/null
+++ b/templates/tracker/issue-entry-page.ezt
@@ -0,0 +1,556 @@
+[define title]New Issue[end]
+[define category_css]css/ph_detail.css[end]
+[include "../framework/header.ezt" "hidetabs"]
+
+[# Note: base permission for this page is CreateIssue]
+
+[if-any read_only][include "../framework/read-only-rejection.ezt"]
+[else]
+
+<div id="color_control" style="margin-top: 0; padding: 3px;" class="closed [if-any code_font]codefont[end]">
+ <form action="entry.do" method="POST" style="margin: 0; padding: 0" enctype="multipart/form-data" id="create_issue_form">
+  <input type="hidden" name="token" value="[form_token]">
+  <input type="hidden" name="template_name" value="[template_name]">
+  <input type="hidden" name="star" id="star_input" value="1">
+  <table cellpadding="0" cellspacing="0" border="0" role="presentation">
+   <tr><td>
+
+    <table cellspacing="0" cellpadding="3" border="0" class="rowmajor vt" role="presentation">
+     [if-any offer_templates]
+      <tr><th><label for="template_name">Template:</label></th>
+       <td colspan="2">
+        <select name="template_name" id="template_name" data-project-name="[projectname]" ignore-dirty>
+         [for config.template_names]
+           <option role="option" value="[format "url"][config.template_names][end]" [is config.template_names template_name]selected=selected[end]>[config.template_names]</option>
+         [end]
+        </select>
+        <span id="mr-code-font-toggle-slot"></span>
+       </td>
+      </tr>
+     [else]
+      <tr>
+       <td colspan="3">
+        <span id="mr-code-font-toggle-slot"></span>
+       </td>
+      </tr>
+     [end]
+
+      <tr><th><label for="summary">Summary:</label></th>
+       <td colspan="2" class="inplace">
+        <input type="text" id="summary" name="summary" value="[initial_summary]" required data-clear-summary-on-click="[clear_summary_on_click]"
+               [if-any any_errors][else]autofocus[end]>
+        [if-any errors.summary]
+         <div class="fielderror">[errors.summary]</div>
+        [end]
+
+        [if-any any_errors][else]
+          <script type="text/javascript" nonce="[nonce]">
+            document.getElementById('summary').select();
+          </script>
+        [end]
+       </td>
+      </tr>
+
+      <tr><th rowspan="3"><label for="comment">Description:</label></th>
+       <td colspan="2">
+        <textarea style="width:100%" cols="80" rows="15" name="comment" id="comment" class="issue_text" required>[initial_description]
+</textarea> [# We want 1 final newline but 0 trailing spaces in the textarea]
+        [if-any errors.comment]
+         <div class="fielderror">[errors.comment]</div>
+        [end]
+       </td>
+      </tr>
+
+      <tr><td colspan="2">
+       [include "../features/cues-conduct.ezt"]
+       <div id="attachmentareadeventry"></div>
+      </td></tr>
+
+      <tr>
+       <td style="width: 12em">
+        [if-any allow_attachments]
+         <span id="attachprompt"><img width="16" height="16" src="/static/images/paperclip.png" border="0" alt="A paperclip">
+         <a href="#" id="attachafile">Attach a file</a></span>
+         <div id="attachmaxsize" style="margin-left:1.2em; display:none">Max. attachments: [max_attach_size]</div>
+         [if-any errors.attachments]
+          <div class="fielderror">[errors.attachments]</div>
+         [end]
+        [else]
+         <div style="color:#666">Issue attachment storage quota exceeded.</div>
+        [end]
+       </td>
+       <td id="star_cell" style="vertical-align: initial">
+        [# Note: if the user is permitted to enter an issue, they are permitted to star it.]
+        <a class="star" id="star" style="color:cornflowerblue;">&#9733;</a>
+        Notify me of issue changes, if enabled in <a id="settings" target="new" href="/hosting/settings">settings</a>
+       </td>
+      </tr>
+
+      <tr [if-any page_perms.EditIssue page_perms.EditIssueStatus][else]style="display:none;"[end]><th width="10%"><label for="statusenter">Status:</label></th>
+       <td colspan="2" class="inplace">
+       <select id="statusenter" name="status">
+         <option style="display: none" value="[initial_status]"></option>
+       </select>
+       </label>
+       </td>
+      </tr>
+      <tr [if-any page_perms.EditIssue page_perms.EditIssueOwner][else]style="display:none;"[end]><th width="10%"><label for="ownerenter">Owner:</label></th>
+       <td colspan="2">
+        <input type="text" id="ownerenter" autocomplete="off"
+               style="width:16em"
+               name="owner" value="[initial_owner]" aria-autocomplete="list" role="combobox">
+          <span class="availability_[owner_avail_state]" id="owner_avail_state"
+                style="padding-left:1em; [if-any owner_avail_message_short][else]display:none[end]">
+            &#9608;
+            <span id="owner_availability">[owner_avail_message_short]</span>
+          </span>
+        </div>
+        [if-any errors.owner]
+         <div class="fielderror">[errors.owner]</div>
+        [end]
+       </td>
+      </tr>
+
+      <tr [if-any page_perms.EditIssue page_perms.EditIssueCc][else]style="display:none;"[end]><th><label for="memberenter">Cc:</label></th>
+       <td colspan="2" class="inplace">
+        <input type="text" multiple id="memberenter" autocomplete="off" name="cc" value="[initial_cc]" aria-autocomplete="list" role="combobox">
+        [if-any errors.cc]
+         <div class="fielderror">[errors.cc]</div>
+        [end]
+       </td>
+      </tr>
+
+      [# TODO(jrobbins): page_perms.EditIssueComponent]
+      <tr [if-any page_perms.EditIssue][else]style="display:none;"[end]><th><label for="components">Components:</label></th>
+       <td colspan="2" class="inplace">
+        <input type="text" id="components" autocomplete="off" name="components" value="[initial_components]" aria-autocomplete="list" role="combobox">
+        [if-any errors.components]
+         <div class="fielderror">[errors.components]</div>
+        [end]
+       </td>
+      </tr>
+
+      [if-any uneditable_fields]
+      <tr id="res_fd_banner"><th></th>
+        <td colspan="2" class="inplace" style="text-align:left; border-radius:25px">
+          <span style="background:var(--chops-orange-50); padding:5px; margin-top:10px; padding-left:10px; padding-right:10px; border-radius:25px">
+            <span style="padding-right:7px">
+              Info: Disabled inputs occur when you are not allowed to edit that restricted field.
+            </span>
+            <i id="res_fd_message" class="material-icons inline-icon" style="font-weight:bold; font-size:14px; vertical-align: text-bottom; cursor: pointer">
+            close</i>
+          </span>
+        </td>
+      </tr>
+      [end]
+
+      <tbody [if-any page_perms.EditIssue][else]style="display:none;"[end] class="collapse">
+       [define any_fields_to_reveal]No[end]
+       [for fields]
+        [if-any fields.applicable][if-any fields.field_def.is_approval_subfield][else][if-any fields.field_def.is_phase_field][else]
+         [# TODO(jrobbins): determine applicability dynamically and update fields in JS]
+         <tr [if-any fields.display][else]class="ifExpand"[define any_fields_to_reveal]Yes[end][end]>
+          <th id="[fields.field_id]_label">[fields.field_name]:</th>
+          <td colspan="2">
+            [if-any fields.is_editable]
+              [include "field-value-widgets.ezt" fields.field_def.is_multivalued_bool "" fields.field_def.is_required_bool ""]
+              <div class="fielderror" style="display:none" id="error_custom_[fields.field_id]"></div>
+            [else]
+              <input disabled value = "
+              [for fields.values]
+                [fields.values.val]
+              [end]
+              " style="text-align:right; width:12em" class="multivalued customfield" aria-labelledby="[fields.field_id]_label">
+            [end]
+          </td>
+         <tr>
+        [end][end][end]
+       [end]
+       [is any_fields_to_reveal "Yes"]
+        <tr class="ifCollapse">
+         <td colspan="2"><a href="#" class="toggleCollapse">Show all fields</a><t/td>
+        </tr>
+       [end]
+      </tbody>
+
+      <tr [if-any page_perms.EditIssue][else]style="display:none;"[end]><th>Labels:</th>[# aria-labels added in label-fields.ezt]
+       <td colspan="2" class="labelediting">
+        [include "label-fields.ezt" "just-two" ""]
+       </td>
+      </tr>
+
+      <tbody class="collapse">
+       [if-any page_perms.EditIssue]
+       <tr class="ifCollapse">
+        <td><a href="#" class="toggleCollapse">More options</a></td>
+       </tr>
+       [end]
+
+       <tr [if-any page_perms.EditIssue][else]style="display:none;"[end] class="ifExpand"><th style="white-space:nowrap"><label for="blocked_on">Blocked on:</label></th>
+        <td class="inplace" colspan="2">
+         <input type="text" name="blocked_on" id="blocked_on" value="[initial_blocked_on]">
+         [if-any errors.blocked_on]
+          <div class="fielderror">[errors.blocked_on]</div>
+         [end]
+        </td>
+       </tr>
+       <tr [if-any page_perms.EditIssue][else]style="display:none;"[end] class="ifExpand"><th><label for="blocking">Blocking:</label></th>
+        <td class="inplace" colspan="2">
+         <input type="text" name="blocking" id="blocking" value="[initial_blocking]" />
+         [if-any errors.blocking]
+          <div class="fielderror">[errors.blocking]</div>
+         [end]
+        </td>
+       </tr>
+
+       <tr [if-any page_perms.EditIssue][else]style="display:none;"[end] class="ifExpand"><th><label for="hotlistsenter">Hotlists:</label></th>
+        <td class="inplace" colspan="2">
+         <input type="text" name="hotlists" autocomplete="off" id="hotlistsenter" value="[initial_hotlists]" />
+         [if-any errors.hotlists]
+          <div class="fielderror">[errors.hotlists]</div>
+         [end]
+        </td>
+       </tr>
+     </tbody>
+
+     [if-any approvals]
+        <tr>
+          <th>Launch Gates:</th>
+          <td colspan="7">
+            [include "launch-gates-widget.ezt"]
+          </td>
+        </tr>
+     [end]
+
+     [for fields][if-any fields.applicable][if-any fields.field_def.is_approval_subfield]
+     <tr is="subfield-row">
+       <th>[fields.field_def.parent_approval_name] [fields.field_name]:</th>
+       <td colspan="2">
+         [if-any fields.is_editable]
+           [include "field-value-widgets.ezt" False "tmpl" False ""]
+           <div class="fielderror" style="display:none" id="error_custom_[fields.field_id]"></div>
+         [else]
+           <input disabled value = "
+           [for fields.values]
+             [fields.values.val]
+           [end]
+           " style="text-align:right; width:12em" class="multivalued customfield" aria-labelledby="[fields.field_id]_label">
+         [end]
+       </td>
+     </tr>
+    [end][end][end]
+
+    [for issue_phase_names]
+      [for fields]
+        [is fields.phase_name issue_phase_names]
+        <tr>
+          <th>[issue_phase_names].[fields.field_name]:</th>
+            <td colspan="2">
+              [if-any fields.is_editable]
+                [include "field-value-widgets.ezt" False "tmpl" False issue_phase_names]
+                <div class="fielderror" style="display:none" id="error_custom_[issue_phase_names]_[fields.field_id]"></div>
+              [else]
+                <input disabled value = "
+                [for fields.values]
+                  [fields.values.val]
+                [end]
+                " style="text-align:right; width:12em" class="multivalued customfield" aria-labelledby="[fields.field_id]_label">
+              [end]
+            </td>
+          </th>
+        </tr>
+      [end][end][end]
+
+     [include "../framework/label-validation-row.ezt"]
+     [include "../framework/component-validation-row.ezt"]
+    </table>
+
+    <div style="padding:6px">
+     <input type="submit" id="submit_btn" name="btn" value="Submit issue">
+     <input type="button" id="discard" name="nobtn" value="Discard">
+    </div>
+
+   </td>
+   </tr>
+  </table>
+ </form>
+</div>
+
+[include "../features/filterrules-preview.ezt"]
+
+<div style="margin-top:5em; margin-left: 8px;">
+  Problems submitting issues?
+  <a href="#" id="new-issue-feedback-link">
+    Send feedback
+  </a>
+</div>
+
+<div id="helparea"></div>
+
+<script type="text/javascript" nonce="[nonce]">
+runOnLoad(function() {
+  window.getTSMonClient().recordIssueEntryTiming();
+
+  if ($('launch-gates-table')) {
+    $('launch-gates-table').classList.remove('hidden');
+  }
+
+  if ($("template_name")) {
+    $("template_name").addEventListener("change", function(event) {
+      _switchTemplate(event.target.getAttribute("data-project-name"),
+                      event.target.value)
+    });
+  }
+
+  if ($("summary")) {
+    var clearSummaryOnClick = $("summary").getAttribute("data-clear-summary-on-click");
+    if (clearSummaryOnClick) {
+      $("summary").addEventListener("keydown", function(event) {
+        _clearOnFirstEvent('[format "js"][initial_summary][end]');
+      });
+    }
+    $("summary").addEventListener("click", function(event) {
+      if (clearSummaryOnClick) {
+        _clearOnFirstEvent('[format "js"][initial_summary][end]');
+      }
+      checksubmit();
+    });
+    $("summary").addEventListener("focus", function(event) {
+      _acrob(null);
+      _acof(event);
+    });
+    $("summary").addEventListener("keyup", function(event) {
+      checksubmit();
+      return true;
+    });
+  }
+
+  if ($("settings")) {
+    $("settings").addEventListener("focus", function(event) {
+      _acrob(null);
+    });
+  }
+  if ($("statusenter")) {
+    _loadStatusSelect("[projectname]", "statusenter", "[initial_status]");
+    $("statusenter").addEventListener("focus", function(event) {
+      _acrob(null);
+    });
+  }
+  if($("res_fd_message")) {
+    $("res_fd_message").onclick = function(){
+      $("res_fd_banner").classList.add("hidden");
+    };
+  };
+
+  if ($("submit_btn")) {
+    $("submit_btn").addEventListener("focus", function(event) {
+      _acrob(null);
+    });
+    $("submit_btn").addEventListener("click", function(event) {
+      _acrob(null);
+      _trimCommas();
+      userMadeChanges = false;
+    });
+  }
+  if ($("discard")) {
+    $("discard").addEventListener("focus", function(event) {
+      _acrob(null);
+    });
+    $("discard").addEventListener("click", function(event) {
+      _acrob(null);
+      _confirmDiscardEntry(event.target);
+      event.preventDefault();
+    });
+  }
+  if ($("new-issue-feedback-link")) {
+    $("new-issue-feedback-link").addEventListener("click", function(event) {
+      userfeedback.api.startFeedback({
+          'productId': '5208992',  // Required.
+          'productVersion': '[app_version]'  // Optional.
+        });
+    })
+  }
+
+  window.allowSubmit = true;
+  $("create_issue_form").addEventListener("submit", function() {
+    if (allowSubmit) {
+      allowSubmit = false;
+      $("submit_btn").value = "Creating issue...";
+      $("submit_btn").disabled = "disabled";
+    }
+    else {
+      event.preventDefault();
+    }
+  });
+
+  var _blockIdsToListeners = [[]"blocked_on", "blocking", "hotlistsenter"];
+  for (var i = 0; i < _blockIdsToListeners.length; i++) {
+    var id = _blockIdsToListeners[[]i];
+    if ($(id)) {
+      $(id).addEventListener("focus", function(event) {
+        _acrob(null);
+        _acof(event);
+      });
+    }
+  }
+
+  var _idsToAddDefaultListeners = [[]"ownerenter", "memberenter", "components"];
+  for (var i = 0; i < _idsToAddDefaultListeners.length; i++) {
+    var id = _idsToAddDefaultListeners[[]i];
+    if ($(id)) {
+      $(id).addEventListener("focus", function(event) {
+        _acrob(null);
+        _acof(event);
+      });
+    }
+  }
+
+  var _elementsToAddPresubmit = document.querySelectorAll(
+      "#create_issue_form input, #create_issue_form select");
+  var debounced_presubmit = debounce(TKR_presubmit, 500);
+  for (var i = 0; i < _elementsToAddPresubmit.length; i++) {
+    var el = _elementsToAddPresubmit[[]i];
+    el.addEventListener("keyup", debounced_presubmit);
+    el.addEventListener("change", debounced_presubmit);
+  }
+  debounced_presubmit();
+
+  if ($("attachafile")) {
+    $("attachafile").addEventListener("click", function(event) {
+      _addAttachmentFields("attachmentareadeventry");
+      event.preventDefault();
+    });
+  }
+
+  document.addEventListener('keydown', function(event) {
+    if (event.key === 'Enter' && (event.ctrlKey || event.metaKey)) {
+      event.preventDefault();
+      $('submit_btn').click();
+    }
+  })
+
+  window.onsubmit = function() {
+    TKR_initialFormValues = TKR_currentFormValues();
+  };
+
+  window.onbeforeunload = function() {
+    if (TKR_isDirty()) {
+      // This message is ignored in recent versions of Chrome and Firefox.
+      return "You have unsaved changes. Leave this page and discard them?";
+    }
+  };
+
+  _lfidprefix = 'labelenter';
+  [if-any any_errors]
+   function _clearOnFirstEvent(){}
+  [end]
+
+  [if-any page_perms.EditIssue page_perms.EditIssueStatus page_perms.EditIssueOwner page_perms.EditIssueCc]
+    setTimeout(_forceProperTableWidth, 100);
+  [end]
+
+  [if-any page_perms.EditIssue]
+   _exposeExistingLabelFields();
+  [end]
+
+  var field_error;
+  [if-any  errors.custom_fields]
+    [for errors.custom_fields]
+      field_error = document.getElementById('error_custom_' + [errors.custom_fields.field_id]);
+      field_error.textContent = "[errors.custom_fields.message]";
+      field_error.style.display = "";
+    [end]
+  [end]
+
+
+
+function checksubmit() {
+  var restrict_to_known = [if-any restrict_to_known]true[else]false[end];
+  var confirmmsg = document.getElementById('confirmmsg');
+  var cg = document.getElementById('cg');
+  var label_blocksubmitmsg = document.getElementById('blocksubmitmsg');
+  var component_blocksubmitmsg = document.getElementById('component_blocksubmitmsg');
+
+  // Check for templates that require components.
+  var component_required = [if-any component_required]true[else]false[end];
+  var components = document.getElementById('components');
+  if (components && component_required && components.value == "") {
+    component_blocksubmitmsg.textContent = "You must specify a component for this template.";
+  } else {
+    component_blocksubmitmsg.textContent = "";
+  }
+
+  var submit = document.getElementById('submit_btn');
+  var summary = document.getElementById('summary');
+  if ((restrict_to_known && confirmmsg && confirmmsg.textContent) ||
+      (label_blocksubmitmsg && label_blocksubmitmsg.textContent) ||
+      (component_blocksubmitmsg && component_blocksubmitmsg.textContent) ||
+      (cg && cg.value == "") ||
+      (!allowSubmit) ||
+      (!summary.value [if-any must_edit_summary]|| summary.value == '[format "js"][template_summary][end]'[end])) {
+     submit.disabled='disabled';
+  } else {
+     submit.disabled='';
+  }
+}
+checksubmit();
+setInterval(checksubmit, 700); [# catch changes that were not keystrokes, e.g., paste menu item.]
+
+$("star").addEventListener("click", function (event) {
+    _TKR_toggleStarLocal($("star"), "star_input");
+});
+
+  const mrCodeFontToggle = document.createElement('mr-pref-toggle');
+  mrCodeFontToggle.style = 'float:right; margin: 3px;';
+  [if-any code_font]
+    mrCodeFontToggle.initialValue = true;
+  [end]
+  [if-any logged_in_user]
+    mrCodeFontToggle.userDisplayName = "[logged_in_user.email]";
+  [end]
+  mrCodeFontToggle.label = "Code";
+  mrCodeFontToggle.title = "Code font";
+  mrCodeFontToggle.prefName = "code_font";
+  $('mr-code-font-toggle-slot').appendChild(mrCodeFontToggle);
+  mrCodeFontToggle.fetchPrefs();
+  mrCodeFontToggle.addEventListener('font-toggle', function(e) {
+    const checked = e.detail.checked;
+    const ancestor = $('color_control');
+    if (ancestor) {
+      if (checked) {
+        ancestor.classList.add('codefont');
+      } else {
+        ancestor.classList.remove('codefont');
+      }
+    }
+  });
+
+
+});
+</script>
+
+<script type="text/javascript" defer src="/static/third_party/js/keys.js?version=[app_version]" nonce="[nonce]"></script>
+<script type="text/javascript" defer src="/static/third_party/js/skipper.js?version=[app_version]" nonce="[nonce]"></script>
+<script type="text/javascript" defer src="https://support.google.com/inapp/api.js" nonce="[nonce]"></script>
+<script type="text/javascript" nonce="[nonce]">
+runOnLoad(function() {
+  _setupKibblesOnEntryPage('[project_home_url]/issues/list');
+});
+</script>
+
+[end]
+
+[include "field-value-widgets-js.ezt"]
+[include "../framework/footer.ezt"]
+
+<script type="text/javascript" nonce="[nonce]">
+runOnLoad(function() {
+  if (typeof(ClientLogger) === "function") {
+    const l = new ClientLogger("issues");
+    l.logStart("new-issue", "user-time");
+    document.forms.create_issue_form.addEventListener('submit', function() {
+      l.logStart("new-issue", "server-time");
+    });
+  }
+});
+</script>
diff --git a/templates/tracker/issue-export-page.ezt b/templates/tracker/issue-export-page.ezt
new file mode 100644
index 0000000..c7892d7
--- /dev/null
+++ b/templates/tracker/issue-export-page.ezt
@@ -0,0 +1,39 @@
+[define category_css]css/ph_detail.css[end]
+[include "../framework/header.ezt" "showtabs"]
+
+<h3>Issue export</h3>
+
+<form action="export/json" method="GET">
+  [# We use xhr_token here because we are doing a GET on a JSON servlet.]
+  <input type="hidden" name="token" value="[xhr_token]">
+  <table cellpadding="3" class="rowmajor vt">
+    <tr>
+     <th>Format</th>
+     <td style="width:90%">JSON</td>
+   </tr>
+   <tr>
+     <select id="can" name="can">
+       [include "issue-can-widget.ezt" "search"]
+     </select>
+     <label for="searchq"> for </label>
+     <span id="qq"><input type="text" size="[q_field_size]" id="searchq" name="q"
+         value="[query]" autocomplete="off"></span>
+   </tr>
+   <tr>
+     <th>Start</th>
+     <td><input type="number" size="7" name="start" value="[initial_start]"></td>
+   </tr>
+   <tr>
+     <th>Num</th>
+     <td><input type="number" size="4" name="num" value="[initial_num]"></td>
+   </tr>
+   <tr>
+     <th></th>
+     <td><input type="submit" name="btn" value="Submit"></td>
+   </tr>
+ </table>
+</form>
+
+
+
+[include "../framework/footer.ezt"]
diff --git a/templates/tracker/issue-grid-body.ezt b/templates/tracker/issue-grid-body.ezt
new file mode 100644
index 0000000..94f08ea
--- /dev/null
+++ b/templates/tracker/issue-grid-body.ezt
@@ -0,0 +1,75 @@
+[if-any results]
+
+ [is grid_x_attr "--"][else]
+  <tr>
+   [is grid_y_attr "--"][else]<th>&nbsp;</th>[end]
+   [for grid_x_headings]
+    <th>[grid_x_headings]</th>
+   [end]
+  </tr>
+ [end]
+
+ [for grid_data]
+  <tr class="grid">
+   [is grid_y_attr "--"][else]<th>[grid_data.grid_y_heading]</th>[end]
+
+   [for grid_data.cells_in_row]
+    <td class="vt hoverTarget [is grid_cell_mode "tiles"][else]idcount[end]">
+     [for grid_data.cells_in_row.tiles]
+      [is grid_cell_mode "tiles"]
+       [include "issue-grid-tile.ezt" grid_data.cells_in_row.tiles.starred grid_data.cells_in_row.tiles.local_id grid_data.cells_in_row.tiles.status grid_data.cells_in_row.tiles.summary grid_data.cells_in_row.tiles.issue_url grid_data.cells_in_row.tiles.data_idx]
+      [end]
+      [is grid_cell_mode "ids"]
+       <a title="[grid_data.cells_in_row.tiles.summary]"
+          href=[grid_data.cells_in_row.tiles.issue_url] class="computehref" data-idx="[grid_data.cells_in_row.tiles.data_idx]">[if-any is_hotlist][grid_data.cells_in_row.tiles.issue_ref][else][grid_data.cells_in_row.tiles.local_id][end]</a>
+      [end]
+     [end]
+     [is grid_cell_mode "counts"]
+      [is grid_data.cells_in_row.count "0"]
+      [else]
+       [is grid_data.cells_in_row.count "1"]
+        <a href=[for grid_data.cells_in_row.tiles][grid_data.cells_in_row.tiles.issue_url][end]
+           >[grid_data.cells_in_row.count] item</a>
+       [else]
+        <a href="[if-any is_hotlist][else]list[end]?can=[can]&amp;q=[grid_data.cells_in_row.drill_down][query]">[grid_data.cells_in_row.count] items</a>
+       [end]
+      [end]
+
+     [end]
+    </td>
+   [end]
+  </tr>
+ [end]
+
+[else]
+
+ <tr>
+  <td colspan="40" class="id" style="cursor:default">
+   <div style="padding: 3em; text-align: center">
+    [if-any is_hotlist]
+     This hotlist currently has no issues.<br>
+     [if-any owner_permissions editor_permissions]
+      Select 'Add issues...' in the above 'Actions...' dropdown menu to add some.
+     [end]
+    [else]
+     [if-any project_has_any_issues]
+      Your search did not generate any results.  <br>
+      [is can "1"]
+       You may want to remove some terms from your query.<br>
+      [else]
+       You may want to try your search over <a href="list?can=1&amp;q=[query]&amp;x=[grid_x_attr]&amp;y=[grid_y_attr]&amp;mode=grid">all issues</a>.<br>
+      [end]
+     [else]
+      This project currently has no issues.<br>
+      [if-any page_perms.CreateIssue]
+       [if-any read_only][else]
+        You may want to enter a <a class="id" href="entry">new issue</a>.
+       [end]
+      [end]
+     [end]
+    [end]
+   </div>
+  </td>
+ </tr>
+
+[end]
diff --git a/templates/tracker/issue-grid-controls-top.ezt b/templates/tracker/issue-grid-controls-top.ezt
new file mode 100644
index 0000000..5d63e67
--- /dev/null
+++ b/templates/tracker/issue-grid-controls-top.ezt
@@ -0,0 +1,59 @@
+<div class="list">
+
+<div class="button_set">
+  <a class="choice_chip" href="[if-any is_hotlist][else]list[end]?can=[can]&amp;q=[query]&amp;colspec=[format "url"][colspec][end]&amp;groupby=[format "url"][groupby][end]&amp;sort=[format "url"][sortspec][end]&amp;x=[grid_x_attr]&amp;y=[grid_y_attr]&amp;cells=[grid_cell_mode]">List</a><span
+  class="choice_chip active_choice">Grid</span>
+  <a class="choice_chip" href="[if-any is_hotlist][else]list[end]?can=[can]&amp;q=[query]&amp;colspec=[format "url"][colspec][end]&amp;groupby=[format "url"][groupby][end]&amp;sort=[format "url"][sortspec][end]&amp;x=[grid_x_attr]&amp;y=[grid_y_attr]&amp;cells=[grid_cell_mode]&amp;mode=chart">Chart</a>
+</div>
+
+[if-any pagination]
+ [if-any pagination.visible]
+  <div class="pagination">
+   [is pagination.total_count "1"]
+    [pagination.total_count] issue shown
+   [else]
+    [if-any grid_limited][grid_shown] issues of [end]
+    [pagination.total_count] issues shown
+   [end]
+  </div>
+ [end]
+[end]
+
+ <form id="colspecform" action="[if-any is_hotlist][else]list[end]" method="GET" style="display:inline">
+  <input type="hidden" name="can" value="[can]">
+  <input type="hidden" name="q" value="[query]">
+  <input type="hidden" name="colspec" id="colspec" value="[colspec]">
+  <input type="hidden" name="sort" value="[sortspec]">
+  <input type="hidden" name="groupby" value="[groupby]">
+  <input type="hidden" name="mode" value="grid">
+<span>Rows:</span>
+<select name="y" class="drop-down-bub">
+ <option value="--" [if-any grid_y_attr][else]selected=selected[end]>None</option>
+ [for grid_axis_choices]
+  <option value="[grid_axis_choices]"
+          [is grid_axis_choices grid_y_attr]selected=selected[end]
+    >[grid_axis_choices]</option>
+ [end]
+</select>
+
+<span style="margin-left:.7em">Cols:</span>
+<select name="x" class="drop-down-bub">
+ <option value="--" [if-any grid_x_attr][else]selected=selected[end]>None</option>
+ [for grid_axis_choices]
+  <option value="[grid_axis_choices]"
+          [is grid_axis_choices grid_x_attr]selected=selected[end]
+    >[grid_axis_choices]</option>
+ [end]
+</select>
+
+<span style="margin-left:.7em">Cells:</span>
+<select name="cells" class="drop-down-bub">
+ <option value="tiles" [is grid_cell_mode "tiles"]selected=selected[end]>Tiles</option>
+ <option value="ids" [is grid_cell_mode "ids"]selected=selected[end]>IDs</option>
+ <option value="counts" [is grid_cell_mode "counts"]selected=selected[end]>Counts</option>
+</select>
+
+<input type="submit" name="nobtn" style="font-size:90%; margin-left:.5em" value="Update">
+
+</form>
+</div>
diff --git a/templates/tracker/issue-grid-tile.ezt b/templates/tracker/issue-grid-tile.ezt
new file mode 100644
index 0000000..43e6a04
--- /dev/null
+++ b/templates/tracker/issue-grid-tile.ezt
@@ -0,0 +1,27 @@
+<div class="gridtile">
+ <table cellspacing="0" cellpadding="0">
+  <tr>
+   <td class="id">
+    [if-any read_only][else]
+     [if-any page_perms.SetStar]
+      <a class="star"
+       style="color:[if-any arg0]cornflowerblue[else]gray[end]; text-decoration:none;"
+       title="[if-any arg0]Un-s[else]S[end]tar this issue"
+       data-project-name="[projectname]" data-local-id="[arg1]">
+       [if-any arg0]&#9733;[else]&#9734;[end]
+      </a>
+     [end]
+    [end]
+    <a href=[arg4] class="computehref" data-idx=[arg5]>[arg1]</a>
+   </td>
+   <td class="status">[arg2]</td>
+  </tr>
+  <tr>
+   <td colspan="2">
+    <div>
+     <a href=[arg4] class="computehref" data-idx=[arg5]>[arg3]</a>
+    </div>
+   </td>
+  </tr>
+ </table>
+</div>
diff --git a/templates/tracker/issue-hidden-fields.ezt b/templates/tracker/issue-hidden-fields.ezt
new file mode 100644
index 0000000..eaeeedf
--- /dev/null
+++ b/templates/tracker/issue-hidden-fields.ezt
@@ -0,0 +1,14 @@
+[# This template part renders important hidden fields for issue update forms.
+]
+
+<input type="hidden" name="_charset_" value="">
+<input type="hidden" name="token" value="[form_token]">
+<input type="hidden" name="id" value="[issue.local_id]">
+<input type="hidden" name="can" value="[can]">
+<input type="hidden" name="q" value="[query]">
+<input type="hidden" name="colspec" value="[colspec]">
+<input type="hidden" name="sort" value="[sortspec]">
+<input type="hidden" name="groupby" value="[groupby]">
+<input type="hidden" name="start" value="[start]">
+<input type="hidden" name="num" value="[num]">
+<input type="hidden" name="pagegen" value="[pagegen]">
diff --git a/templates/tracker/issue-hovercard.ezt b/templates/tracker/issue-hovercard.ezt
new file mode 100644
index 0000000..b70341b
--- /dev/null
+++ b/templates/tracker/issue-hovercard.ezt
@@ -0,0 +1,6 @@
+[# Show a small dialog box allows the user to quickly view one issue.]
+
+<div id="infobubble">
+ <div id="peekarea" style="width:72em; padding:0"
+      ><div class="loading">Loading...</div></div>
+</div>
diff --git a/templates/tracker/issue-import-page.ezt b/templates/tracker/issue-import-page.ezt
new file mode 100644
index 0000000..524486d
--- /dev/null
+++ b/templates/tracker/issue-import-page.ezt
@@ -0,0 +1,44 @@
+[define category_css]css/ph_detail.css[end]
+[include "../framework/header.ezt" "showtabs"]
+
+<h3>Issue export</h3>
+
+[if-any import_errors]
+  [# This is actually used to show both errors and progress messages
+     after a successful import.]
+  <div class="error" style="margin-bottom:1em">
+    Import event log:
+    <ul>
+      [for import_errors]
+        <li>[import_errors]</li>
+      [end]
+    </ul>
+  </div>
+[end]
+
+
+<form action="import.do" enctype="multipart/form-data" method="POST">
+  <input type="hidden" name="token" value="[form_token]">
+  <table cellpadding="3" class="rowmajor vt">
+    <tr>
+     <th>Format</th>
+     <td style="width:90%">JSON</td>
+   </tr>
+   <tr>
+     <th>File</th>
+     <td><input type="file" name="jsonfile"></td>
+   </tr>
+   <tr>
+     <th>Pre-check only</th>
+     <td><input type="checkbox" name="pre_check_only"></td>
+   </tr>
+   <tr>
+     <th></th>
+     <td><input type="submit" name="btn" value="Submit"></td>
+   </tr>
+ </table>
+</form>
+
+
+
+[include "../framework/footer.ezt"]
diff --git a/templates/tracker/issue-list-controls-bottom.ezt b/templates/tracker/issue-list-controls-bottom.ezt
new file mode 100644
index 0000000..b1905a1
--- /dev/null
+++ b/templates/tracker/issue-list-controls-bottom.ezt
@@ -0,0 +1,7 @@
+<div class="list-foot">
+[if-any logged_in_user]
+  <a href="[csv_link]&token=[form_token]" style="float:right; margin-left: 1em">CSV</a>
+[end]
+
+[include "../framework/artifact-list-pagination-part.ezt"]
+</div>
diff --git a/templates/tracker/issue-list-controls-top.ezt b/templates/tracker/issue-list-controls-top.ezt
new file mode 100644
index 0000000..d308570
--- /dev/null
+++ b/templates/tracker/issue-list-controls-top.ezt
@@ -0,0 +1,108 @@
+<div class="list">
+  <div class="button_set">
+   <span class="active_choice choice_chip">List</span>
+   <a class="choice_chip" href="[if-any is_hotlist][else]list[end]?can=[can]&amp;q=[query]&amp;colspec=[format "url"][colspec][end]&amp;groupby=[format "url"][groupby][end]&amp;sort=[format "url"][sortspec][end]&amp;x=[grid_x_attr]&amp;y=[grid_y_attr]&amp;cells=[grid_cell_mode]&amp;mode=grid">Grid</a>
+   <a class="choice_chip" href="[if-any is_hotlist][else]list[end]?can=[can]&amp;q=[query]&amp;colspec=[format "url"][colspec][end]&amp;groupby=[format "url"][groupby][end]&amp;sort=[format "url"][sortspec][end]&amp;x=[grid_x_attr]&amp;y=[grid_y_attr]&amp;cells=[grid_cell_mode]&amp;mode=chart">Chart</a>
+  </div>
+
+   [include "../framework/artifact-list-pagination-part.ezt"]
+   [include "update-issues-hotlists-dialog.ezt"]
+
+   [if-any page_perms.EditIssue]
+     [if-any is_cross_project][else]
+       <span style="margin:0 .7em">Select:
+         <a id="selectall" href="#">All</a>
+         <a id="selectnone" href="#">None</a>
+       </span>
+     [end]
+    <select id="moreactions" class="drop-down-bub">
+     <option value="moreactions" disabled="disabled" selected="selected">Actions...</option>
+     <option value="colspec">Change columns...</option>
+     [if-any is_cross_project][else][# TODO(jrobbins): cross-project bulk edit]
+       <option value="bulk">Bulk edit...</option>
+     [end]
+     [if-any is_cross_project][else][# TODO(jrobbins): cross-project spam flagging]
+       <option value="flagspam">Flag as spam...</option>
+       <option value="unflagspam">Un-flag as spam...</option>
+     [end]
+     <option value="addtohotlist">Add to hotlist...</option>
+    </select>
+    <span id='bulk-action-loading' class='loading' style='visibility:hidden'>Processing</span>
+   [end]
+
+   [if-any hotlist_id][if-any logged_in_user]
+   <span style="margin:0 .7em">Select:
+     <a id="selectall" href="#">All</a>
+     <a id="selectnone" href="#">None</a>
+   </span>
+   <select id="moreactions" class="drop-down-bub">
+     <option value="moreactions" disabled="disabled" [if-any add_issues_selected][else]selected="selected"[end]>Actions...</option>
+     [if-any owner_permissions editor_permissions]
+     <option value="addissues" [if-any add_issues_selected]selected="selected"[end]>Add issues...</option>
+     <option value="removeissues">Remove issues...</options>
+     <option value="colspec">Change columns...</option>
+   [end]
+     <option value="addtohotlist">Add to hotlist...</option>
+   </select>
+   [end][end]
+
+
+   <form id="colspecform" action=[if-any hotlist_id]"[hotlist.name]"[else]"list"[end] method="GET" autocomplete="off"
+         style="display:inline; margin-left:1em">
+    <input type="hidden" name="can" value="[can]">
+    <input type="hidden" name="q" value="[query]">
+    <input type="hidden" name="sort" value="[sortspec]">
+    <input type="hidden" id="groupbyspec" name="groupby" value="[groupby]">
+    <span id="columnspec" style="display:none; font-size:90%">
+      <span>Columns:</span>
+      <span id="colspec_field"><input type="text" size="60" name="colspec"
+                   value="[colspec]"></span>
+      <input type="submit" name="nobtn" value="Update">
+      [# TODO(jrobbins): <a href="TODO">Learn more</a> ]
+    </span>
+   </form>
+</div>
+
+[if-any is_hotlist]
+<form id='bulkremoveissues' method="POST" action="/u/[viewed_user_id]/hotlists/[hotlist.name].do">
+<input type="hidden" name="token" value="[edit_hotlist_token]">
+  <input type="hidden" id="current_col_spec" name="current_col_spec" value="[col_spec]">
+  <input type="hidden" id="bulk_remove_local_ids" name="remove_local_ids">
+  <input type ="hidden" id="bulk_remove_value" name = "remove" value="false">
+  <span id="addissuesspec" style="display:none; font-size:90%">
+    <span>Issues:</span>
+    <span id="issues_field"><input type="text" size="60" name="add_local_ids"
+                   value="[add_local_ids]" placeholder="[placeholder]"></span>
+    <input type="submit" name="nobtn" value="Add Issues">
+  </span>
+  [if-any errors.issues]
+  <div class="fielderror">[errors.issues]</div>
+  [end]
+  <div class="fielderror">&nbsp;
+    <span id="add_local_idsfeedback">
+       [if-any errors.add_local_ids][errors.add_local_ids][end]
+    </span>
+  </div>
+</form>
+[end]
+
+<script type="text/javascript" nonce="[nonce]">
+runOnLoad(function() {
+  if ($("selectall")) {
+    $("selectall").addEventListener("click", function() { _selectAllIssues(); });
+  }
+  if ($("selectnone")) {
+    $("selectnone").addEventListener("click", function() { _selectNoneIssues(); });
+  }
+  if ($("moreactions")) {
+    $("moreactions").addEventListener("change", function(event) {
+        _handleListActions(event.target);
+    });
+    if ($("moreactions").value == 'addissues') {
+      _showID('addissuesspec');
+    }
+  }
+  window.__hotlists_dialog.onResponse = onAddIssuesResponse;
+  window.__hotlists_dialog.onFailure = onAddIssuesFailure;
+});
+</script>
diff --git a/templates/tracker/issue-list-csv.ezt b/templates/tracker/issue-list-csv.ezt
new file mode 100644
index 0000000..62de9c6
--- /dev/null
+++ b/templates/tracker/issue-list-csv.ezt
@@ -0,0 +1,16 @@
+[# Prefix response body with over 1024 bytes of static content to avoid content sniffing.]
+"-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-"
+This file contains the same information as the issue list web page, but in CSV format.
+You can adjust the columns of the CSV file by adjusting the columns shown on the web page
+before clicking the CSV link.
+"-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-"
+
+
+[for panels][# There will always be exactly one panel.][for panels.ordered_columns]"[panels.ordered_columns.name]"[if-index panels.ordered_columns last][else],[end][end][end]
+[for table_data][for table_data.cells][is table_data.cells.type "ID"]"[table_data.local_id]",[else]"[format "raw"][if-any table_data.cells.values][for table_data.cells.values][is table_data.cells.type "issues"][table_data.cells.values.item.id][else][table_data.cells.values.item][end][if-index table_data.cells.values last][else], [end][end][end][end]"[if-index table_data.cells last][else],[end][end][end]
+[end]
+
+[if-any next_csv_link]
+This file is truncated to [item_count] out of [pagination.total_count] total results.
+See [next_csv_link] for the next set of results.
+[end]
diff --git a/templates/tracker/issue-list-headings.ezt b/templates/tracker/issue-list-headings.ezt
new file mode 100644
index 0000000..6ec4e41
--- /dev/null
+++ b/templates/tracker/issue-list-headings.ezt
@@ -0,0 +1,31 @@
+[# arg0 is the ordered_columns argument that gives the name and index of each column.]
+
+<thead id="resultstablehead">
+<tr id="headingrow"><th style="border-left: 0"> &nbsp; </th>
+ [for panels.ordered_columns]
+  [is panels.ordered_columns.name "Summary"]
+   <th class="col_[panels.ordered_columns.col_index]" nowrap="nowrap" id="summaryheading"
+       data-col-index="[panels.ordered_columns.col_index]" width="100%"
+       ><a href="#" style="text-decoration: none">Summary + Labels <span class="indicator">&#9660;</span></a></th>
+  [else]
+   [is panels.ordered_columns.name "ID"]
+    <th class="col_[panels.ordered_columns.col_index]" nowrap="nowrap"
+        data-col-index="[panels.ordered_columns.col_index]"
+       ><a href="#" style="text-decoration: none">[panels.ordered_columns.name] <span class="indicator">&#9660;</span></a></th>
+   [else]
+    <th class="col_[panels.ordered_columns.col_index]"
+        data-col-index="[panels.ordered_columns.col_index]"
+       ><a href="#" style="text-decoration: none">[panels.ordered_columns.name]&nbsp;<span class="indicator">&#9660;</span></a></th>
+   [end]
+  [end]
+ [end]
+ [if-any is_hotlist]
+ <th data-col-index="dot" style="width:3ex"><a href="#columnprefs"
+     class="dotdotdot" aria-label="Column list">...</a></th>
+ [else]
+ <th style="padding: 0;">
+   <ezt-show-columns-connector colspec="[colspec]" phasespec="[phasespec]"></ezt-show-columns-connector>
+ </th>
+ [end]
+</tr>
+</thead>
diff --git a/templates/tracker/issue-list-js.ezt b/templates/tracker/issue-list-js.ezt
new file mode 100644
index 0000000..1eb1aad
--- /dev/null
+++ b/templates/tracker/issue-list-js.ezt
@@ -0,0 +1,110 @@
+<script type="text/javascript" nonce="[nonce]">
+runOnLoad(function() {
+
+  [# Pass the list of column names from HTML to JS ]
+  window._allColumnNames = [
+    [for column_values]'[column_values.column_name]'[if-index column_values last][else], [end][end]
+    ];
+
+  [# Update the issue link hrefs on-load and whenever the column-spec changes.]
+  _ctxCan = [can];
+  _ctxQuery = "[format "js"][query][end]";
+  _ctxSortspec = "[format "js"][sortspec][end]";
+  _ctxGroupBy = "[format "js"][groupby][end]";
+  _ctxDefaultColspec = "[format "js"][default_colspec][end]";
+  _ctxStart = [start];
+  _ctxNum = [num];
+  _ctxResultsPerPage = [default_results_per_page];
+  _ctxHotlistID = "[hotlist_id]";
+  _ctxArgs = _formatContextQueryArgs();
+
+  function _goIssue(issueIndex, newWindow) {
+    var url = _makeIssueLink(issueRefs[[]issueIndex]);
+    _go(url, newWindow);
+  }
+  // Added to enable calling from TKR_openArtifactAtCursor
+  window._goIssue = _goIssue;
+
+  window.issueRefs = [[]
+   [for table_data]
+     {project_name: "[format "js"][table_data.project_name][end]",
+      id: [table_data.local_id]}[if-index table_data last][else],[end][end]
+   ];
+
+  function _handleResultsClick(event) {
+    var target = event.target;
+    if (event.button >= 3)
+      return;
+    if (target.classList.contains("label"))
+      return;
+    if (target.classList.contains("rowwidgets") || target.parentNode.classList.contains("rowwidgets"))
+      return;
+    while (target && target.tagName != "TR") target = target.parentNode;
+    if ('[is_hotlist]') {
+       if (!target.attributes[[]"issue-context-url"]) return;
+       _go(target.attributes[[]"issue-context-url"].value, (event.metaKey || event.ctrlKey || event.button == 1));
+       }
+    else {
+       if (!target.attributes[[]"data-idx"]) return;
+       _goIssue(target.attributes[[]"data-idx"].value,
+       (event.metaKey || event.ctrlKey || event.button == 1));
+         }
+  };
+  [if-any table_data]
+    _addClickListener($("resultstable"), _handleResultsClick);
+  [end]
+
+  var issueCheckboxes = document.getElementsByClassName("checkRangeSelect");
+  for (var i = 0; i < issueCheckboxes.length; ++i) {
+    var el = issueCheckboxes[[]i];
+    el.addEventListener("click", function (event) {
+        _checkRangeSelect(event, event.target);
+        _highlightRow(event.target);
+    });
+  }
+
+  function _handleHeaderClick(event) {
+    var target = event.target;
+    while (target && target.tagName != "TH") target = target.parentNode;
+    var colIndex = target.getAttribute("data-col-index");
+    if (colIndex) {
+      _showBelow("pop_" + colIndex, target);
+    }
+    event.preventDefault();
+  }
+  var resultsTableHead = $("resultstablehead");
+  if (resultsTableHead) {
+    resultsTableHead.addEventListener("click", _handleHeaderClick);
+  }
+
+  if (typeof(ClientLogger) == "function") {
+    let cl = new ClientLogger("issues");
+    if (cl.started("issue-search")) {
+      cl.logPause("issue-search", "computer-time");
+      cl.logResume("issue-search", "user-time");
+
+      // Now we want to listen for clicks on any issue search result.
+      let logResultClick = function() {
+        cl.logPause("issue-search", "user-time");
+        cl.logResume("issue-search", "computer-time");
+      }
+
+      let links = document.querySelectorAll("#resultstable tbody .id a");
+      for (let i = 0; i < links.length; i++) {
+        links[[]i].addEventListener("click", logResultClick);
+      }
+    }
+  }
+});
+</script>
+
+<script type="text/javascript" defer src="/static/third_party/js/keys.js?version=[app_version]" nonce="[nonce]"></script>
+<script type="text/javascript" defer src="/static/third_party/js/skipper.js?version=[app_version]" nonce="[nonce]"></script>
+<script type="text/javascript" nonce="[nonce]">
+runOnLoad(function() {
+  _setupKibblesOnListPage(
+    [is arg0 "issuelist"]'[project_home_url]/issues/list'[else]'[currentPageURLEncoded]'[end],
+    '[project_home_url]/issues/entry',
+    '[projectname]', [is arg0 "issuelist"]1[else]5[end], 0);
+});
+</script>
diff --git a/templates/tracker/issue-list-menus.ezt b/templates/tracker/issue-list-menus.ezt
new file mode 100644
index 0000000..d4ab7a5
--- /dev/null
+++ b/templates/tracker/issue-list-menus.ezt
@@ -0,0 +1,170 @@
+[# Table header popup menus ]
+
+[for column_values]
+ [is column_values.column_name "id"]
+   <div id="pop_[column_values.col_index]" class="popup">
+    <table cellspacing="0" cellpadding="0" border="0">
+     <tr id="pop_up_[column_values.col_index]"><td>Sort Up</td></tr>
+     <tr id="pop_down_[column_values.col_index]"><td>Sort Down</td></tr>
+     <tr id="pop_hide_[column_values.col_index]"><td>Hide Column</td></tr>
+    </table>
+   </div>
+ [else]
+  [is column_values.column_name "summary"]
+   <div id="pop_[column_values.col_index]" class="popup">
+    <table cellspacing="0" cellpadding="0" border="0">
+     <tr id="pop_up_[column_values.col_index]"><td>Sort Up</td></tr>
+     <tr id="pop_down_[column_values.col_index]"><td>Sort Down</td></tr>
+     [if-any is_hotlist][else]
+     [if-any column_values.filter_values]
+      <tr id="pop_show_only_[column_values.col_index]"><td>Show only
+          <span class="indicator">&#9658;</span></td></tr>
+     [end][end]
+     <tr id="pop_hide_[column_values.col_index]"><td>Hide Column</td></tr>
+    </table>
+   </div>
+  [else]
+   <div id="pop_[column_values.col_index]" class="popup">
+    <table cellspacing="0" cellpadding="0" border="0">
+     <tr id="pop_up_[column_values.col_index]"><td>Sort Up</td></tr>
+     <tr id="pop_down_[column_values.col_index]"><td>Sort Down</td></tr>
+     [if-any is_hotlist][else]
+     [if-any column_values.filter_values]
+      <tr id="pop_show_only_[column_values.col_index]"><td>Show only
+          <span class="indicator">&#9658;</span></td></tr>
+     [end][end]
+     <tr id="pop_hide_[column_values.col_index]"><td>Hide Column</td></tr>
+     <tr id="pop_groupby_[column_values.col_index]"><td>Group Rows</td></tr>
+    </table>
+   </div>
+  [end]
+ [end]
+[end]
+
+[# Table header popup submenus for autofiltering of values ]
+
+[for column_values]
+ <div id="filter_[column_values.col_index]" class="popup subpopup">
+  <table cellspacing="0" cellpadding="0" border="0">
+   [for column_values.filter_values]
+    <tr data-filter-column="[is column_values.column_name "Summary"]label[else][column_values.column_name][end]"
+        data-filter-value="[column_values.filter_values]">
+     <td>[column_values.filter_values]</td></tr>
+   [end]
+  </table>
+ </div>
+[end]
+
+[# Popup menu showing the list of available columns allowing show/hide ]
+
+<div id="pop_dot" class="popup">
+ <table cellspacing="0" cellpadding="0" border="0">
+  <tr><th>Show columns:</th></tr>
+   [for panels.ordered_columns]
+    <tr data-toggle-column-index="[panels.ordered_columns.col_index]"><td>&nbsp;<span
+        class="col_[panels.ordered_columns.col_index]">&diams;</span>&nbsp;[panels.ordered_columns.name]</td></tr>
+   [end]
+   [for unshown_columns]
+    <tr data-add-column-name="[unshown_columns]"
+        ><td>&nbsp;&nbsp;&nbsp;&nbsp;[unshown_columns]</td></tr>
+   [end]
+   <tr id="pop_dot_edit"
+      ><td>&nbsp;&nbsp;&nbsp;&nbsp;Edit&nbsp;column&nbsp;spec...</td></tr>
+ </table>
+</div>
+
+
+<script type="text/javascript" nonce="[nonce]">
+runOnLoad(function() {
+  function registerPopHandlers(colIndex, colName) {
+    var sortUpEl = $("pop_up_" + colIndex);
+    if (sortUpEl) {
+      sortUpEl.addEventListener("click", function () {
+        _closeAllPopups(sortUpEl);
+        _sortUp(colName);
+      });
+      sortUpEl.addEventListener("mouseover", function () {
+       _closeSubmenus();
+      });
+    }
+
+    var sortDownEl = $("pop_down_" + colIndex);
+    if (sortDownEl) {
+      sortDownEl.addEventListener("click", function () {
+        _closeAllPopups(sortDownEl);
+        _sortDown(colName);
+      });
+      sortDownEl.addEventListener("mouseover", function () {
+       _closeSubmenus();
+      });
+    }
+
+    var hideEl = $("pop_hide_" + colIndex);
+    if (hideEl) {
+      hideEl.addEventListener("click", function () {
+        _closeAllPopups(hideEl);
+        _toggleColumnUpdate(colIndex);
+      });
+      hideEl.addEventListener("mouseover", function () {
+       _closeSubmenus();
+      });
+    }
+
+    var showOnlyEl = $("pop_show_only_" + colIndex);
+    if (showOnlyEl) {
+      showOnlyEl.addEventListener("mouseover", function () {
+        _showRight("filter_" + colIndex, showOnlyEl);
+      });
+    }
+
+    var groupByEl = $("pop_groupby_" + colIndex);
+    if (groupByEl) {
+      groupByEl.addEventListener("click", function () {
+        _closeAllPopups(groupByEl);
+        _addGroupBy(colIndex);
+      });
+      groupByEl.addEventListener("mouseover", function () {
+       _closeSubmenus();
+      });
+    }
+  }
+
+  [for column_values]
+    registerPopHandlers([column_values.col_index], "[column_values.column_name]");
+  [end]
+
+  function handleFilterValueClick(event) {
+    var target = event.target;
+    if (target.tagName != "TR") target = target.parentNode;
+    _closeAllPopups(target);
+    var filterColumn = target.getAttribute("data-filter-column");
+    var filterValue = target.getAttribute("data-filter-value");
+    _filterTo(filterColumn, filterValue);
+  }
+
+  [for column_values]
+    $("filter_" + [column_values.col_index]).addEventListener(
+        "click", handleFilterValueClick);
+  [end]
+
+  function handleDotDotDotClick(event) {
+    var target = event.target;
+    if (target.tagName != "TR") target = target.parentNode;
+    _closeAllPopups(target);
+    var colIndex = target.getAttribute("data-toggle-column-index");
+    if (colIndex != null)
+      _toggleColumnUpdate(colIndex);
+    var colName = target.getAttribute("data-add-column-name");
+    if (colName != null)
+      _addcol(colName);
+  }
+
+  $("pop_dot").addEventListener("click", handleDotDotDotClick);
+
+  $("pop_dot_edit").addEventListener("click", function() {
+    var target = $("pop_dot_edit");
+    _closeAllPopups(target);
+    $("columnspec").style.display = "";
+  });
+});
+</script>
diff --git a/templates/tracker/issue-original-page.ezt b/templates/tracker/issue-original-page.ezt
new file mode 100644
index 0000000..580b739
--- /dev/null
+++ b/templates/tracker/issue-original-page.ezt
@@ -0,0 +1,17 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
+<html>
+ <head>
+ <title>Issue [projectname]:[local_id] comment #[seq]</title>
+ <meta name="ROBOTS" content="NOINDEX">
+ <meta name="referrer" content="no-referrer">
+ <link type="text/css" rel="stylesheet" href="[version_base]/static/css/ph_core.css">
+ </head>
+ <body>
+  <h3>Original email for issue [projectname]:[local_id] comment #[seq]</h3>
+  [if-any is_binary]
+   <i>The message could not be displayed.</i>
+  [else]
+   <pre>[message_body]</pre>
+  [end]
+ </body>
+</html>
diff --git a/templates/tracker/issue-reindex-page.ezt b/templates/tracker/issue-reindex-page.ezt
new file mode 100644
index 0000000..0d80dee
--- /dev/null
+++ b/templates/tracker/issue-reindex-page.ezt
@@ -0,0 +1,45 @@
+[define title]Reindex Issues[end]
+[define category_css]css/ph_detail.css[end]
+[include "../framework/header.ezt" "showtabs"]
+
+[if-any read_only][include "../framework/read-only-rejection.ezt"]
+[else]
+
+<form action="reindex.do" method="POST" id="form">
+  <input type="hidden" name="token" value="[form_token]">
+  <table>
+    <tr>
+      <td>Start:</td>
+      <td><input type="input" name="start" value="[start]"></td>
+    </tr>
+    <tr>
+      <td>Num:</td>
+      <td><input type="input" name="num" value="[num]"></td>
+    </tr>
+    <tr>
+      <td colspan="2">
+        <input type="submit" id="submit_btn" name="btn" value="Re-index"></td>
+    </tr>
+    <tr>
+      <td><label for="autosubmit">Autosubmit:</label></td>
+      <td><input type="checkbox" name="auto_submit" id="autosubmit"
+                 [is auto_submit "True"]checked="checked"[end] ></td>
+    </tr>
+  </table>
+</form>
+
+<script type="text/javascript" nonce="[nonce]">
+runOnLoad(function() {
+  function autosubmit() {
+    if (document.getElementById('autosubmit').checked) {
+      document.getElementById('form').submit();
+    }
+  }
+  if (document.getElementById('autosubmit').checked) {
+    setTimeout(autosubmit, 5000);
+  }
+});
+</script>
+
+[end]
+[include "../framework/footer.ezt"]
diff --git a/templates/tracker/issue-search-tips.ezt b/templates/tracker/issue-search-tips.ezt
new file mode 100644
index 0000000..50d4d8c
--- /dev/null
+++ b/templates/tracker/issue-search-tips.ezt
@@ -0,0 +1,398 @@
+[define category_css]css/ph_detail.css[end]
+[include "../framework/header.ezt" "hidetabs"]
+
+[# Note: No UI element permission checking needed on this page. ]
+
+<div id="searchtips">
+
+<h3>Basic issue search</h3>
+
+<p>In most cases you can find the issues that you want to work with
+very easily by using the issue list headers or by entering a few
+simple keywords into the main search field.</p>
+
+<p>Whenever you visit the "<a href="list">issue list</a>" in your
+project, you are presented with a table of all open issues, or the default
+query set up by the project owners.  If you
+see too many results, you can quickly filter your results by clicking
+on the table headers and choosing a specific value from the "Show
+only:" submenu.</p>
+
+[# TODO screenshot ]
+
+<p>The main search field consists of two parts:</p>
+
+<ul>
+ <li>A drop-down selection of search scopes, e.g, "All issues" or just "Open issues".</li>
+ <li>A search text field where you can enter search terms.</li>
+</ul>
+
+[# TODO screenshot ]
+
+<p>In the text field, you may enter simple search terms, or add any of
+the search operators described below.</p>
+
+<p>You can also use the search text field to jump directly to any
+issue by entering its issue number.  If you wish to search for issues
+that contain a number, rather than jumping to that issue, enclose the
+number in quotation marks.</p>
+
+<p>Behind the scenes, the search scope is simply an additional set of
+search terms that is automatically combined with the user's search
+terms to make a complete query.  To see what search terms will be
+used for each scope, hover your mouse over the scope item.</p>
+
+
+<h3>Advanced issue search</h3>
+
+<p>The <a href="advsearch">Advanced Search</a> page helps you
+compose a complex query.  The advanced search form breaks the search
+down into several popular criteria and allows you to specify each one
+easily.  The search criteria boil down to the same thing as the search
+operators described below, but you don't need to remember the operator
+names.</p>
+
+
+
+<h3>Full-text search</h3>
+
+<p>As with Google web search, you can search for issues by simply
+entering a few words.  However, you may get a few more results than
+you expected.  When you need to search more precisely, you can use
+search operators for more power.</p>
+
+<form action="list" method="GET">
+ <p><input type="text" size="45" name="q" value="&quot;out of memory&quot;">
+ <input type="submit" name="btn" value="Search"></p>
+</form>
+
+<p>Full-text search terms can include quoted phrases, and words or
+phrases can be negated by using a leading minus sign.  Please note
+that negated full-text terms are likely to give large result sets,
+so it is best to use structured search operators when possible.</p>
+
+
+<h3>Search operators</h3>
+
+<p>Normal search terms will match words found in any field of an
+issue.  You can narrow the search to a specific field by using the
+name of the field.  The built-in field operators are <tt>summary</tt>,
+<tt>description</tt>, <tt>comment</tt>, <tt>status</tt>, <tt>reporter</tt>,
+<tt>owner</tt>, <tt>cc</tt>, <tt>component</tt>, <tt>commentby</tt>,
+<tt>hotlist</tt>, <tt>ID</tt>, <tt>project</tt>,
+and <tt>label</tt>.</p>
+
+<p>Field names can be compared to a list of values using:</p>
+<ul>
+  <li>a colon (:) for word matching,</li>
+  <li>an equals sign (=) for full string matching,</li>
+  <li>a not equals sign (!=) or leading minus sign to negate, or</li>
+  <li>inequality operators (&lt;, &gt;, &lt;=, &gt;=) for numeric comparison.</li>
+</ul>
+
+<p>You can limit your search to just open issues by using
+is:open, or to just closed issues by using a minus sign to negate it:
+<tt>-is:open</tt>.</p>
+[# TODO(jrobbins): dateopened:]
+
+<p>For example, here's how to search for issues with the word
+"calculation" in the summary field.</p>
+
+<form action="list" method="GET">
+ <p><input type="text" size="45" name="q" value="summary:calculation">
+ <input type="submit" name="btn" value="Search"></p>
+</form>
+
+<p>When searching for issues owned by a specific user, you can use their
+email address, or part of it.  When referring to yourself, you can also
+ use the special term <tt>me</tt>. For example, this restricts the search to
+issues that are owned by you.</p>
+
+<form action="list" method="GET">
+ <p><input type="text" size="45" name="q" value="owner:user@chromium.org">
+ <input type="submit" name="btn" value="Search">
+</form>
+
+<form action="list" method="GET">
+ <p><input type="text" size="45" name="q" value="owner:me">
+ <input type="submit" name="btn" [if-any logged_in_user][else]disabled=disabled[end] value="Search">
+ [if-any logged_in_user][else]
+   <span style="white-space:nowrap"><a href="[login_url]"
+   >Sign in</a> to try this example</span>
+ [end]</p>
+</form>
+
+<p>Rather than have a large number of predefined fields, our issue
+tracker stores many issue details as labels.</p>
+
+<p>For example, if you labeled security-related issues with the label
+<tt>Security</tt>, here's how to search for them.</p>
+
+<form action="list" method="GET">
+ <p><input type="text" size="45" name="q" value="label:security">
+ <input type="submit" name="btn" value="Search"></p>
+</form>
+
+
+<p>In addition to simple one-word labels, you can use two part labels
+that specify an attribute and a value, like <tt>Priority-High</tt>,
+<tt>Priority-Medium</tt>, and <tt>Priority-Low</tt>.  You can search for
+these with the <tt>label</tt> operator, or you can use the first part of the
+label name like an operator.</p>
+
+<p>For example, if you labeled high priority issues with
+<tt>Priority-High</tt>, here's one way to search for them.</p>
+
+<form action="list" method="GET">
+ <p><input type="text" size="45" name="q" value="label:Priority-High">
+ <input type="submit" name="btn" value="Search"></p>
+</form>
+
+<p>And, here is a more compact way to do the same search.</p>
+
+<form action="list" method="GET">
+ <p><input type="text" size="45" name="q" value="Priority:High">
+ <input type="submit" name="btn" value="Search"></p>
+</form>
+
+<p>For the <tt>components</tt> operator, the default search will find
+issues in that component and all of its subcomponents.</p>
+
+<form action="list" method="GET">
+ <p><input type="text" size="45" name="q" value="component:UI">
+ <input type="submit" name="btn" value="Search"></p>
+</form>
+
+<p>And of course, you can combine any of these field operators.</p>
+
+<form action="list" method="GET">
+ <p><input type="text" size="45" name="q"
+     value="status!=New owner:me component:UI">
+ <input type="submit" name="btn" value="Search"></p>
+</form>
+
+<p>You can search for issues in the current project that are also on a user's
+hotlist.</p>
+
+<form action="list" method="GET">
+ <p><input type="text" size="45" name="q"
+     value="hostlist=username@domain:hotlistname">
+ <input type="submit" name="btn" value="Search"></p>
+</form>
+
+
+<h3>Empty (or non-empty) field search</h3>
+
+<p>For each built-in field operator, you can use the <tt>has</tt>
+operator to search for issues with empty or non-empty fields.  The
+<tt>has</tt> operator can be used with status, owner, cc, component,
+attachments, blocking, blockedon, mergedinto, any key-value label prefix, or
+any custom field name.</p>
+
+<p>For example, here's how to search for issues that have one or more
+components.</p>
+
+<form action="list" method="GET">
+ <p><input type="text" size="45" name="q" value="has:component">
+ <input type="submit" name="btn" value="Search">
+</form>
+
+<p>Or, you can use the <tt>-has</tt> operator for negation, to search for
+issues with empty fields.</p>
+
+<p>For example, here's how to search for issues that are not associated with
+any component.</p>
+
+<form action="list" method="GET">
+ <p><input type="text" size="45" name="q" value="-has:component">
+ <input type="submit" name="btn" value="Search">
+</form>
+
+
+<h3>Multiple values in search terms</h3>
+
+<p>You can search for two values for one field, or two labels
+with the same prefix by using.</p>
+
+<form action="list" method="GET">
+ <p><input type="text" size="45" name="q" value="Priority:High,Medium">
+ <input type="submit" name="btn" value="Search"></p>
+</form>
+
+
+<p>You can combine two separate queries into one using the <tt>OR</tt> operator.</p>
+
+<form action="list" method="GET">
+ <p><input type="text" size="45" name="q" value="Priority:High OR -has:owner">
+ <input type="submit" name="btn" value="Search"></p>
+</form>
+
+
+<p>You can create more complex <tt>OR</tt> queries using parentheses nesting to
+distribute search terms across <tt>OR</tt> clauses. A search query may contain as
+many sets of parentheses and levels of parentheses nesting as needed.</p>
+
+<form action="list" method="GET">
+ <p><input type="text" size="45" name="q" value="Pri:0,1 (status:Untriaged OR -has:owner)">
+ <input type="submit" name="btn" value="Search"></p>
+</form>
+
+
+<h3>Exact value search</h3>
+
+<p>You can search for issues that exactly match the given term by using
+the search operator <tt>=</tt>.</p>
+
+<p>For example, searching for <tt>Milestone=2009</tt> only matches issues with the
+label <tt>Milestone-2009</tt>, while searching for <tt>Milestone:2009</tt> matches
+issues with the labels <tt>Milestone-2009</tt>, <tt>Milestone-2009-Q1</tt>, <tt>Milestone-2009-Q3</tt>,
+etc.</p>
+
+<form action="list" method="GET">
+ <p><input type="text" size="45" name="q" value="Milestone=2009">
+ <input type="submit" name="btn" value="Search"></p>
+</form>
+
+<p>Similarly, using exact matching on components will get you only those issues
+that are in that component, not including any of its descendants.</p>
+
+<form action="list" method="GET">
+ <p><input type="text" size="45" name="q" value="component=UI">
+ <input type="submit" name="btn" value="Search"></p>
+</form>
+
+<h3>Star search</h3>
+
+<p>Any logged in user can mark any issue with a star.  The star
+indicates interest in the issue.</p>
+
+<p>For example, to quickly see all the issues in this project that you
+have starred, you could use the following:</p>
+
+<form action="list" method="GET">
+ <p><input type="text" size="45" name="q" value="is:starred">
+ <input type="submit" name="btn" [if-any logged_in_user][else]disabled="disabled"[end] value="Search">
+ [if-any logged_in_user][else]
+   <span style="white-space:nowrap"><a href="[login_url]"
+   >Sign in</a> to try this example</span>
+ [end]</p>
+</form>
+
+<p>And, to see the issues that more than ten users have starred, use the following:</p>
+
+<form action="list" method="GET">
+ <p><input type="text" size="45" name="q" value="stars>10">
+ <input type="submit" name="btn" value="Search"></p>
+</form>
+
+
+<h3>Jump to issue and numeric search</h3>
+
+<p>You can jump directly to a specific issue by entering its ID in the
+search field.</p>
+
+<p>For example, to jump to issue 1, just search for 1.  If there is no
+existing issue with that ID, the system will search for issues that
+contain that number anywhere in the issue.</p>
+
+<form action="list" method="GET">
+ <p><input type="text" size="45" name="q" value="1">
+ <input type="submit" name="btn" value="Search"></p>
+</form>
+
+<p>If you just want to search for issues that contain the number 1, without
+jumping to issue 1, enclose the number in quotation marks.</p>
+
+<form action="list" method="GET">
+ <p><input type="text" size="45" name="q" value="&quot;1&quot;">
+ <input type="submit" name="btn" value="Search"></p>
+</form>
+
+<p>Searching for a list of specific issue IDs is one way to
+communicate a set of issues to someone that you are working with.  Be
+sure to set the search scope to "All issues" if the issues might be
+closed.</p>
+
+<form action="list" method="GET">
+ <p><input type="text" size="45" name="q" value="ID=1,2,3,4">
+ <input type="hidden" name="can" value="1">
+ <input type="submit" name="btn" value="Search"></p>
+</form>
+
+
+<h3>Attachment search</h3>
+
+<p>Users can attach files to any issues, either when issues are created or as
+part of issue comments.</p>
+
+<p>To quickly see all the issues that have attachments, use the following:</p>
+
+<form action="list" method="GET">
+ <p><input type="text" size="45" name="q" value="has:attachments">
+ <input type="submit" name="btn" value="Search"></p>
+</form>
+
+<p>Or, you can search for a specific filename of the attachment.</p>
+
+<form action="list" method="GET">
+ <p><input type="text" size="45" name="q" value="attachment:screenshot">
+ <input type="submit" name="btn" value="Search"></p>
+</form>
+
+<p>You can also search for the file extension of the attachment.</p>
+
+<form action="list" method="GET">
+ <p><input type="text" size="45" name="q" value="attachment:png">
+ <input type="submit" name="btn" value="Search"></p>
+</form>
+
+
+<p>You can also search for issues with a certain number of  attachments.</p>
+
+<form action="list" method="GET">
+ <p><input type="text" size="45" name="q" value="attachments>10">
+ <input type="submit" name="btn" value="Search"></p>
+</form>
+
+
+<h3>Date range search</h3>
+
+<p>You can perform searches based on date ranges.</p>
+
+<p>This search syntax is divided into two parts, the action and the date,
+[[]action]:[[]date]</p>
+
+<p>Built-in date operators include <tt>opened</tt>,
+<tt>modified</tt>, and <tt>closed</tt>. Each can be paired with an
+inequality operator <tt>&lt</tt> or <tt>&gt</tt>. The date must to be
+specified as YYYY-MM-DD, YYYY/MM/DD or today-N.</p>
+
+<p>For example, if you want to search for issues opened after 2009/4/1, you
+could do the following:</p>
+
+<form action="list" method="GET">
+ <p><input type="text" size="45" name="q" value="opened>2009/4/1">
+ <input type="submit" name="btn" value="Search"></p>
+</form>
+
+<p>Or, if you want to search for issues modified 20 days before today's date,
+you could do the following:</p>
+
+<form action="list" method="GET">
+ <p><input type="text" size="45" name="q" value="modified<today-20">
+ <input type="submit" name="btn" value="Search"></p>
+</form>
+
+<p>You can search for issues that had specific fields modified
+recently by using ownermodified:, statusmodified:, componentmodified:.
+For example:</p>
+
+<form action="list" method="GET">
+ <p><input type="text" size="45" name="q" value="ownermodified>today-20">
+ <input type="submit" name="btn" value="Search"></p>
+</form>
+
+
+</div>
+
+[include "../framework/footer.ezt"]
diff --git a/templates/tracker/label-fields.ezt b/templates/tracker/label-fields.ezt
new file mode 100644
index 0000000..7909c3e
--- /dev/null
+++ b/templates/tracker/label-fields.ezt
@@ -0,0 +1,128 @@
+[# Make a 3x8 grid of label entry form fields with autocomplete on each one.
+
+   Args:
+     arg0: if "just-two" is passed, only show the first two rows
+         and give the user links to click to expose more rows.
+     arg1: the ID prefix for the row divs.
+]
+
+<div id="[arg1]LF_row1" class="nowrap">
+ <input aria-label="label 1" type="text" class="labelinput" id="[arg1]label0" size="20" autocomplete="off"
+        name="label" value="[label0]">
+ <input aria-label="label 2" type="text" class="labelinput" id="[arg1]label1" size="20" autocomplete="off"
+        name="label" value="[label1]">
+ <input aria-label="label 3" type="text" class="labelinput" id="[arg1]label2" size="20" autocomplete="off"
+        name="label" value="[label2]">
+</div>
+
+<div id="[arg1]LF_row2" class="nowrap">
+ <input aria-label="label 4" type="text" class="labelinput" id="[arg1]label3" size="20" autocomplete="off"
+        name="label" value="[label3]">
+ <input aria-label="label 5" type="text" class="labelinput" id="[arg1]label4" size="20" autocomplete="off"
+        name="label" value="[label4]">
+ <input aria-label="label 6" type="text" class="labelinput" id="[arg1]label5" size="20" autocomplete="off"
+        [is arg0 "just-two"]data-show-id="LF_row3" data-hide-id="addrow2"[end]
+        name="label" value="[label5]">
+ [is arg0 "just-two"]<span id="addrow2" class="fakelink" data-instead="LF_row3">Add a row</span>[end]
+</div>
+
+<div id="[arg1]LF_row3" [is arg0 "just-two"]style="display:none"[end] class="nowrap">
+ <input aria-label="label 7" type="text" class="labelinput" id="[arg1]label6" size="20" autocomplete="off"
+        name="label" value="[label6]">
+ <input aria-label="label 8" type="text" class="labelinput" id="[arg1]label7" size="20" autocomplete="off"
+        name="label" value="[label7]">
+ <input aria-label="label 9" type="text" class="labelinput" id="[arg1]label8" size="20" autocomplete="off"
+        [is arg0 "just-two"]data-show-id="LF_row4" data-hide-id="addrow3"[end]
+        name="label" value="[label8]">
+ [is arg0 "just-two"]<span id="addrow3" class="fakelink" data-instead="LF_row4">Add a row</span>[end]
+</div>
+
+<div id="[arg1]LF_row4" [is arg0 "just-two"]style="display:none"[end] class="nowrap">
+ <input aria-label="label 10" type="text" class="labelinput" id="[arg1]label9" size="20" autocomplete="off"
+        name="label" value="[label9]">
+ <input aria-label="label 11" type="text" class="labelinput" id="[arg1]label10" size="20" autocomplete="off"
+        name="label" value="[label10]">
+ <input aria-label="label 12" type="text" class="labelinput" id="[arg1]label11" size="20" autocomplete="off"
+        [is arg0 "just-two"]data-show-id="LF_row5" data-hide-id="addrow4"[end]
+        name="label" value="[label11]">
+ [is arg0 "just-two"]<span id="addrow4" class="fakelink" data-instead="LF_row5">Add a row</span>[end]
+</div>
+
+<div id="[arg1]LF_row5" [is arg0 "just-two"]style="display:none"[end] class="nowrap">
+ <input aria-label="label 13" type="text" class="labelinput" id="[arg1]label12" size="20" autocomplete="off"
+        name="label" value="[label12]">
+ <input aria-label="label 14" type="text" class="labelinput" id="[arg1]label13" size="20" autocomplete="off"
+        name="label" value="[label13]">
+ <input aria-label="label 15" type="text" class="labelinput" id="[arg1]label14" size="20" autocomplete="off"
+        [is arg0 "just-two"]data-show-id="LF_row6" data-hide-id="addrow5"[end]
+        name="label" value="[label14]">
+ [is arg0 "just-two"]<span id="addrow5" class="fakelink" data-instead="LF_row6">Add a row</span>[end]
+</div>
+
+<div id="[arg1]LF_row6" [is arg0 "just-two"]style="display:none"[end] class="nowrap">
+ <input aria-label="label 16" type="text" class="labelinput" id="[arg1]label15" size="20" autocomplete="off"
+        name="label" value="[label15]">
+ <input aria-label="label 17" type="text" class="labelinput" id="[arg1]label16" size="20" autocomplete="off"
+        name="label" value="[label16]">
+ <input aria-label="label 18" type="text" class="labelinput" id="[arg1]label17" size="20" autocomplete="off"
+        [is arg0 "just-two"]data-show-id="LF_row7" data-hide-id="addrow6"[end]
+        name="label" value="[label17]">
+ [is arg0 "just-two"]<span id="addrow6" class="fakelink" data-instead="LF_row7">Add a row</span>[end]
+</div>
+
+<div id="[arg1]LF_row7" [is arg0 "just-two"]style="display:none"[end] class="nowrap">
+ <input aria-label="label 19" type="text" class="labelinput" id="[arg1]label18" size="20" autocomplete="off"
+        name="label" value="[label18]">
+ <input aria-label="label 20" type="text" class="labelinput" id="[arg1]label19" size="20" autocomplete="off"
+        name="label" value="[label19]">
+ <input aria-label="label 21" type="text" class="labelinput" id="[arg1]label20" size="20" autocomplete="off"
+        [is arg0 "just-two"]data-show-id="LF_row8" data-hide-id="addrow7"[end]
+        name="label" value="[label20]">
+ [is arg0 "just-two"]<span id="addrow7" class="fakelink" data-instead="LF_row8">Add a row</span>[end]
+</div>
+
+<div id="[arg1]LF_row8" [is arg0 "just-two"]style="display:none"[end] class="nowrap">
+ <input aria-label="label 22" type="text" class="labelinput" id="[arg1]label21" size="20" autocomplete="off"
+        name="label" value="[label21]">
+ <input aria-label="label 23" type="text" class="labelinput" id="[arg1]label22" size="20" autocomplete="off"
+        name="label" value="[label22]">
+ <input aria-label="label 24" type="text" class="labelinput" id="[arg1]label23" size="20" autocomplete="off"
+        name="label" value="[label23]">
+</div>
+
+<script type="text/javascript" nonce="[nonce]">
+runOnLoad(function() {
+  var labelInputs = document.getElementsByClassName("labelinput");
+  for (var i = 0; i < labelInputs.length; ++i) {
+    var labelInput = labelInputs[[]i];
+    if (labelInput.getAttribute("id").startsWith("hidden")) continue;
+    labelInput.addEventListener("keyup", function (event) {
+        if (event.target.getAttribute("data-show-id") &&
+            event.target.getAttribute("data-hide-id") &&
+            event.target.value) {
+          _showID(event.target.getAttribute("data-show-id"));
+          _hideID(event.target.getAttribute("data-hide-id"));
+        }
+        return _vallab(event.target);
+    });
+    labelInput.addEventListener("blur", function (event) {
+        _acrob(null);
+        return _vallab(event.target);
+    });
+    labelInput.addEventListener("focus", function (event) {
+        return _acof(event);
+    });
+  }
+
+  var addRowLinks = document.getElementsByClassName("fakelink");
+  for (var i = 0; i < addRowLinks.length; ++i) {
+    var rowLink = addRowLinks[[]i];
+    rowLink.addEventListener("click", function (event) {
+        _acrob(null);
+        var insteadID = event.target.getAttribute("data-instead");
+        if (insteadID)
+          _showInstead(insteadID, this);
+    });
+  }
+});
+</script>
diff --git a/templates/tracker/launch-gates-widget.ezt b/templates/tracker/launch-gates-widget.ezt
new file mode 100644
index 0000000..225a3d2
--- /dev/null
+++ b/templates/tracker/launch-gates-widget.ezt
@@ -0,0 +1,50 @@
+<table id="launch-gates-table" class="hidden">
+  <tr>
+    <th>Approval</th>
+    <th style="color:grey">gate-less</th>
+    <th><input name="phase_0" placeholder="Gate Name" size="7" [if-any allow_edit][else]disabled[end]></th>
+    <th><input name="phase_1" size="7" [if-any allow_edit][else]disabled[end]></th>
+    <th><input name="phase_2" size="7" [if-any allow_edit][else]disabled[end]></th>
+    <th><input name="phase_3" size="7" [if-any allow_edit][else]disabled[end]></th>
+    <th><input name="phase_4" size="7" [if-any allow_edit][else]disabled[end]></th>
+    <th><input name="phase_5" size="7" [if-any allow_edit][else]disabled[end]></th>
+    <th style="color:grey">omit</th>
+  </tr>
+  [for approvals]
+    <tr>
+      <td nowrap><b>[approvals.field_name]</b>
+        <br>
+        <span><input id="[approvals.field_id]_required" name="approval_[approvals.field_id]_required" type="checkbox" [if-any allow_edit][else]disabled[end]>
+        <label for="[approvals.field_id]_required">Require review</label></span>
+      </td>
+      <td><input id="[approvals.field_id]" name="approval_[approvals.field_id]" value="no_phase" type="radio" [if-any allow_edit][else]disabled[end]></td>
+      <td><input id="[approvals.field_id]_phase_0" name="approval_[approvals.field_id]" value="phase_0" type="radio" [if-any allow_edit][else]disabled[end]></td>
+      <td><input id="[approvals.field_id]_phase_1" name="approval_[approvals.field_id]" value="phase_1" type="radio" [if-any allow_edit][else]disabled[end]></td>
+      <td><input id="[approvals.field_id]_phase_2" name="approval_[approvals.field_id]" value="phase_2" type="radio" [if-any allow_edit][else]disabled[end]></td>
+      <td><input id="[approvals.field_id]_phase_3" name="approval_[approvals.field_id]" value="phase_3" type="radio" [if-any allow_edit][else]disabled[end]></td>
+      <td><input id="[approvals.field_id]_phase_4" name="approval_[approvals.field_id]" value="phase_4" type="radio" [if-any allow_edit][else]disabled[end]></td>
+      <td><input id="[approvals.field_id]_phase_5" name="approval_[approvals.field_id]" value="phase_5" type="radio" [if-any allow_edit][else]disabled[end]></td>
+      <td><input name="approval_[approvals.field_id]" value="omit" type="radio" checked="checked" [if-any allow_edit][else]disabled[end]></td>
+    </tr>
+  [end]
+</table>
+
+
+<script type="text/javascript" nonce="[nonce]">
+runOnLoad(function() {
+  let phaseNum = 0;
+  [for initial_phases]
+    document.getElementsByName(`phase_${phaseNum++}`)[0].value = '[format "js"][initial_phases.name][end]';
+  [end]
+
+  [for prechecked_approvals]
+    document.getElementById("[prechecked_approvals]").checked = "checked"
+  [end]
+
+  [for required_approval_ids]
+    document.getElementById("[required_approval_ids]_required").checked = "checked"
+  [end]
+
+});
+
+</script>
\ No newline at end of file
diff --git a/templates/tracker/render-plain-text.ezt b/templates/tracker/render-plain-text.ezt
new file mode 100644
index 0000000..e79bb2a
--- /dev/null
+++ b/templates/tracker/render-plain-text.ezt
@@ -0,0 +1,8 @@
+[# Safely display some text that includes some markup, completely removing the markup.
+
+  arg0 is a list of element EZT objects that have a tad and content and maybe some
+  other attributes.
+
+  We do not use extra whitespace in this template because it generates text into a
+  context where whitespace is significant.
+][arg0.content]
\ No newline at end of file
diff --git a/templates/tracker/render-rich-text.ezt b/templates/tracker/render-rich-text.ezt
new file mode 100644
index 0000000..89ab07d
--- /dev/null
+++ b/templates/tracker/render-rich-text.ezt
@@ -0,0 +1,10 @@
+[# Safely display some text that includes some markup.  Only the tags
+   that we explicitly allowlist are allowed, everything else gets
+   escaped.
+
+   description.text_runs is a list of element EZT objects that have a
+   tag and content and maybe some other attributes.
+
+   We do not use extra whitespace in this template because it
+   generates text into a context where whitespace is significant.
+][is arg0.tag ""][arg0.content][end][is arg0.tag "a"]<a href="[arg0.href]" title="[arg0.title]" class="[arg0.css_class]" rel="nofollow">[arg0.content]</a>[end][is arg0.tag "b"]<b>[arg0.content]</b>[end]
\ No newline at end of file
diff --git a/templates/tracker/spam-moderation-queue.ezt b/templates/tracker/spam-moderation-queue.ezt
new file mode 100644
index 0000000..e43e477
--- /dev/null
+++ b/templates/tracker/spam-moderation-queue.ezt
@@ -0,0 +1,121 @@
+[define title]Spam Moderation Queue[end]
+[define category_css]css/ph_list.css[end]
+[define page_css]css/ph_detail.css[end][# needed for infopeek]
+
+[if-any projectname]
+  [include "../framework/header.ezt" "showtabs"]
+[else]
+  [include "../framework/header.ezt" "hidetabs"]
+[end]
+[include "../framework/js-placeholders.ezt" "showtabs"]
+
+<h2>Spam Moderation Queue: Automatic Classifier Close Calls</h2>
+[include "../framework/artifact-list-pagination-part.ezt"]
+
+<button type="submit" vaue="mark_spam" disabled="true">Mark as Spam</button>
+<button type="submit" value="mark_ham" disabled="true">Mark as Ham</button>
+
+<span style="margin:0 .7em">Select:
+  <a id="selectall" href="#">All</a>
+  <a id="selectnone" href="#">None</a>
+</span>
+
+<table id='resultstable'>
+<tr>
+  <td>
+  </td>
+  <td>ID</td>
+  <td>Author</td>
+  <td>Summary</td>
+  <td>Snippet</td>
+  <td>Opened at</td>
+  <td>Spam?</td>
+  <td>Verdict reason</td>
+  <td>Confidence</td>
+  <td>Verdict at</td>
+  <td>Flag count</td>
+</tr>
+[for issue_queue]
+<tr>
+  <td><input type='checkbox' name='issue_local_id' value='[issue_queue.issue.local_id]'/></td>
+  <td><a href='/p/[projectname]/issues/detail?id=[issue_queue.issue.local_id]'>[issue_queue.issue.local_id]</a></td>
+  <td><a href='/u/[issue_queue.reporter.email]'>[issue_queue.reporter.email]</a></td>
+  <td><a href='/p/[projectname]/issues/detail?id=[issue_queue.issue.local_id]'>[issue_queue.summary]</a></td>
+  <td>
+  [issue_queue.comment_text]
+  </td>
+  <td>[issue_queue.issue.opened_timestamp]</td>
+  <td>[issue_queue.issue.is_spam]</td>
+
+  <td>[issue_queue.reason]</td>
+  <td>[issue_queue.classifier_confidence]</td>
+  <td>[issue_queue.verdict_time]</td>
+  <td>[issue_queue.flag_count]</td>
+</tr>
+[end]
+</table>
+
+[include "../framework/artifact-list-pagination-part.ezt"]
+<button type="submit" vaue="mark_spam" disabled="true">Mark as Spam</button>
+<button type="submit" value="mark_ham" disabled="true">Mark as Ham</button>
+
+</form>
+
+<script type="text/javascript" nonce="[nonce]">
+runOnLoad(function() {
+  if ($("selectall")) {
+    $("selectall").addEventListener("click", function() {
+        _selectAllIssues();
+        setDisabled(false);
+    });
+  }
+  if ($("selectnone")) {
+    $("selectnone").addEventListener("click", function() {
+        _selectNoneIssues();
+        setDisabled(true);
+    });
+  }
+
+  const checkboxes = Array.from(
+      document.querySelectorAll('input[type=checkbox]'));
+  checkboxes.forEach(checkbox => {
+    checkbox.addEventListener('change', updateEnabled);
+  });
+
+  const buttons = Array.from(
+      document.querySelectorAll('button[type=submit]'));
+  buttons.forEach(button => {
+    button.addEventListener('click', function(event) {
+      const markSpam = (button.value === 'mark_spam');
+      const issueRefs = [];
+      checkboxes.forEach(checkbox => {
+        if (checkbox.checked) {
+          issueRefs.push({
+              projectName: window.CS_env.projectName,
+              localId: checkbox.value,
+          });
+          const rowElement = checkbox.parentElement.parentElement;
+          rowElement.parentElement.removeChild(rowElement);
+        }
+      });
+      window.prpcClient.call('monorail.Issues', 'FlagIssues', {
+        issueRefs: issueRefs,
+        flag: markSpam,
+      });
+    });
+  });
+
+  function updateEnabled() {
+    const anySelected = checkboxes.some(checkbox => checkbox.checked);
+    setDisabled(!anySelected);
+   }
+
+  function setDisabled(disabled) {
+    buttons.forEach(button => {
+      button.disabled = disabled;
+    });
+  }
+});
+</script>
+
+[include "../framework/footer.ezt"]
diff --git a/templates/tracker/template-detail-page.ezt b/templates/tracker/template-detail-page.ezt
new file mode 100644
index 0000000..b596e4c
--- /dev/null
+++ b/templates/tracker/template-detail-page.ezt
@@ -0,0 +1,303 @@
+[define title]Issue Template [template_name][end]
+[define category_css]css/ph_detail.css[end]
+[include "../framework/header.ezt" "showtabs"]
+
+<a href="/p/[projectname]/adminTemplates">&lsaquo; Back to template list</a><br><br>
+
+[if-any read_only][include "../framework/read-only-rejection.ezt"]
+[else]
+
+<h4>Issue Template</h4>
+
+[if-any new_template_form]
+  <form action="create.do" method="POST">
+[else]
+  <form action="detail.do" method="POST">
+[end]
+<input type="hidden" name="token" value="[form_token]">
+<input type="hidden" name="template" value="[template_name]">
+
+
+<table cellspacing="0" cellpadding="3" class="rowmajor vt">
+  <tr>
+    <th>Members only:</th>
+    <td>
+      <input type="checkbox"[if-any allow_edit][else]disabled[end] name="members_only" [if-any initial_members_only]checked[end]>
+      <label for="members_only_checkbox">Only offer this template to project members</label>
+    </td>
+  </tr>
+
+  <tr>
+    <th>Name:</th>
+    <td>
+      [if-any new_template_form]
+        <input type="text" name="name" value="[template_name]">
+        <span id="fieldnamefeedback" class="fielderror" style="margin-left:1em">
+          [if-any errors.name][errors.name][end]
+        </span>
+      [else]
+        [template_name]
+        <input type="hidden" name="name" value="[template_name]">
+      [end]
+    </td>
+  </tr>
+
+  <tr>
+    <th>Summary:</th>
+    <td>
+      [if-any allow_edit]
+        <input type="text" name="summary" size="60" class=acob" value="[initial_summary]"><br>
+      [else]
+        [initial_summary]<br>
+      [end]
+      <input type="checkbox" [if-any allow_edit][else]disabled[end] name="summary_must_be_edited" [if-any initial_must_edit_summary]checked[end]>
+      <label for="summary_must_be_edited_checkbox">Users must edit issue summary before submitting</label>
+    </td>
+  </tr>
+
+  <tr>
+    <th>Description:</th>
+    <td>
+      [if-any allow_edit]
+         <textarea name="content" rows="12" cols="75">[initial_content]</textarea>
+         [# Note: wrap="hard" has no effect on content_editor because we copy to a hidden field before submission.]
+      [else]
+        [initial_content]
+      [end]
+    </td>
+  </tr>
+
+  <tr>
+    <th>Status:</th>
+    <td>
+      [if-any allow_edit]
+        <select id="status" name="status">
+          <option style="display: none" value="[initial_status]"></option>
+        </select>
+      [else]
+        [initial_status]
+      [end]
+    </td>
+  </tr>
+
+  <tr>
+    <th>Owner:</th>
+    <td>
+       [if-any allow_edit]
+         <input id="owner_editor" type="text" name="owner" size="25" class="acob" value="[initial_owner]"
+                autocomplete="off">
+         <span id="fieldnamefeedback" class="fielderror" style="margin-left:1em">
+            [if-any errors.owner][errors.owner][end]
+         </span>
+       [else]
+         [initial_owner]<br>
+       [end]
+       <span>
+        <input type="checkbox" [if-any allow_edit][else]disabled[end] name="owner_defaults_to_member" style="margin-left:2em" [if-any initial_owner_defaults_to_member]checked[end]>
+        <label for="owner_defaults_to_member_checkbox">Default to member who is entering the issue</label>
+       </span>
+    </td>
+  </tr>
+
+  <tr>
+    <th>Components:</th>
+    <td>
+      [if-any allow_edit]
+        <input id="components" type="text" name="components" size="75" class="acob"
+               autocomplete="off" value="[initial_components]">
+       <span id="fieldnamefeedback" class="fielderror" style="margin-left:1em">
+          [if-any errors.components][errors.components][end]
+       </span>
+      [else]
+        [initial_components]
+      [end]
+       <br/>
+       <span>
+        <input type="checkbox" [if-any allow_edit][else]disabled[end] name="component_required" [if-any initial_component_required]checked[end]>
+        <label for="component_required_checkbox">Require at least one component</label>
+       </span>
+    </td>
+  </tr>
+
+  [if-any allow_edit][if-any uneditable_fields]
+  <tr id="res_fd_banner"><th></th>
+    <td style="text-align:left; border-radius:25px">
+      <span style="background:var(--chops-orange-50); padding:5px; margin-top:10px;padding-left:10px; padding-right:10px; border-radius:25px">
+        <span style="padding-right:7px">
+        Info: Disabled inputs occur when you are not allowed to edit that restricted field.
+        </span>
+        <i id="res_fd_message" class="material-icons inline-icon" style="font-weight:bold; font-size:14px; vertical-align: text-bottom; cursor: pointer">
+        close</i>
+      </span>
+    </td>
+  </tr>
+  [end][end]
+
+  [for fields]
+    [# TODO(jrobbins): determine applicability dynamically and update fields in JS]
+    [# approval subfields are shown below, not here]
+    [if-any fields.field_def.is_approval_subfield][else][if-any fields.field_def.is_phase_field][else]
+      <tr>
+        <th>[fields.field_name]:</th>
+        <td colspan="2">
+          [if-any allow_edit]
+            [if-any fields.is_editable]
+              [include "field-value-widgets.ezt" False "tmpl" False ""]
+            [else]
+              <input disabled value = "
+              [for fields.values]
+                [fields.values.val]
+              [end]
+              " style="text-align:right; width:12em" class="multivalued customfield" aria-labelledby="[fields.field_id]_label">
+            [end]
+          [else]
+            [for fields.values]
+              [fields.values.val]
+            [end]
+          [end]
+        </td>
+      <tr>
+    [end][end]
+  [end]
+
+  <tr>
+    <th>Labels:</th>
+    <td>
+      [include "label-fields.ezt" "all" ""]
+     </td>
+   </tr>
+
+   <tr>
+     <th>Template admins:</th>
+     <td>
+       [if-any allow_edit]
+         <input id="admin_names_editor" type="text" name="admin_names" size="75" class="acob" value="[initial_admins]"
+                autocomplete="off">
+       [else]
+         [initial_admins]
+       [end]
+     </td>
+   </tr>
+
+  [if-any approvals]
+     <tr>
+       <th>Launch Gates:</th>
+       <td colspan="7">
+         <input type="checkbox" name="add_approvals" id="cb_add_approvals" [if-any allow_edit][else]disabled[end] [if-any initial_add_approvals]checked="checked"[end]>
+         <label for="cb_add_approvals">Include Gates and Approval Tasks in issue</label>
+         [include "launch-gates-widget.ezt"]
+         <span id="fieldnamefeedback" class="fielderror" style="margin-left:1em">
+              [if-any errors.phase_approvals][errors.phase_approvals][end]
+         </span>
+       </td>
+     </tr>
+  [end]
+
+  [for fields]
+    [if-any fields.field_def.is_approval_subfield]
+      <tr id="subfield-row" class="subfield-row-class">
+        <th>[fields.field_def.parent_approval_name] [fields.field_name]:</th>
+        <td colspan="2">
+          [if-any allow_edit]
+            [if-any fields.is_editable]
+              [include "field-value-widgets.ezt" False "tmpl" False ""]
+            [else]
+              <input disabled value = "
+              [for fields.values]
+                [fields.values.val]
+              [end]
+              " style="text-align:right; width:12em" class="multivalued customfield" aria-labelledby="[fields.field_id]_label">
+            [end]
+          [else]
+            [for fields.values][fields.values.val][end]
+          [end]
+        </td>
+      </tr>
+  [end][end]
+
+  [if-any allow_edit]
+    <tr>
+      <td></td>
+      <td>
+        <input id="submit_btn" type="submit" name="submit" value="Save template">
+        <input id="delete_btn" type="submit" class="secondary" name="deletetemplate" value="Delete Template">
+      </td>
+    </tr>
+  [end]
+
+</table>
+</form>
+
+[include "field-value-widgets-js.ezt"]
+
+[end][# end if not read_only]
+
+<script type="text/javascript" nonce="[nonce]">
+runOnLoad(function() {
+
+  [if-any allow_edit]
+    let addPhasesCheckbox = document.getElementById('cb_add_approvals');
+    if (addPhasesCheckbox) {
+      addPhasesCheckbox.addEventListener('change', toggleGatesView);
+    }
+
+    var acobElements = document.getElementsByClassName("acob");
+    for (var i = 0; i < acobElements.length; ++i) {
+       var el = acobElements[[]i];
+       el.addEventListener("focus", function(event) {
+           _acrob(null);
+           _acof(event);
+       });
+    }
+
+    if ($("status")) {
+      _loadStatusSelect("[projectname]", "status", "[initial_status]");
+      $("status").addEventListener("focus", function(event) {
+        _acrob(null);
+      });
+    }
+
+    if($("res_fd_message")) {
+      $("res_fd_message").onclick = function(){
+        $("res_fd_banner").classList.add("hidden");
+      };
+    };
+
+  [else]
+
+    let labelInputs = document.getElementsByClassName("labelinput");
+    Array.prototype.forEach.call(labelInputs, labelInput => {
+      labelInput.disabled = true;
+    });
+  [end]
+
+  toggleGatesView();
+  function toggleGatesView() {
+    let addPhasesCheckbox = document.getElementById('cb_add_approvals');
+    if (addPhasesCheckbox === null) return;
+    let addPhases = addPhasesCheckbox.checked;
+    let subfieldRows = document.getElementsByClassName('subfield-row-class');
+    let phasefieldRows = document.getElementsByClassName('phasefield-row-class');
+    if (addPhases) {
+      $('launch-gates-table').classList.remove('hidden');
+      for (let i=0; i<subfieldRows.length; i++){
+        subfieldRows[[]i].classList.remove('hidden');
+      }
+      for (let i=0; i<phasefieldRows.length; i++){
+        phasefieldRows[[]i].classList.remove('hidden');
+      }
+    } else{
+      $('launch-gates-table').classList.add('hidden');
+      for (let i=0; i<subfieldRows.length; i++){
+        subfieldRows[[]i].classList.add('hidden');
+      }
+      for (let i=0; i<phasefieldRows.length; i++){
+        phasefieldRows[[]i].classList.add('hidden');
+      }
+    }
+  }
+
+});
+</script>
+
+[include "../framework/footer.ezt"]
diff --git a/templates/tracker/update-issues-hotlists-dialog.ezt b/templates/tracker/update-issues-hotlists-dialog.ezt
new file mode 100644
index 0000000..a453246
--- /dev/null
+++ b/templates/tracker/update-issues-hotlists-dialog.ezt
@@ -0,0 +1,11 @@
+[# TODO(jojwang): refine what buttons are shown when there are no hotlists]
+<div id="update-issues-hotlists" style="display: none">
+  <div id="update-issues-hotlists-dialog">
+    <table id="js-hotlists-table">
+    </table>
+    <menu>
+      <button id="cancel-update-hotlists" type="reset">Cancel</button>
+      <button id="save-issues-hotlists">Save</button>
+    </menu>
+  </div>
+</div>
diff --git a/templates/tracker/web-components-page.ezt b/templates/tracker/web-components-page.ezt
new file mode 100644
index 0000000..88a42e4
--- /dev/null
+++ b/templates/tracker/web-components-page.ezt
@@ -0,0 +1,41 @@
+[if-any local_id]
+  [define title][local_id][end]
+[else]
+  [define title]Monorail[end]
+[end]
+
+[define is_ezt][end]
+[include "../framework/header-shared.ezt"]
+
+[include "../webpack-out/mr-app.ezt"]
+
+<mr-app [if-any logged_in_user]
+  userDisplayName="[logged_in_user.email]"[end]
+  loginUrl="[login_url]"
+  logoutUrl="[logout_url]"
+  versionBase="[version_base]"
+></mr-app>
+
+[include "../framework/polymer-footer.ezt"]
+
+[if-any local_id]
+  <script type="text/javascript" nonce="[nonce]">
+    window.addEventListener('load', () => {
+      window.getTSMonClient().recordIssueDetailSpaTiming();
+    });
+  </script>
+[end]
+
+<script type="text/javascript" nonce="[nonce]">
+  runOnLoad(function() {
+    if (typeof(ClientLogger) === "function") {
+      let cl = new ClientLogger("issues");
+      if (cl.started("new-issue")) {
+        cl.logEnd("new-issue", null, 120 * 1000);
+      }
+      if (cl.started("issue-search")) {
+        cl.logEnd("issue-search");
+      }
+    }
+  });
+</script>