diff --git a/mo/db.py b/mo/db.py
index 1adbd4674ec6ae1f8ea8d61e684f88868f71de85..fa367394a5a8e7147a202aafdc9a5332ffdefc60 100644
--- a/mo/db.py
+++ b/mo/db.py
@@ -718,6 +718,7 @@ class RegionContestStat(Base):
 
     round_id = Column(Integer, ForeignKey('rounds.round_id'), primary_key=True)
     region = Column(Integer, ForeignKey('places.place_id'), primary_key=True)
+    state = Column(Enum(RoundState, name='round_state'), primary_key=True)
     count = Column(Integer, nullable=False)
 
     round = relationship('Round')
diff --git a/mo/web/org_contest.py b/mo/web/org_contest.py
index 1e130206cf08a386a69bb015a502e68ff5888859..446992a185f0e08802178522c6e545071f10eaeb 100644
--- a/mo/web/org_contest.py
+++ b/mo/web/org_contest.py
@@ -93,6 +93,14 @@ class Context:
             for p in parents[1:]:
                 elements.append((url_for('org_round', round_id=self.round_id, hier_id=p.place_id), p.name or '???'))
         if self.contest:
+            if self.round.level >= 2:
+                parents = g.gatekeeper.get_parents(self.contest.place)
+                parents = sorted(parents, key=lambda p: p.level)
+                for i in range(1, len(parents) - 1):
+                    p = parents[i]
+                    if p.level >= 3:
+                        break
+                    elements.append((url_for('org_round', round_id=self.round_id, hier_id=p.place_id), db.Place.get_code(p)))
             elements.append((url_for('org_contest', ct_id=self.ct_id), self.contest.place.name or '???'))
         if self.site:
             elements.append((url_for('org_contest', ct_id=self.ct_id, site_id=self.site_id), f"soutěžní místo {self.site.name}"))
diff --git a/mo/web/org_round.py b/mo/web/org_round.py
index 79bbe48b96f78a028fec1546d0c51f24fa941554..5a12dbe9555119d6bb32d5e01613b8458a025295 100644
--- a/mo/web/org_round.py
+++ b/mo/web/org_round.py
@@ -1,3 +1,4 @@
+from dataclasses import dataclass, field
 import decimal
 from flask import render_template, g, redirect, flash, request
 import locale
@@ -8,9 +9,9 @@ from bleach.sanitizer import ALLOWED_TAGS
 import markdown
 import os
 from sqlalchemy import func
-from sqlalchemy.orm import joinedload
+from sqlalchemy.orm import joinedload, aliased
 from sqlalchemy.sql.functions import coalesce
-from typing import Optional
+from typing import Optional, List, Dict, Tuple, Set
 import werkzeug.exceptions
 import wtforms
 from wtforms import validators, ValidationError
@@ -157,44 +158,95 @@ def create_subcontests(master_round: db.Round, master_contest: db.Contest):
         app.logger.info(f"Podsoutěž #{subcontest.contest_id} založena: {db.row2dict(subcontest)}")
 
 
-@app.route('/org/contest/r/<int:round_id>/', methods=('GET', 'POST'))
-def org_round(round_id: int):
+@dataclass
+class ContestStat:
+    region: db.Place
+    contest: Optional[db.Contest] = None
+    num_contests: int = 0
+    contest_states: Set[db.RoundState] = field(default_factory=set)
+    num_active_pants: int = 0
+    num_unconfirmed_pants: int = 0
+
+
+def region_stats(round: db.Round, region: db.Place) -> List[ContestStat]:
+    stats: Dict[int, ContestStat] = {}
     sess = db.get_session()
-    ctx = get_context(round_id=round_id)
-    round = ctx.round
-    rights = ctx.rights
 
-    participants_count = sess.query(
-        db.Participation.contest_id,
-        func.count(db.Participation.user_id).label('count')
-    ).group_by(db.Participation.contest_id).subquery()
-
-    # Účastníci jsou jen pod master contesty
-    contests_counts = (sess.query(
-                    db.Contest,
-                    coalesce(participants_count.c.count, 0)
-                ).outerjoin(participants_count, db.Contest.master_contest_id == participants_count.c.contest_id)
-                .filter(db.Contest.round == round)
-                .options(joinedload(db.Contest.place))
-                .all())
-
-    contests_counts.sort(key=lambda c: locale.strxfrm(c[0].place.name))
-
-    sol_counts_q = (
-        sess.query(db.Solution.task_id, func.count(db.Solution.task_id))
-        .filter(db.Solution.task_id.in_(
-            sess.query(db.Task.task_id).filter_by(round=round)
-        ))
+    if region.level > round.level:
+        return []
+
+    if (region.level >= round.level - 1
+       or region.level == 2 and round.level == 4):
+        # List individual contests
+        q = sess.query(db.Contest).filter_by(round=round)
+        q = db.filter_place_nth_parent(q, db.Contest.place_id, round.level - region.level, region.place_id)
+        q = q.options(joinedload(db.Contest.place))
+        for c in q.all():
+            s = ContestStat(region=c.place, contest=c, num_contests=1)
+            stats[c.place.place_id] = s
+        have_contests = True
+    else:
+        # List sub-regions
+        regs = sess.query(db.Place).filter(db.Place.parent_place == region).all()
+        for r in regs:
+            s = ContestStat(region=r)
+            stats[r.place_id] = s
+        have_contests = False
+
+    region_ids = [s.region.place_id for s in stats.values()]
+
+    if not have_contests:
+        rcs = (sess.query(db.RegionContestStat)
+                   .filter_by(round=round)
+                   .filter(db.RegionContestStat.region.in_(region_ids))
+                   .all())
+        for r in rcs:
+            stats[r.region].num_contests += r.count
+            stats[r.region].contest_states.add(r.state)
+
+    rs = (sess.query(db.RegionParticipantStat)
+              .filter_by(round_id=round.master_round_id)
+              .filter(db.RegionParticipantStat.region.in_(region_ids))
+              .all())
+    for r in rs:
+        if r.state == db.PartState.active:
+            stats[r.region].num_active_pants = r.count
+        elif r.state == db.PartState.registered:
+            stats[r.region].num_unconfirmed_pants = r.count
+
+    out = list(stats.values())
+    out.sort(key=lambda s: locale.strxfrm(s.region.name or ""))
+    return out
+
+
+def region_totals(region: db.Place, stats: List[ContestStat]) -> ContestStat:
+    return ContestStat(
+        region=region,
+        num_contests=sum(s.num_contests for s in stats),
+        num_active_pants=sum(s.num_active_pants for s in stats),
+        num_unconfirmed_pants=sum(s.num_unconfirmed_pants for s in stats),
     )
 
-    sol_counts = {}
-    for task_id, count in sol_counts_q.group_by(db.Solution.task_id).all():
-        sol_counts[task_id] = count
 
+def task_stats(round: db.Round, region: db.Place) -> List[Tuple[db.Task, int]]:
+    sess = db.get_session()
     tasks = sess.query(db.Task).filter_by(round=round).all()
     tasks.sort(key=lambda t: t.code)
-    for task in tasks:
-        task.sol_count = sol_counts[task.task_id] if task.task_id in sol_counts else 0
+
+    ts = (sess.query(db.RegionTaskStat)
+              .filter_by(round=round, region=region.place_id)
+              .all())
+    count_by_id = {s.task_id: s.count for s in ts}
+
+    return [(t, count_by_id.get(t.task_id, 0)) for t in tasks]
+
+
+@app.route('/org/contest/r/<int:round_id>/', methods=('GET', 'POST'))
+@app.route('/org/contest/r/<int:round_id>/h/<int:hier_id>', methods=('GET', 'POST'))
+def org_round(round_id: int, hier_id: Optional[int] = None):
+    ctx = get_context(round_id=round_id, hier_id=hier_id)
+    round = ctx.round
+    rights = ctx.rights
 
     form_delete_task = TaskDeleteForm()
     if rights.have_right(Right.manage_round) and delete_task(round_id, form_delete_task):
@@ -208,13 +260,19 @@ def org_round(round_id: int):
     group_rounds = round.get_group_rounds(True)
     group_rounds.sort(key=lambda r: r.round_code())
 
+    region = ctx.hier_place or db.get_root_place()
+    reg_stats = region_stats(round, region)
+    reg_total = region_totals(region, reg_stats)
+    task_info = task_stats(round, region)
+
     return render_template(
         'org_round.html',
         ctx=ctx, rights=rights,
         round=round, group_rounds=group_rounds,
         roles=[r.friendly_name() for r in rights.get_roles()],
-        contests_counts=contests_counts,
-        tasks=tasks, form_delete_task=form_delete_task,
+        reg_stats=reg_stats, reg_total=reg_total,
+        task_info=task_info,
+        form_delete_task=form_delete_task,
         form_add_contest=form_add_contest,
         statement_exists=mo.web.util.task_statement_exists(round),
     )
diff --git a/mo/web/templates/org_round.html b/mo/web/templates/org_round.html
index a39e611f450043aa40208d0710e9f6c8de9cfb42..ddb74769aa51ad4f7a30c0c211cac1615d08454b 100644
--- a/mo/web/templates/org_round.html
+++ b/mo/web/templates/org_round.html
@@ -1,6 +1,7 @@
 {% extends "base.html" %}
 {% import "bootstrap/wtf.html" as wtf %}
-{% set can_manage_round = rights.have_right(Right.manage_round) %}
+{% set in_hier = ctx.hier_id != None %}
+{% set can_manage_round = rights.have_right(Right.manage_round) and not in_hier %}
 {% set can_manage_contest = rights.have_right(Right.manage_contest) %}
 {% set can_view_contestants = rights.have_right(Right.view_contestants) %}
 {% set can_handle_submits = rights.have_right(Right.view_submits) %}
@@ -8,7 +9,13 @@
 {% set can_view_statement = rights.can_view_statement() %}
 {% set can_add_contest = g.gatekeeper.rights_generic().have_right(Right.add_contest) %}
 
-{% block title %}{{ round.name }} {{ round.round_code() }}{% endblock %}
+{% block title %}
+	{% if in_hier %}
+		{{ round.round_code() }}: {{ ctx.hier_place.name }}
+	{% else %}
+		{{ round.name }} {{ round.round_code() }}
+	{% endif %}
+{% endblock %}
 {% block breadcrumbs %}
 {{ ctx.breadcrumbs() }}
 {% endblock %}
@@ -17,7 +24,7 @@
 
 <table class=data style="float: left; margin-right: 10px;">
 	<thead>
-		<tr><th colspan=2>Parametry kola <i>(nelze editovat)</i>
+		<tr><th colspan=2>Parametry kola{% if can_manage_round %} <i>(nelze editovat)</i>{% endif %}
 	</thead>
 	<tr><td>Ročník<td>{{ round.year }}
 	<tr><td>Kategorie<td>{{ round.category }}
@@ -81,7 +88,7 @@
 	{% if can_view_contestants %}
 	<a class="btn btn-primary" href='{{ ctx.url_for('org_generic_list') }}'>Seznam účastníků</a>
 	{% endif %}
-	{% if can_view_contestants and round.state in [RoundState.grading, RoundState.closed, RoundState.delegate] %}
+	{% if can_view_contestants and round.state in [RoundState.grading, RoundState.closed, RoundState.delegate] and not in_hier %}
 	<a class="btn btn-primary" href='{{ ctx.url_for('org_score') }}'>Výsledky</a>
 	{% endif %}
 	{% if can_manage_contest %}
@@ -94,38 +101,59 @@
 	{% if round.has_messages %}
 	<a class="btn btn-default" href='{{ ctx.url_for('org_round_messages') }}'>Zprávičky</a>
 	{% endif %}
-	{% if g.user.is_admin %}
+	{% if g.user.is_admin and not in_hier %}
 	<a class="btn btn-default" href='{{ log_url('round', round.round_id) }}'>Historie</a>
 	{% endif %}
 </div>
 {% endif %}
 
 <h3>Soutěže</h3>
-{% if contests_counts %}
-<table class=data>
-	<thead>
-		<tr>
-			<th>{{ round.get_level().name|capitalize }}
-			<th>Stav
-			<th>Počet účastníků
-		</tr>
-	</thead>
-	{% for (c, count) in contests_counts %}
-	<tr>
-		<td><a href='{{ url_for('org_contest', ct_id=c.contest_id) }}'>{{ c.place.name }}</a>
-		{% with state=c.state %}
-		<td class='rstate-{{state.name}}'>{{ state.friendly_name() }}
-		{% endwith %}
-		<td>{{ count }}
-	{% endfor %}
-	<tfoot>
+{% if reg_total.num_contests %}
+	{% set show_contests = reg_stats[0].contest != None %}
+	<table class=data>
+		<thead>
+			<tr>
+			{% if show_contests %}
+				<th>{{ round.get_level().name|capitalize }}
+				<th>Stav
+			{% else %}
+				<th>{{ reg_stats[0].region.type_name()|capitalize }}
+				<th>Počet soutěží
+				<th>Stavy soutěží
+			{% endif %}
+				<th>Počet účastníků
+				<th>Počet přihlášek
+			</tr>
+		</thead>
+		{% for rs in reg_stats %}
 		<tr>
-			<th>Celkem
-			<th>
-			<th>{{ contests_counts|sum(attribute=1) }}
-		</tr>
-	</tfoot>
-</table>
+		{% if show_contests %}
+			<td><a href='{{ url_for('org_contest', ct_id=rs.contest.contest_id) }}'>{{ rs.region.name }}</a>
+			{% with state=rs.contest.state %}
+			<td class='rstate-{{state.name}}'>{{ state.friendly_name() }}
+			{% endwith %}
+		{% else %}
+			<td><a href='{{ ctx.url_for('org_round', hier_id=rs.region.place_id) }}'>{{ rs.region.name }}</a>
+			<td>{{ rs.num_contests }}
+			<td>{% for s in rs.contest_states %}<span class='rstate-{{s.name}}'>{{ s.friendly_name() }}</span> {% endfor %}
+		{% endif %}
+			<td>{{ rs.num_active_pants }}
+			<td>{{ rs.num_unconfirmed_pants }}
+		{% endfor %}
+		<tfoot>
+			<tr>
+				<th>Celkem
+			{% if show_contests %}
+				<th>
+			{% else %}
+				<th>{{ reg_total.num_contests }}
+				<th>
+			{% endif %}
+				<th>{{ reg_total.num_active_pants }}
+				<th>{{ reg_total.num_unconfirmed_pants }}
+			</tr>
+		</tfoot>
+	</table>
 {% else %}
 <p>Zatím nebyly založeny žádné soutěže.
 {% endif %}
@@ -139,7 +167,7 @@
 {% endif %}
 
 <h3>Úlohy</h3>
-{% if tasks %}
+{% if task_info %}
 <table class=data>
 	<thead>
 		<tr>
@@ -151,11 +179,11 @@
 			{% if can_handle_submits or can_upload %}<th>Dávkové operace{% endif %}
 		</tr>
 	</thead>
-	{% for task in tasks %}
+	{% for task, sol_count in task_info %}
 		<tr>
 			<td>{{ task.code }}
 			<td>{{ task.name }}
-			<td>{{ task.sol_count }}
+			<td>{{ sol_count }}
 			<td>{{ task.max_points|decimal|none_value('–') }}
 			{% if can_manage_round %}
 			<td><div class="btn-group">