Add code listing CSS hack article
This commit is contained in:
parent
338de75fb4
commit
f5d03ed1cf
3 changed files with 286 additions and 57 deletions
217
content/blog/css-only-code-blocks/index.rst
Normal file
217
content/blog/css-only-code-blocks/index.rst
Normal file
|
|
@ -0,0 +1,217 @@
|
|||
---
|
||||
title: "Code listings with nice line wrapping and line numbers from plain CSS"
|
||||
date: 2025-07-23T23:42:00+01:00
|
||||
summary: >
|
||||
Code listings in web pages are often a bit of a pain to use. Usually, they don't wrap on small screens. Also,
|
||||
copy-pasting code from a code listing often copies the line numbers along with the code. Finally, many
|
||||
implementations use heavyweight HTML and/or javascript, making them slow to render. For this blog, I wrote a little
|
||||
CSS hack that renders nice, wrapping code blocks with line continuation markers in plain CSS without any JS.
|
||||
---
|
||||
|
||||
Code listings in web pages are often a bit of a pain to use. Often, they don't wrap on small screens. Also, copy-pasting
|
||||
code from a code listing often copies the line numbers along with the code. Finally, many implementations use
|
||||
heavyweight HTML and/or javascript, making them slow to render (looking at you, gitlab).
|
||||
|
||||
For this blog, I wrote an implementation that renders HTML code listings entirely without JavaScript, renders line
|
||||
numbers using plain CSS such that they don't get selected with the code, and that works with the browser to wrap in a
|
||||
natural way while still supporting the little line continuation arrows that are used to show that a line was soft
|
||||
wrapped in text editors.
|
||||
|
||||
This blog is rendered as a static site using Hugo_ from a pile of RestructuredText_ documents. RestructuredText renders
|
||||
code listings using Pygments_ by default. Pygments hard-bakes the line numbers into the generated HTML, so I am using a
|
||||
`monkey-patched`_ hook that changes the line number rendering to just a bunch of empty ``<span>`` elements. The resulting
|
||||
HTML for a code block then looks like this:
|
||||
|
||||
.. code:: html
|
||||
|
||||
<pre class="code [language] literal-block">
|
||||
<span class="lineno"></span>
|
||||
<span class="line">
|
||||
<span class="[syntax highlight token]">The </span><span class="[other syntax highlight token]">code!<span>
|
||||
</span>
|
||||
<!-- ... repeat once for each source line. -->
|
||||
</pre>
|
||||
|
||||
You can find the (rather short) source of the ``rst2html`` wrapper `below <rst2html_wrapper>`_.
|
||||
|
||||
The CSS
|
||||
-------
|
||||
|
||||
This modified HTML structure of the code listing gets accompanied by some CSS to make it flow nicely. Here is a listing
|
||||
of the complete CSS controlling the listing. The only bit that isn't included here is the actual syntax styling rules
|
||||
for the pygments tokens.
|
||||
|
||||
.. code:: css
|
||||
|
||||
/*****************************************************/
|
||||
/* Code block formatting / syntax highlighting rules */
|
||||
/*****************************************************/
|
||||
|
||||
.code {
|
||||
font-family: "Fira Code";
|
||||
font-size: 13px;
|
||||
text-align: left; /* Override default content "justify" alignment */
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
overflow-x: auto;
|
||||
display: grid;
|
||||
align-items: start;
|
||||
grid-template-columns: min-content 1fr;
|
||||
}
|
||||
|
||||
.code > .line {
|
||||
padding-left: calc(2em + 5px);
|
||||
text-indent: -2em;
|
||||
padding-top: 2px;
|
||||
min-width: 15em;
|
||||
}
|
||||
|
||||
/* Make individual syntax tokens wrap anywhere */
|
||||
.code > .line > span {
|
||||
overflow-wrap: anywhere;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
/* We render line numbers in CSS! */
|
||||
.code > .lineno {
|
||||
counter-increment: lineno;
|
||||
word-break: keep-all;
|
||||
margin: 0;
|
||||
padding-left: 15px;
|
||||
padding-right: 5px;
|
||||
overflow: clip;
|
||||
position: relative;
|
||||
text-align: right;
|
||||
color: var(--c-text-muted);
|
||||
border-right: 1px solid var(--c-fg-highlight);
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
/* We also handle line continuation markers in CSS. */
|
||||
.code > .lineno::after {
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
content: "\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳";
|
||||
white-space: pre;
|
||||
color: var(--c-text-muted);
|
||||
}
|
||||
|
||||
/* Insert the actual line number */
|
||||
.code > .lineno::before {
|
||||
content: counter(lineno);
|
||||
}
|
||||
|
||||
.code::before {
|
||||
counter-reset: lineno;
|
||||
}
|
||||
|
||||
footer {
|
||||
margin-top: 30px;
|
||||
align-self: center;
|
||||
margin-bottom: 10px;
|
||||
text-align: center;
|
||||
padding: 5px 25px 5px 25px;
|
||||
}
|
||||
|
||||
.code .hll {}
|
||||
/* Following are about 50 lines that define the styling of each kind of pygments syntax highlight token. These lines
|
||||
all look like the following: */
|
||||
.code .c { color: var(--c-text); font-weight: 400 } /* Comment */
|
||||
|
||||
This CSS does a few things:
|
||||
|
||||
1. It renders the ``<pre>`` code listing element using a two-column CSS ``display: grid`` layout. The left column is
|
||||
used for the line numbers, and the right column is used for the code lines.
|
||||
2. It numbers the lines using a `CSS Counter`_. CSS counters are meant for things like numbering headings and such, but
|
||||
they are a perfect fit for our purpose.
|
||||
3. It inserts the counter value as the line number into the ``<span class="lineno">`` element's ``::before``
|
||||
pseudo-element. A side effect of using the ``::before`` pseudo-element is that without doing anything extra, the
|
||||
line numbers will remain outside of the normal text selection so they will neither be highlighted when selecting
|
||||
listing content, nor will they be copied when copy/pasting the listing content.
|
||||
4. It inserts a string of ``"\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳"`` into the line number span's
|
||||
``::after`` pseudo-element. This string evaluates to a sequence of unicode arrows separated by line breaks, and
|
||||
starting with an empty line. The ``::after`` pseudo-element is positioned using ``position: absolute``, and the
|
||||
parent ``<span class="lineno">`` has ``position: relative`` set. This way, the arrow pseudo-element gets placed on
|
||||
top of the lineno span without affecting the layout at all. By setting ``overflow: clip`` on the parent ``<span
|
||||
class="lineno">``, the arrow pseudo-element gets cut off vertically wherever the parent lineno element naturally
|
||||
ends.
|
||||
|
||||
The line number span is inserted into the parent ``<pre>`` element's CSS grid using ``align-self: stretch``, which
|
||||
causes it to vertically stretch to fill the available space. Since the line number span only contains the line number,
|
||||
its minimum height is a single line. As a result, it will stretch higher only when the corresponding code line in the
|
||||
right grid column stretches vertically because of line wrapping. When that happens, part of the arrow pseudo-element
|
||||
starts showing through from behind the ``overflow: clip`` of the line number span, and one arrow gets rendered for each
|
||||
wrapped listing line.
|
||||
|
||||
When the page is too narrow, we don't want the code listing's lines to wrapp into a column of single characters. To
|
||||
prevent that, we simply set a ``min-width`` on the ``<span class="line">`` in the right column, and set ``overflow-x:
|
||||
auto`` on the listing ``<pre>``. This results in a horizontal scroll bar appearing whenever the listing gets too narrow.
|
||||
|
||||
You can try out the line wrapping by resizing this page!
|
||||
|
||||
rst2html wrapper
|
||||
----------------
|
||||
|
||||
Here is the python ``rst2html`` wrapper that monkey-patches code rendering. I made hugo invoke this while building the
|
||||
page by simply overriding the ``PATH`` environment variable.
|
||||
|
||||
.. code:: python
|
||||
|
||||
#!/usr/bin/env python3
|
||||
# Based on https://gist.github.com/mastbaum/2655700 for the basic plugin scaffolding
|
||||
|
||||
import sys
|
||||
import re
|
||||
|
||||
import docutils.core
|
||||
from docutils.transforms import Transform
|
||||
from docutils.nodes import TextElement, Inline, Text
|
||||
from docutils.parsers.rst import Directive, directives
|
||||
from docutils.writers.html4css1 import Writer, HTMLTranslator
|
||||
|
||||
|
||||
class UnfuckedHTMLTranslator(HTMLTranslator):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.in_literal_block = False
|
||||
|
||||
def visit_literal_block(self, node):
|
||||
# Insert an empty "lineno" span before each line. We insert the line numbers using pure CSS in a ::before
|
||||
# pseudo-element. This has the added advantage that the line numbers don't get included in text selection.
|
||||
# These line number spans are also used to show line continuation markers when a line is wrapped.
|
||||
self.in_literal_block = True
|
||||
self.body.append(self.starttag(node, 'pre', CLASS='literal-block'))
|
||||
self.body.append('<span class="lineno"></span><span class="line">')
|
||||
|
||||
def depart_literal_block(self, node):
|
||||
self.in_literal_block = False
|
||||
self.body.append('\n</span></pre>\n')
|
||||
|
||||
def visit_Text(self, node):
|
||||
if self.in_literal_block:
|
||||
for match in re.finditer('([^\n]*)(\n|$)', node.astext()):
|
||||
text, end = match.groups()
|
||||
|
||||
if text:
|
||||
super().visit_Text(Text(text))
|
||||
|
||||
if end == '\n':
|
||||
if isinstance(node.parent, Inline):
|
||||
self.depart_inline(node.parent)
|
||||
self.body.append(f'</span>\n<span class="lineno"></span><span class="line">')
|
||||
if isinstance(node.parent, Inline):
|
||||
self.visit_inline(node.parent)
|
||||
|
||||
else:
|
||||
super().visit_Text(node)
|
||||
|
||||
|
||||
html_writer = Writer()
|
||||
html_writer.translator_class = UnfuckedHTMLTranslator
|
||||
docutils.core.publish_cmdline(writer=html_writer)
|
||||
|
||||
.. _Hugo: https://gohugo.io/
|
||||
.. _RestructuredText: https://www.sphinx-doc.org/en/master/usage/restructuredtext/index.html
|
||||
.. _Pygments: https://pygments.org/
|
||||
.. _`monkey-patched`: https://en.wikipedia.org/wiki/Monkey_patch
|
||||
.. _`CSS Counter`: https://developer.mozilla.org/en-US/docs/Web/CSS/counter
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
#!/usr/bin/env python3
|
||||
# https://gist.github.com/mastbaum/2655700
|
||||
# Based on https://gist.github.com/mastbaum/2655700 for the basic plugin scaffolding
|
||||
|
||||
import sys
|
||||
import re
|
||||
|
|
@ -17,6 +17,9 @@ class UnfuckedHTMLTranslator(HTMLTranslator):
|
|||
self.in_literal_block = False
|
||||
|
||||
def visit_literal_block(self, node):
|
||||
# Insert an empty "lineno" span before each line. We insert the line numbers using pure CSS in a ::before
|
||||
# pseudo-element. This has the added advantage that the line numbers don't get included in text selection.
|
||||
# These line number spans are also used to show line continuation markers when a line is wrapped.
|
||||
self.in_literal_block = True
|
||||
self.body.append(self.starttag(node, 'pre', CLASS='literal-block'))
|
||||
self.body.append('<span class="lineno"></span><span class="line">')
|
||||
|
|
|
|||
|
|
@ -471,9 +471,14 @@ img:hover {
|
|||
filter: none;
|
||||
}
|
||||
|
||||
/*****************************************************/
|
||||
/* Code block formatting / syntax highlighting rules */
|
||||
/*****************************************************/
|
||||
|
||||
.code {
|
||||
font-family: "Fira Code";
|
||||
font-size: 13px;
|
||||
text-align: left; /* Override default content "justify" alignment */
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
overflow-x: auto;
|
||||
|
|
@ -495,6 +500,7 @@ img:hover {
|
|||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
/* We render line numbers in CSS! */
|
||||
.code > .lineno {
|
||||
counter-increment: lineno;
|
||||
word-break: keep-all;
|
||||
|
|
@ -509,6 +515,7 @@ img:hover {
|
|||
align-self: stretch;
|
||||
}
|
||||
|
||||
/* We also handle line continuation markers in CSS. */
|
||||
.code > .lineno::after {
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
|
|
@ -517,6 +524,7 @@ img:hover {
|
|||
color: var(--c-text-muted);
|
||||
}
|
||||
|
||||
/* Insert the actual line number */
|
||||
.code > .lineno::before {
|
||||
content: counter(lineno);
|
||||
}
|
||||
|
|
@ -533,62 +541,63 @@ footer {
|
|||
padding: 5px 25px 5px 25px;
|
||||
}
|
||||
|
||||
body .hll {}
|
||||
body .c { color: var(--c-text); font-weight: 400 } /* Comment */
|
||||
body .n { color: var(--c-text); font-weight: 400 } /* Name */
|
||||
body .o { color: var(--c-text); font-weight: 400 } /* Operator */
|
||||
body .cm { color: var(--c-text); font-weight: 400 } /* Comment.Multiline */
|
||||
body .cp { color: var(--c-text); font-weight: 400 } /* Comment.Preproc */
|
||||
body .c1 { color: var(--c-text); font-weight: 400 } /* Comment.Single */
|
||||
body .cs { color: var(--c-text); font-weight: 400 } /* Comment.Special */
|
||||
body .nd { color: var(--c-text); font-weight: 400 } /* Name.Decorator */
|
||||
body .nn { color: var(--c-text); font-weight: 400 } /* Name.Namespace */
|
||||
body .vc { color: var(--c-text); font-weight: 400 } /* Name.Variable.Class */
|
||||
body .vg { color: var(--c-text); font-weight: 400 } /* Name.Variable.Global */
|
||||
body .vi { color: var(--c-text); font-weight: 400 } /* Name.Variable.Instance */
|
||||
body .err { color: var(--c-text-highlight); font-weight: 500 } /* Error */
|
||||
body .k { color: var(--c-text-highlight); font-weight: 500 } /* Keyword */
|
||||
body .l { color: var(--c-text-highlight); font-weight: 500 } /* Literal */
|
||||
body .kc { color: var(--c-text-highlight); font-weight: 500 } /* Keyword.Constant */
|
||||
body .kd { color: var(--c-text-highlight); font-weight: 500 } /* Keyword.Declaration */
|
||||
body .kn { color: var(--c-text-highlight); font-weight: 500 } /* Keyword.Namespace */
|
||||
body .kp { color: var(--c-text-highlight); font-weight: 500 } /* Keyword.Pseudo */
|
||||
body .kr { color: var(--c-text-highlight); font-weight: 500 } /* Keyword.Reserved */
|
||||
body .kt { color: var(--c-text-highlight); font-weight: 500 } /* Keyword.Type */
|
||||
body .na { color: var(--c-text-highlight); font-weight: 500 } /* Name.Attribute */
|
||||
body .nb { color: var(--c-text-highlight); font-weight: 500 } /* Name.Builtin */
|
||||
body .nc { color: var(--c-text-highlight); font-weight: 500 } /* Name.Class */
|
||||
body .no { color: var(--c-text-highlight); font-weight: 500 } /* Name.Constant */
|
||||
body .ni { color: var(--c-text-highlight); font-weight: 500 } /* Name.Entity */
|
||||
body .ne { color: var(--c-text-highlight); font-weight: 500 } /* Name.Exception */
|
||||
body .nf { color: var(--c-text-highlight); font-weight: 500 } /* Name.Function */
|
||||
body .nl { color: var(--c-text-highlight); font-weight: 500 } /* Name.Label */
|
||||
body .nx { color: var(--c-text-highlight); font-weight: 500 } /* Name.Other */
|
||||
body .py { color: var(--c-text-highlight); font-weight: 500 } /* Name.Property */
|
||||
body .nt { color: var(--c-text-highlight); font-weight: 500 } /* Name.Tag */
|
||||
body .nv { color: var(--c-text-highlight); font-weight: 500 } /* Name.Variable */
|
||||
body .ow { color: var(--c-text-highlight); font-weight: 500 } /* Operator.Word */
|
||||
body .bp { color: var(--c-text-highlight); font-weight: 500 } /* Name.Builtin.Pseudo */
|
||||
body .ld { color: var(--c-text); font-weight: 600 } /* Literal.Date */
|
||||
body .m { color: var(--c-text); font-weight: 600 } /* Literal.Number */
|
||||
body .s { color: var(--c-text); font-weight: 600 } /* Literal.String */
|
||||
body .mb { color: var(--c-text); font-weight: 600 } /* Literal.Number.Bin */
|
||||
body .mf { color: var(--c-text); font-weight: 600 } /* Literal.Number.Float */
|
||||
body .mh { color: var(--c-text); font-weight: 600 } /* Literal.Number.Hex */
|
||||
body .mi { color: var(--c-text); font-weight: 600 } /* Literal.Number.Integer */
|
||||
body .mo { color: var(--c-text); font-weight: 600 } /* Literal.Number.Oct */
|
||||
body .sb { color: var(--c-text); font-weight: 600 } /* Literal.String.Backtick */
|
||||
body .sc { color: var(--c-text); font-weight: 600 } /* Literal.String.Char */
|
||||
body .sd { color: var(--c-text); font-weight: 600 } /* Literal.String.Doc */
|
||||
body .s2 { color: var(--c-text); font-weight: 600 } /* Literal.String.Double */
|
||||
body .se { color: var(--c-text); font-weight: 600 } /* Literal.String.Escape */
|
||||
body .sh { color: var(--c-text); font-weight: 600 } /* Literal.String.Heredoc */
|
||||
body .si { color: var(--c-text); font-weight: 600 } /* Literal.String.Interpol */
|
||||
body .sx { color: var(--c-text); font-weight: 600 } /* Literal.String.Other */
|
||||
body .sr { color: var(--c-text); font-weight: 600 } /* Literal.String.Regex */
|
||||
body .s1 { color: var(--c-text); font-weight: 600 } /* Literal.String.Single */
|
||||
body .ss { color: var(--c-text); font-weight: 600 } /* Literal.String.Symbol */
|
||||
body .il { color: var(--c-text); font-weight: 600 } /* Literal.Number.Integer.Long */
|
||||
/* Token styling rules for syntax highlighting */
|
||||
.code .hll {}
|
||||
.code .c { color: var(--c-text); font-weight: 400 } /* Comment */
|
||||
.code .n { color: var(--c-text); font-weight: 400 } /* Name */
|
||||
.code .o { color: var(--c-text); font-weight: 400 } /* Operator */
|
||||
.code .cm { color: var(--c-text); font-weight: 400 } /* Comment.Multiline */
|
||||
.code .cp { color: var(--c-text); font-weight: 400 } /* Comment.Preproc */
|
||||
.code .c1 { color: var(--c-text); font-weight: 400 } /* Comment.Single */
|
||||
.code .cs { color: var(--c-text); font-weight: 400 } /* Comment.Special */
|
||||
.code .nd { color: var(--c-text); font-weight: 400 } /* Name.Decorator */
|
||||
.code .nn { color: var(--c-text); font-weight: 400 } /* Name.Namespace */
|
||||
.code .vc { color: var(--c-text); font-weight: 400 } /* Name.Variable.Class */
|
||||
.code .vg { color: var(--c-text); font-weight: 400 } /* Name.Variable.Global */
|
||||
.code .vi { color: var(--c-text); font-weight: 400 } /* Name.Variable.Instance */
|
||||
.code .err { color: var(--c-text-highlight); font-weight: 500 } /* Error */
|
||||
.code .k { color: var(--c-text-highlight); font-weight: 500 } /* Keyword */
|
||||
.code .l { color: var(--c-text-highlight); font-weight: 500 } /* Literal */
|
||||
.code .kc { color: var(--c-text-highlight); font-weight: 500 } /* Keyword.Constant */
|
||||
.code .kd { color: var(--c-text-highlight); font-weight: 500 } /* Keyword.Declaration */
|
||||
.code .kn { color: var(--c-text-highlight); font-weight: 500 } /* Keyword.Namespace */
|
||||
.code .kp { color: var(--c-text-highlight); font-weight: 500 } /* Keyword.Pseudo */
|
||||
.code .kr { color: var(--c-text-highlight); font-weight: 500 } /* Keyword.Reserved */
|
||||
.code .kt { color: var(--c-text-highlight); font-weight: 500 } /* Keyword.Type */
|
||||
.code .na { color: var(--c-text-highlight); font-weight: 500 } /* Name.Attribute */
|
||||
.code .nb { color: var(--c-text-highlight); font-weight: 500 } /* Name.Builtin */
|
||||
.code .nc { color: var(--c-text-highlight); font-weight: 500 } /* Name.Class */
|
||||
.code .no { color: var(--c-text-highlight); font-weight: 500 } /* Name.Constant */
|
||||
.code .ni { color: var(--c-text-highlight); font-weight: 500 } /* Name.Entity */
|
||||
.code .ne { color: var(--c-text-highlight); font-weight: 500 } /* Name.Exception */
|
||||
.code .nf { color: var(--c-text-highlight); font-weight: 500 } /* Name.Function */
|
||||
.code .nl { color: var(--c-text-highlight); font-weight: 500 } /* Name.Label */
|
||||
.code .nx { color: var(--c-text-highlight); font-weight: 500 } /* Name.Other */
|
||||
.code .py { color: var(--c-text-highlight); font-weight: 500 } /* Name.Property */
|
||||
.code .nt { color: var(--c-text-highlight); font-weight: 500 } /* Name.Tag */
|
||||
.code .nv { color: var(--c-text-highlight); font-weight: 500 } /* Name.Variable */
|
||||
.code .ow { color: var(--c-text-highlight); font-weight: 500 } /* Operator.Word */
|
||||
.code .bp { color: var(--c-text-highlight); font-weight: 500 } /* Name.Builtin.Pseudo */
|
||||
.code .ld { color: var(--c-text); font-weight: 600 } /* Literal.Date */
|
||||
.code .m { color: var(--c-text); font-weight: 600 } /* Literal.Number */
|
||||
.code .s { color: var(--c-text); font-weight: 600 } /* Literal.String */
|
||||
.code .mb { color: var(--c-text); font-weight: 600 } /* Literal.Number.Bin */
|
||||
.code .mf { color: var(--c-text); font-weight: 600 } /* Literal.Number.Float */
|
||||
.code .mh { color: var(--c-text); font-weight: 600 } /* Literal.Number.Hex */
|
||||
.code .mi { color: var(--c-text); font-weight: 600 } /* Literal.Number.Integer */
|
||||
.code .mo { color: var(--c-text); font-weight: 600 } /* Literal.Number.Oct */
|
||||
.code .sb { color: var(--c-text); font-weight: 600 } /* Literal.String.Backtick */
|
||||
.code .sc { color: var(--c-text); font-weight: 600 } /* Literal.String.Char */
|
||||
.code .sd { color: var(--c-text); font-weight: 600 } /* Literal.String.Doc */
|
||||
.code .s2 { color: var(--c-text); font-weight: 600 } /* Literal.String.Double */
|
||||
.code .se { color: var(--c-text); font-weight: 600 } /* Literal.String.Escape */
|
||||
.code .sh { color: var(--c-text); font-weight: 600 } /* Literal.String.Heredoc */
|
||||
.code .si { color: var(--c-text); font-weight: 600 } /* Literal.String.Interpol */
|
||||
.code .sx { color: var(--c-text); font-weight: 600 } /* Literal.String.Other */
|
||||
.code .sr { color: var(--c-text); font-weight: 600 } /* Literal.String.Regex */
|
||||
.code .s1 { color: var(--c-text); font-weight: 600 } /* Literal.String.Single */
|
||||
.code .ss { color: var(--c-text); font-weight: 600 } /* Literal.String.Symbol */
|
||||
.code .il { color: var(--c-text); font-weight: 600 } /* Literal.Number.Integer.Long */
|
||||
|
||||
@media (max-width: 40em) {
|
||||
nav > div {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue