fix(iOS): fix horizontal scroll, use fake sticky button on iOS (#711)

fixes #667
This commit is contained in:
Nolan Lawson 2018-12-02 11:22:18 -08:00 committed by GitHub
parent 537ad208a3
commit 945c1e7a23
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 194 additions and 94 deletions

View File

@ -143,7 +143,8 @@
"Image", "Image",
"NotificationEvent", "NotificationEvent",
"NodeList", "NodeList",
"DOMParser" "DOMParser",
"CSS"
], ],
"ignore": [ "ignore": [
"dist", "dist",

View File

@ -15,10 +15,7 @@
<ComposeLengthIndicator {length} {overLimit} /> <ComposeLengthIndicator {length} {overLimit} />
<ComposeMedia {realm} {media} /> <ComposeMedia {realm} {media} />
</div> </div>
<div class="compose-box-button-sentinel {hideAndFadeIn}" ref:sentinel></div> <ComposeStickyButton {showSticky} {hideAndFadeIn} {overLimit} on:postAction="doPostStatus()" />
<div class="compose-box-button-wrapper {realm === 'home' ? 'compose-button-sticky' : ''} {hideAndFadeIn}" >
<ComposeButton {length} {overLimit} {sticky} on:click="onClickPostButton()" />
</div>
{#if !hideBottomBorder} {#if !hideBottomBorder}
<div class="compose-box-border-bottom {hideAndFadeIn}"></div> <div class="compose-box-border-bottom {hideAndFadeIn}"></div>
{/if} {/if}
@ -59,27 +56,6 @@
width: 100%; width: 100%;
} }
.compose-box-button-wrapper {
/*
* We want pointer-events only for the sticky button, so use fit-content so that
* the element doesn't take up the full width, and then set its left margin to
* auto so that it sticks to the right. fit-content doesn't work in Edge, but
* that just means that content that is level with the button is not clickable.
*/
width: -moz-fit-content;
width: fit-content;
margin-left: auto;
display: flex;
justify-content: flex-end;
}
.compose-box-button-wrapper.compose-button-sticky {
position: -webkit-sticky;
position: sticky;
top: 52px; /* padding-top for .main-content plus 10px */
z-index: 5000;
}
.compose-content-warning-wrapper { .compose-content-warning-wrapper {
grid-area: cw; grid-area: cw;
} }
@ -90,18 +66,11 @@
max-width: calc(100vw - 20px); max-width: calc(100vw - 20px);
width: 580px; width: 580px;
} }
.compose-box.slim-size { .compose-box.slim-size {
width: 560px; width: 560px;
max-width: calc(100vw - 40px); max-width: calc(100vw - 40px);
} }
.compose-box-button-wrapper.compose-button-sticky {
top: 57px; /* padding-top for .main-content plus 5px */
}
}
@media (max-width: 991px) {
.compose-box-button-wrapper.compose-button-sticky {
top: 62px; /* padding-top for .main-content plus 10px */
}
} }
</style> </style>
<script> <script>
@ -110,7 +79,7 @@
import ComposeLengthIndicator from './ComposeLengthIndicator.html' import ComposeLengthIndicator from './ComposeLengthIndicator.html'
import ComposeAuthor from './ComposeAuthor.html' import ComposeAuthor from './ComposeAuthor.html'
import ComposeInput from './ComposeInput.html' import ComposeInput from './ComposeInput.html'
import ComposeButton from './ComposeButton.html' import ComposeStickyButton from './ComposeStickyButton.html'
import ComposeMedia from './ComposeMedia.html' import ComposeMedia from './ComposeMedia.html'
import ComposeContentWarning from './ComposeContentWarning.html' import ComposeContentWarning from './ComposeContentWarning.html'
import { measureText } from '../../_utils/measureText' import { measureText } from '../../_utils/measureText'
@ -118,42 +87,33 @@
import { store } from '../../_store/store' import { store } from '../../_store/store'
import { slide } from 'svelte-transitions' import { slide } from 'svelte-transitions'
import { postStatus, insertHandleForReply, setReplySpoiler, setReplyVisibility } from '../../_actions/compose' import { postStatus, insertHandleForReply, setReplySpoiler, setReplyVisibility } from '../../_actions/compose'
import { importShowComposeDialog } from '../dialog/asyncDialogs'
import { classname } from '../../_utils/classname' import { classname } from '../../_utils/classname'
import { observe } from 'svelte-extras'
export default { export default {
oncreate () { oncreate () {
let { realm } = this.get() let { realm, replySpoiler, replyVisibility } = this.get()
if (realm === 'home') { if (realm !== 'home' && realm !== 'dialog') {
this.setupStickyObserver()
} else if (realm !== 'dialog') {
// if this is a reply, populate the handle immediately // if this is a reply, populate the handle immediately
insertHandleForReply(realm) /* no await */ insertHandleForReply(realm)
} }
let { replySpoiler } = this.get()
if (replySpoiler) { if (replySpoiler) {
// default spoiler is same as the replied-to status // default spoiler is same as the replied-to status
setReplySpoiler(realm, replySpoiler) setReplySpoiler(realm, replySpoiler)
} }
let { replyVisibility } = this.get()
if (replyVisibility) { if (replyVisibility) {
// make sure the visibility is consistent with the replied-to status // make sure the visibility is consistent with the replied-to status
setReplyVisibility(realm, replyVisibility) setReplyVisibility(realm, replyVisibility)
} }
}, },
ondestroy () {
this.teardownStickyObserver()
},
components: { components: {
ComposeAuthor, ComposeAuthor,
ComposeToolbar, ComposeToolbar,
ComposeLengthGauge, ComposeLengthGauge,
ComposeLengthIndicator, ComposeLengthIndicator,
ComposeInput, ComposeInput,
ComposeButton, ComposeStickyButton,
ComposeMedia, ComposeMedia,
ComposeContentWarning ComposeContentWarning
}, },
@ -161,7 +121,6 @@
size: void 0, size: void 0,
isReply: false, isReply: false,
autoFocus: false, autoFocus: false,
sticky: false,
hideBottomBorder: false, hideBottomBorder: false,
hidden: false hidden: false
}), }),
@ -177,6 +136,7 @@
'compose-box-fade-in', 'compose-box-fade-in',
hidden && 'hidden' hidden && 'hidden'
)), )),
showSticky: ({ realm }) => realm === 'home',
composeData: ({ $currentComposeData, realm }) => $currentComposeData[realm] || {}, composeData: ({ $currentComposeData, realm }) => $currentComposeData[realm] || {},
text: ({ composeData }) => composeData.text || '', text: ({ composeData }) => composeData.text || '',
media: ({ composeData }) => composeData.media || [], media: ({ composeData }) => composeData.media || [],
@ -190,26 +150,12 @@
), ),
overLimit: ({ length, $maxStatusChars }) => length > $maxStatusChars, overLimit: ({ length, $maxStatusChars }) => length > $maxStatusChars,
contentWarningShown: ({ composeData }) => composeData.contentWarningShown, contentWarningShown: ({ composeData }) => composeData.contentWarningShown,
contentWarning: ({ composeData }) => composeData.contentWarning || '', contentWarning: ({ composeData }) => composeData.contentWarning || ''
timelineInitialized: ({ $timelineInitialized }) => $timelineInitialized
}, },
transitions: { transitions: {
slide slide
}, },
methods: { methods: {
observe,
async onClickPostButton () {
let { sticky } = this.get()
if (sticky) {
// when the button is sticky, we're scrolled down the home timeline,
// so we should launch a new compose dialog
let showComposeDialog = await importShowComposeDialog()
showComposeDialog()
} else {
// else we're actually posting a new toot
this.doPostStatus()
}
},
doPostStatus () { doPostStatus () {
let { let {
text, text,
@ -233,30 +179,6 @@
postStatus(realm, text, inReplyTo, mediaIds, postStatus(realm, text, inReplyTo, mediaIds,
sensitive, contentWarning, postPrivacyKey, sensitive, contentWarning, postPrivacyKey,
mediaDescriptions, inReplyToUuid) mediaDescriptions, inReplyToUuid)
},
setupStickyObserver () {
this.__stickyObserver = new IntersectionObserver(entries => {
this.set({ sticky: !entries[0].isIntersecting })
})
this.__stickyObserver.observe(this.refs.sentinel)
// also create a one-shot observer for the $timelineInitialized event,
// due to a bug in Firefox where when the scrollTop is set
// manually, the other observer doesn't necessarily fire
this.observe('timelineInitialized', timelineInitialized => {
if (timelineInitialized) {
let observer = new IntersectionObserver(entries => {
this.set({ sticky: !entries[0].isIntersecting })
observer.disconnect()
})
observer.observe(this.refs.sentinel)
}
}, { init: false })
},
teardownStickyObserver () {
if (this.__stickyObserver) {
this.__stickyObserver.disconnect()
}
} }
} }
} }

View File

@ -0,0 +1,176 @@
<div class="compose-box-button-sentinel {hideAndFadeIn}" ref:sentinel></div>
<div class="compose-box-button-wrapper {showSticky ? 'compose-box-button-sticky' : ''} {hideAndFadeIn}"
ref:wrapper >
<ComposeButton {overLimit} {sticky} on:click="onClickButton()" />
</div>
<style>
.compose-box-button-wrapper {
/*
* We want pointer-events only for the sticky button, so use fit-content so that
* the element doesn't take up the full width, and then set its left margin to
* auto so that it sticks to the right. fit-content doesn't work in Edge, but
* that just means that content that is level with the button is not clickable.
*/
width: -moz-fit-content;
width: fit-content;
margin-left: auto;
display: flex;
justify-content: flex-end;
}
.compose-box-button-sticky {
position: -webkit-sticky;
position: sticky;
}
:global(.compose-box-button-sticky, .compose-box-button-fixed) {
z-index: 5000;
top: 52px; /* padding-top for .main-content plus 10px */
}
@media (max-width: 767px) {
:global(.compose-box-button-sticky, .compose-box-button-fixed) {
top: 57px; /* padding-top for .main-content plus 5px */
}
}
@media (max-width: 991px) {
:global(.compose-box-button-sticky, .compose-box-button-fixed) {
top: 62px; /* padding-top for .main-content plus 10px */
}
}
@supports (-webkit-overflow-scrolling: touch) {
.compose-box-button-sticky {
/* disable sticky positioning on iOS due to
https://github.com/nolanlawson/pinafore/issues/667 */
position: relative;
z-index: 0;
top: 0;
}
}
</style>
<script>
import ComposeButton from './ComposeButton.html'
import { store } from '../../_store/store'
import { importShowComposeDialog } from '../dialog/asyncDialogs'
import { observe } from 'svelte-extras'
const USE_IOS_WORKAROUND = process.browser && CSS.supports('-webkit-overflow-scrolling', 'touch')
export default {
oncreate () {
this.setupStickyObserver()
this.setupIOSWorkaround()
},
ondestroy () {
this.teardownStickyObserver()
},
store: () => store,
data: () => ({
sticky: false
}),
computed: {
timelineInitialized: ({ $timelineInitialized }) => $timelineInitialized
},
methods: {
observe,
onClickButton () {
let { sticky } = this.get()
if (sticky) {
// when the button is sticky, we're scrolled down the home timeline,
// so we should launch a new compose dialog
this.showDialog()
} else {
// else we're actually posting a new toot, let our parent know
this.fire('postAction')
}
},
async showDialog () {
(await importShowComposeDialog())()
},
setupIOSWorkaround () {
// This is an elaborate fix for https://github.com/nolanlawson/pinafore/issues/667
// We detect iOS using support for -webkit-overflow-scrolling: touch
// (both here and in global.scss). Then, we set the main content element
// to be overflow-x: hidden, which normally would break the sticky button
// because its parent is now the scrolling context. So for iOS only, we
// create a fake sticky button by listening to intersecting events
// and inserting a permanently fixed-position element into the DOM.
let { showSticky } = this.get()
if (!USE_IOS_WORKAROUND || !showSticky) {
return
}
let cleanup = () => {
let existingElement = document.getElementById('the-sticky-button')
if (existingElement) {
document.body.removeChild(existingElement)
}
if (this.__fixedStickyButton) {
this.__fixedStickyButton.destroy()
this.__fixedStickyButton = null
}
}
let createFixedStickyButton = () => {
let element = document.createElement('div')
element.setAttribute('id', 'the-sticky-button')
element.classList.add('compose-box-button-wrapper')
element.classList.add('compose-box-button-fixed')
document.body.appendChild(element)
let rect = this.refs.wrapper.getBoundingClientRect()
Object.assign(element.style, {
right: `${rect.right}px`,
left: `${rect.left}px`,
position: 'fixed'
})
this.__fixedStickyButton = new ComposeButton({
target: element,
data: {
sticky: true,
overLimit: false
}
})
this.__fixedStickyButton.on('click', () => this.showDialog())
}
this.observe('sticky', sticky => {
cleanup()
if (sticky) {
createFixedStickyButton()
}
})
this.on('destroy', () => cleanup())
},
setupStickyObserver () {
let sentinel = this.refs.sentinel
this.__stickyObserver = new IntersectionObserver(entries => this.onObserve(entries))
this.__stickyObserver.observe(sentinel)
// also create a one-shot observer for the $timelineInitialized event,
// due to a bug in Firefox where when the scrollTop is set
// manually, the other observer doesn't necessarily fire
this.observe('timelineInitialized', timelineInitialized => {
if (timelineInitialized) {
let observer = new IntersectionObserver(entries => {
this.onObserve(entries)
observer.disconnect()
})
observer.observe(sentinel)
}
}, { init: false })
},
onObserve (entries) {
this.set({ sticky: !entries[0].isIntersecting })
},
teardownStickyObserver () {
if (this.__stickyObserver) {
this.__stickyObserver.disconnect()
}
}
},
components: {
ComposeButton
}
}
</script>

View File

@ -30,15 +30,16 @@ body {
color: var(--body-text-color); color: var(--body-text-color);
background: var(--body-bg); background: var(--body-bg);
-webkit-tap-highlight-color: transparent; // fix for blue background on spoiler tap on Chrome for Android -webkit-tap-highlight-color: transparent; // fix for blue background on spoiler tap on Chrome for Android
// Prevent horizontal scrolling on mobile Firefox on small screens. Unfortunately iOS Safari ignores this, overflow-x: hidden; // Prevent horizontal scrolling on mobile Firefox on small screens
// but if we were to put overflow-x:hidden anywhere else (which fixes it: https://stackoverflow.com/a/14271049),
// then it would break the sticky-positioned button.
overflow-x: hidden;
} }
.main-content { .main-content {
contain: content; // see https://www.w3.org/TR/2018/CR-css-contain-1-20181108/#valdef-contain-content contain: content; // see https://www.w3.org/TR/2018/CR-css-contain-1-20181108/#valdef-contain-content
// these paddings should be kept in sync with getMainTopMargin.js // these paddings should be kept in sync with getMainTopMargin.js
@supports (-webkit-overflow-scrolling: touch) {
// fixes iOS Safari horizontal scrolling (see https://github.com/nolanlawson/pinafore/issues/667)
overflow-x: hidden;
}
padding-top: 42px; padding-top: 42px;
@media (max-width: 991px) { @media (max-width: 991px) {
padding-top: 52px; padding-top: 52px;
@ -246,4 +247,4 @@ textarea {
.inline-emoji { .inline-emoji {
font-family: PinaforeEmoji, sans-serif; font-family: PinaforeEmoji, sans-serif;
} }

View File

@ -17,7 +17,7 @@
<!-- begin inline CSS --> <!-- begin inline CSS -->
<style> <style>
:root{--button-primary-bg: #6081e6;--button-primary-text: #fff;--button-primary-border: #132c76;--button-primary-bg-active: #456ce2;--button-primary-bg-hover: #6988e7;--button-bg: #e6e6e6;--button-text: #333;--button-border: #a7a7a7;--button-bg-active: #bfbfbf;--button-bg-hover: #f2f2f2;--input-border: #dadada;--anchor-text: #4169e1;--main-bg: #fff;--body-bg: #e8edfb;--body-text-color: #333;--main-border: #dadada;--svg-fill: #4169e1;--form-bg: #f7f7f7;--form-border: #c1c1c1;--nav-bg: #4169e1;--nav-border: #214cce;--nav-a-border: #4169e1;--nav-a-selected-border: #fff;--nav-a-selected-bg: #6d8ce8;--nav-svg-fill: #fff;--nav-text-color: #fff;--nav-a-selected-border-hover: #fff;--nav-a-selected-bg-hover: #839deb;--nav-a-bg-hover: #577ae4;--nav-a-border-hover: #4169e1;--nav-svg-fill-hover: #fff;--nav-text-color-hover: #fff;--action-button-fill-color: #90a8ee;--action-button-fill-color-hover: #a2b6f0;--action-button-fill-color-active: #577ae4;--action-button-fill-color-pressed: #2351dc;--action-button-fill-color-pressed-hover: #3862e0;--action-button-fill-color-pressed-active: #1d44b8;--action-button-deemphasized-fill-color: #666;--action-button-deemphasized-fill-color-hover: #9e9e9e;--action-button-deemphasized-fill-color-active: #737373;--action-button-deemphasized-fill-color-pressed: #545454;--action-button-deemphasized-fill-color-pressed-hover: #616161;--action-button-deemphasized-fill-color-pressed-active: #404040;--settings-list-item-bg: #fff;--settings-list-item-text: #4169e1;--settings-list-item-text-hover: #4169e1;--settings-list-item-border: #dadada;--settings-list-item-bg-active: #e6e6e6;--settings-list-item-bg-hover: #fafafa;--toast-bg: #333;--toast-border: #fafafa;--toast-text: #fff;--mask-bg: #333;--mask-svg-fill: #fff;--mask-opaque-bg: rgba(51,51,51,0.8);--loading-bg: #ededed;--account-profile-bg-backdrop-filter: rgba(255,255,255,0.7);--account-profile-bg: rgba(255,255,255,0.9);--deemphasized-text-color: #666;--focus-outline: #c5d1f6;--very-deemphasized-link-color: rgba(65,105,225,0.6);--very-deemphasized-text-color: rgba(102,102,102,0.6);--status-direct-background: #d2dcf8;--main-theme-color: #4169e1;--warning-color: #e01f19;--alt-input-bg: rgba(255,255,255,0.7);--muted-modal-bg: transparent;--muted-modal-focus: #999;--muted-modal-hover: rgba(255,255,255,0.2);--compose-autosuggest-item-hover: #ced8f7;--compose-autosuggest-item-active: #b8c7f4;--compose-autosuggest-outline: #dbe3f9;--compose-button-halo: rgba(255,255,255,0.1)}:root{--scrollbar-face-color: #90a8ee;--scrollbar-track-color: #dbe3f9;--scrollbar-border-radius: 0;--scrollbar-face-color-hover: #99afef;--scrollbar-face-color-active: #839deb;--scrollbar-width: 12px;--scrollbar-height: 12px;--scrollbar-background-color: transparent} :root{--button-primary-bg: #6081e6;--button-primary-text: #fff;--button-primary-border: #132c76;--button-primary-bg-active: #456ce2;--button-primary-bg-hover: #6988e7;--button-bg: #e6e6e6;--button-text: #333;--button-border: #a7a7a7;--button-bg-active: #bfbfbf;--button-bg-hover: #f2f2f2;--input-border: #dadada;--anchor-text: #4169e1;--main-bg: #fff;--body-bg: #e8edfb;--body-text-color: #333;--main-border: #dadada;--svg-fill: #4169e1;--form-bg: #f7f7f7;--form-border: #c1c1c1;--nav-bg: #4169e1;--nav-border: #214cce;--nav-a-border: #4169e1;--nav-a-selected-border: #fff;--nav-a-selected-bg: #6d8ce8;--nav-svg-fill: #fff;--nav-text-color: #fff;--nav-a-selected-border-hover: #fff;--nav-a-selected-bg-hover: #839deb;--nav-a-bg-hover: #577ae4;--nav-a-border-hover: #4169e1;--nav-svg-fill-hover: #fff;--nav-text-color-hover: #fff;--action-button-fill-color: #90a8ee;--action-button-fill-color-hover: #a2b6f0;--action-button-fill-color-active: #577ae4;--action-button-fill-color-pressed: #2351dc;--action-button-fill-color-pressed-hover: #3862e0;--action-button-fill-color-pressed-active: #1d44b8;--action-button-deemphasized-fill-color: #666;--action-button-deemphasized-fill-color-hover: #9e9e9e;--action-button-deemphasized-fill-color-active: #737373;--action-button-deemphasized-fill-color-pressed: #545454;--action-button-deemphasized-fill-color-pressed-hover: #616161;--action-button-deemphasized-fill-color-pressed-active: #404040;--settings-list-item-bg: #fff;--settings-list-item-text: #4169e1;--settings-list-item-text-hover: #4169e1;--settings-list-item-border: #dadada;--settings-list-item-bg-active: #e6e6e6;--settings-list-item-bg-hover: #fafafa;--toast-bg: #333;--toast-border: #fafafa;--toast-text: #fff;--mask-bg: #333;--mask-svg-fill: #fff;--mask-opaque-bg: rgba(51,51,51,0.8);--loading-bg: #ededed;--account-profile-bg-backdrop-filter: rgba(255,255,255,0.7);--account-profile-bg: rgba(255,255,255,0.9);--deemphasized-text-color: #666;--focus-outline: #c5d1f6;--very-deemphasized-link-color: rgba(65,105,225,0.6);--very-deemphasized-text-color: rgba(102,102,102,0.6);--status-direct-background: #d2dcf8;--main-theme-color: #4169e1;--warning-color: #e01f19;--alt-input-bg: rgba(255,255,255,0.7);--muted-modal-bg: transparent;--muted-modal-focus: #999;--muted-modal-hover: rgba(255,255,255,0.2);--compose-autosuggest-item-hover: #ced8f7;--compose-autosuggest-item-active: #b8c7f4;--compose-autosuggest-outline: #dbe3f9;--compose-button-halo: rgba(255,255,255,0.1)}:root{--scrollbar-face-color: #90a8ee;--scrollbar-track-color: #dbe3f9;--scrollbar-border-radius: 0;--scrollbar-face-color-hover: #99afef;--scrollbar-face-color-active: #839deb;--scrollbar-width: 12px;--scrollbar-height: 12px;--scrollbar-background-color: transparent}
@font-face{font-family:PinaforeRegular;src:local("BlinkMacSystemFont"),local("Segoe UI"),local("Roboto"),local("Oxygen-Sans"),local("Ubuntu"),local("Cantarell"),local("Fira Sans"),local("Droid Sans"),local("Helvetica Neue")}@font-face{font-family:PinaforeEmoji;src:local("Apple Color Emoji"),local("Segoe UI Emoji"),local("Segoe UI Symbol"),local("Twemoji Mozilla"),local("Noto Color Emoji"),local("EmojiOne Color"),local("Android Emoji")}body{margin:0;font-family:system-ui, -apple-system, PinaforeRegular, sans-serif;font-size:14px;line-height:1.4;color:var(--body-text-color);background:var(--body-bg);-webkit-tap-highlight-color:transparent;overflow-x:hidden}.main-content{contain:content;padding-top:42px}@media (max-width: 991px){.main-content{padding-top:52px}}@media (max-width: 767px){.main-content{padding-top:62px}}main{position:relative;width:602px;max-width:100vw;padding:0;box-sizing:border-box;margin:30px auto 15px;background:var(--main-bg);border:1px solid var(--main-border);border-radius:1px;min-height:70vh}@media (max-width: 767px){main{margin:5px auto 15px}}footer{width:602px;max-width:100vw;box-sizing:border-box;margin:15px auto;border-radius:1px;background:var(--main-bg);font-size:0.9em;padding:20px;border:1px solid var(--main-border)}h1,h2,h3,h4,h5,h6{margin:0 0 0.5em 0;font-weight:400;line-height:1.2}h1{font-size:2em}a{color:var(--anchor-text);text-decoration:none}a:visited{color:var(--anchor-text)}a:hover{text-decoration:underline}input{border:1px solid var(--input-border);padding:5px;box-sizing:border-box}input[type=search]{-webkit-appearance:none}input,textarea{background:inherit;color:inherit}textarea{font-family:system-ui, -apple-system, PinaforeRegular, sans-serif, PinaforeEmoji}button,.button{font-size:1.2em;background:var(--button-bg);border-radius:2px;padding:10px 15px;border:1px solid var(--button-border);cursor:pointer;color:var(--button-text)}button:hover,.button:hover{background:var(--button-bg-hover);text-decoration:none}button:active,.button:active{background:var(--button-bg-active)}button[disabled],.button[disabled]{opacity:0.35;pointer-events:none;cursor:not-allowed}button.primary,.button.primary{border:1px solid var(--button-primary-border);background:var(--button-primary-bg);color:var(--button-primary-text)}button.primary:hover,.button.primary:hover{background:var(--button-primary-bg-hover)}button.primary:active,.button.primary:active{background:var(--button-primary-bg-active)}p,label,input{font-size:1.3em}ul,li,p{padding:0;margin:0}.hidden{opacity:0}*:focus{outline:2px solid var(--focus-outline)}.container:focus{outline:none}button::-moz-focus-inner{border:0}input:required,input:invalid{box-shadow:none}textarea{font-family:inherit;font-size:inherit;box-sizing:border-box}@keyframes spin{0%{transform:rotate(0deg)}25%{transform:rotate(90deg)}50%{transform:rotate(180deg)}75%{transform:rotate(270deg)}100%{transform:rotate(360deg)}}.spin{animation:spin 1.5s infinite linear}.ellipsis::after{content:"\2026"}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0, 0, 0, 0);border:0}.inline-custom-emoji{width:1.4em;height:1.4em;margin:-0.1em 0;object-fit:contain;vertical-align:middle}.inline-emoji{font-family:PinaforeEmoji, sans-serif} @font-face{font-family:PinaforeRegular;src:local("BlinkMacSystemFont"),local("Segoe UI"),local("Roboto"),local("Oxygen-Sans"),local("Ubuntu"),local("Cantarell"),local("Fira Sans"),local("Droid Sans"),local("Helvetica Neue")}@font-face{font-family:PinaforeEmoji;src:local("Apple Color Emoji"),local("Segoe UI Emoji"),local("Segoe UI Symbol"),local("Twemoji Mozilla"),local("Noto Color Emoji"),local("EmojiOne Color"),local("Android Emoji")}body{margin:0;font-family:system-ui, -apple-system, PinaforeRegular, sans-serif;font-size:14px;line-height:1.4;color:var(--body-text-color);background:var(--body-bg);-webkit-tap-highlight-color:transparent;overflow-x:hidden}.main-content{contain:content;padding-top:42px}@supports (-webkit-overflow-scrolling: touch){.main-content{overflow-x:hidden}}@media (max-width: 991px){.main-content{padding-top:52px}}@media (max-width: 767px){.main-content{padding-top:62px}}main{position:relative;width:602px;max-width:100vw;padding:0;box-sizing:border-box;margin:30px auto 15px;background:var(--main-bg);border:1px solid var(--main-border);border-radius:1px;min-height:70vh}@media (max-width: 767px){main{margin:5px auto 15px}}footer{width:602px;max-width:100vw;box-sizing:border-box;margin:15px auto;border-radius:1px;background:var(--main-bg);font-size:0.9em;padding:20px;border:1px solid var(--main-border)}h1,h2,h3,h4,h5,h6{margin:0 0 0.5em 0;font-weight:400;line-height:1.2}h1{font-size:2em}a{color:var(--anchor-text);text-decoration:none}a:visited{color:var(--anchor-text)}a:hover{text-decoration:underline}input{border:1px solid var(--input-border);padding:5px;box-sizing:border-box}input[type=search]{-webkit-appearance:none}input,textarea{background:inherit;color:inherit}textarea{font-family:system-ui, -apple-system, PinaforeRegular, sans-serif, PinaforeEmoji}button,.button{font-size:1.2em;background:var(--button-bg);border-radius:2px;padding:10px 15px;border:1px solid var(--button-border);cursor:pointer;color:var(--button-text)}button:hover,.button:hover{background:var(--button-bg-hover);text-decoration:none}button:active,.button:active{background:var(--button-bg-active)}button[disabled],.button[disabled]{opacity:0.35;pointer-events:none;cursor:not-allowed}button.primary,.button.primary{border:1px solid var(--button-primary-border);background:var(--button-primary-bg);color:var(--button-primary-text)}button.primary:hover,.button.primary:hover{background:var(--button-primary-bg-hover)}button.primary:active,.button.primary:active{background:var(--button-primary-bg-active)}p,label,input{font-size:1.3em}ul,li,p{padding:0;margin:0}.hidden{opacity:0}*:focus{outline:2px solid var(--focus-outline)}.container:focus{outline:none}button::-moz-focus-inner{border:0}input:required,input:invalid{box-shadow:none}textarea{font-family:inherit;font-size:inherit;box-sizing:border-box}@keyframes spin{0%{transform:rotate(0deg)}25%{transform:rotate(90deg)}50%{transform:rotate(180deg)}75%{transform:rotate(270deg)}100%{transform:rotate(360deg)}}.spin{animation:spin 1.5s infinite linear}.ellipsis::after{content:"\2026"}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0, 0, 0, 0);border:0}.inline-custom-emoji{width:1.4em;height:1.4em;margin:-0.1em 0;object-fit:contain;vertical-align:middle}.inline-emoji{font-family:PinaforeEmoji, sans-serif}
</style> </style>
<style media="only x" id="theOfflineStyle"> <style media="only x" id="theOfflineStyle">
:root{--button-primary-bg: #ababab;--button-primary-text: #fff;--button-primary-border: #4d4d4d;--button-primary-bg-active: #9c9c9c;--button-primary-bg-hover: #b0b0b0;--button-bg: #e6e6e6;--button-text: #333;--button-border: #a7a7a7;--button-bg-active: #bfbfbf;--button-bg-hover: #f2f2f2;--input-border: #dadada;--anchor-text: #999;--main-bg: #fff;--body-bg: #fafafa;--body-text-color: #333;--main-border: #dadada;--svg-fill: #999;--form-bg: #f7f7f7;--form-border: #c1c1c1;--nav-bg: #999;--nav-border: gray;--nav-a-border: #999;--nav-a-selected-border: #fff;--nav-a-selected-bg: #b3b3b3;--nav-svg-fill: #fff;--nav-text-color: #fff;--nav-a-selected-border-hover: #fff;--nav-a-selected-bg-hover: #bfbfbf;--nav-a-bg-hover: #a6a6a6;--nav-a-border-hover: #999;--nav-svg-fill-hover: #fff;--nav-text-color-hover: #fff;--action-button-fill-color: #c7c7c7;--action-button-fill-color-hover: #d1d1d1;--action-button-fill-color-active: #a6a6a6;--action-button-fill-color-pressed: #878787;--action-button-fill-color-pressed-hover: #949494;--action-button-fill-color-pressed-active: #737373;--action-button-deemphasized-fill-color: #666;--action-button-deemphasized-fill-color-hover: #9e9e9e;--action-button-deemphasized-fill-color-active: #737373;--action-button-deemphasized-fill-color-pressed: #545454;--action-button-deemphasized-fill-color-pressed-hover: #616161;--action-button-deemphasized-fill-color-pressed-active: #404040;--settings-list-item-bg: #fff;--settings-list-item-text: #999;--settings-list-item-text-hover: #999;--settings-list-item-border: #dadada;--settings-list-item-bg-active: #e6e6e6;--settings-list-item-bg-hover: #fafafa;--toast-bg: #333;--toast-border: #fafafa;--toast-text: #fff;--mask-bg: #333;--mask-svg-fill: #fff;--mask-opaque-bg: rgba(51,51,51,0.8);--loading-bg: #ededed;--account-profile-bg-backdrop-filter: rgba(255,255,255,0.7);--account-profile-bg: rgba(255,255,255,0.9);--deemphasized-text-color: #666;--focus-outline: #bfbfbf;--very-deemphasized-link-color: rgba(153,153,153,0.6);--very-deemphasized-text-color: rgba(102,102,102,0.6);--status-direct-background: #ededed;--main-theme-color: #999;--warning-color: #e01f19;--alt-input-bg: rgba(255,255,255,0.7);--muted-modal-bg: transparent;--muted-modal-focus: #999;--muted-modal-hover: rgba(255,255,255,0.2);--compose-autosuggest-item-hover: #c4c4c4;--compose-autosuggest-item-active: #b8b8b8;--compose-autosuggest-outline: #ccc;--compose-button-halo: rgba(255,255,255,0.1)}:root{--scrollbar-face-color: #c7c7c7;--scrollbar-track-color: #f2f2f2;--scrollbar-border-radius: 0;--scrollbar-face-color-hover: #ccc;--scrollbar-face-color-active: #bfbfbf;--scrollbar-width: 12px;--scrollbar-height: 12px;--scrollbar-background-color: transparent} :root{--button-primary-bg: #ababab;--button-primary-text: #fff;--button-primary-border: #4d4d4d;--button-primary-bg-active: #9c9c9c;--button-primary-bg-hover: #b0b0b0;--button-bg: #e6e6e6;--button-text: #333;--button-border: #a7a7a7;--button-bg-active: #bfbfbf;--button-bg-hover: #f2f2f2;--input-border: #dadada;--anchor-text: #999;--main-bg: #fff;--body-bg: #fafafa;--body-text-color: #333;--main-border: #dadada;--svg-fill: #999;--form-bg: #f7f7f7;--form-border: #c1c1c1;--nav-bg: #999;--nav-border: gray;--nav-a-border: #999;--nav-a-selected-border: #fff;--nav-a-selected-bg: #b3b3b3;--nav-svg-fill: #fff;--nav-text-color: #fff;--nav-a-selected-border-hover: #fff;--nav-a-selected-bg-hover: #bfbfbf;--nav-a-bg-hover: #a6a6a6;--nav-a-border-hover: #999;--nav-svg-fill-hover: #fff;--nav-text-color-hover: #fff;--action-button-fill-color: #c7c7c7;--action-button-fill-color-hover: #d1d1d1;--action-button-fill-color-active: #a6a6a6;--action-button-fill-color-pressed: #878787;--action-button-fill-color-pressed-hover: #949494;--action-button-fill-color-pressed-active: #737373;--action-button-deemphasized-fill-color: #666;--action-button-deemphasized-fill-color-hover: #9e9e9e;--action-button-deemphasized-fill-color-active: #737373;--action-button-deemphasized-fill-color-pressed: #545454;--action-button-deemphasized-fill-color-pressed-hover: #616161;--action-button-deemphasized-fill-color-pressed-active: #404040;--settings-list-item-bg: #fff;--settings-list-item-text: #999;--settings-list-item-text-hover: #999;--settings-list-item-border: #dadada;--settings-list-item-bg-active: #e6e6e6;--settings-list-item-bg-hover: #fafafa;--toast-bg: #333;--toast-border: #fafafa;--toast-text: #fff;--mask-bg: #333;--mask-svg-fill: #fff;--mask-opaque-bg: rgba(51,51,51,0.8);--loading-bg: #ededed;--account-profile-bg-backdrop-filter: rgba(255,255,255,0.7);--account-profile-bg: rgba(255,255,255,0.9);--deemphasized-text-color: #666;--focus-outline: #bfbfbf;--very-deemphasized-link-color: rgba(153,153,153,0.6);--very-deemphasized-text-color: rgba(102,102,102,0.6);--status-direct-background: #ededed;--main-theme-color: #999;--warning-color: #e01f19;--alt-input-bg: rgba(255,255,255,0.7);--muted-modal-bg: transparent;--muted-modal-focus: #999;--muted-modal-hover: rgba(255,255,255,0.2);--compose-autosuggest-item-hover: #c4c4c4;--compose-autosuggest-item-active: #b8b8b8;--compose-autosuggest-outline: #ccc;--compose-button-halo: rgba(255,255,255,0.1)}:root{--scrollbar-face-color: #c7c7c7;--scrollbar-track-color: #f2f2f2;--scrollbar-border-radius: 0;--scrollbar-face-color-hover: #ccc;--scrollbar-face-color-active: #bfbfbf;--scrollbar-width: 12px;--scrollbar-height: 12px;--scrollbar-background-color: transparent}